Web performance is "how fast does the page feel." Real users don't time loads with stopwatches; they form impressions of speed in the first 200ms and decide whether to stay around. Performance work is about influencing those impressions efficiently.
Google's Core Web Vitals (LCP, INP, CLS) standardised what "felt fast" actually meant by 2020; they've evolved with INP replacing FID in 2024. As of 2026, this is still the operational target.
| Metric | What it measures | Target | What hurts it |
|---|---|---|---|
| LCP (Largest Contentful Paint) | When the biggest visible element appears | < 2.5s | Slow server, large unoptimised images, render-blocking JS/CSS |
| INP (Interaction to Next Paint) | Latency from user action to next render | < 200ms | Long-running JS, layout thrash, blocking the main thread |
| CLS (Cumulative Layout Shift) | Visual stability | < 0.1 | Images without dimensions, fonts loading late, ads inserting |
These three correlate with revenue, engagement, and SEO ranking. They're worth the engineering attention.
LCP measures when the largest above-the-fold element rendered. Optimising it:
Server-rendered HTML arrives faster than client-rendered HTML waits for JS to execute. For most pages, SSR is the difference between LCP < 2s and LCP > 4s.
Frameworks: Next.js App Router, Remix, SvelteKit. All ship server-rendered HTML for initial loads.
Images are usually the LCP element on content sites. Optimisations:
<img loading="lazy"> for below-the-fold; loading="eager" for above.srcset and sizes so mobile gets smaller files.<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preconnect" href="https://api.example.com">
Tells the browser "fetch this; don't wait for the parser to discover it."
CSS in <head> blocks rendering. Inline critical CSS; defer the rest:
<style>/* critical above-the-fold styles */</style>
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">
Synchronous JS blocks parsing. Use async / defer on <script> tags. For React, ship the framework code at the very top so hydration can start.
Static assets: Cache-Control: public, max-age=31536000, immutable with hashed filenames. Once cached, never re-fetched.
HTML: Cache-Control: public, max-age=0, must-revalidate plus an ETag. Lets the browser cache the document but re-validate on each visit.
CDN in front of everything reduces TTFB dramatically.
INP measures how long after a user action (click, type, tap) the next frame paints. Most modern web apps fail INP because they're doing too much JS work on user interaction.
JavaScript runs on the main thread. Long-running JS prevents the browser from responding to clicks. The hard rule: any single task should be under ~50ms.
When a task is necessarily long:
requestIdleCallback or scheduler.postTask.startTransition in React for non-urgent updates.Reading layout properties (offsetWidth, getBoundingClientRect) and then writing styles forces synchronous reflow. Loop of read-write-read-write = layout thrash. Batch reads, then writes.
When the user clicks, do the minimum to update the UI; defer logging, analytics, and side effects:
function onClick() {
// immediate UI update
setOpen(true);
// deferred non-critical
queueMicrotask(() => analytics.track('opened', ...));
}
Less JS = faster parse + execute. See bundle size below.
CLS measures unexpected movements. Caused by:
Fixes:
width and height attributes on images / videos. Browser reserves space.aspect-ratio CSS for responsive media.font-display: optional or swap with similar-metric fallbacks; consider size-adjust to align fallback metrics.Most CLS issues are dimension-attribute issues. Fix those first.
For SPAs and React apps specifically, JS bundle size is a major lever.
Track:
Tactics:
const AdminPanel = React.lazy(() => import('./AdminPanel'));
Routes / heavy features load only when navigated to. Webpack / Vite handle this with dynamic imports.
Unused exports get eliminated at build time. Use ES modules; avoid import * as patterns; check that your dependencies are tree-shake-friendly (some popular libraries aren't).
Common offenders:
lodash-es.A webpack-bundle-analyzer (or Vite equivalent) report shows the targets.
Standard. Brotli over gzip for ~15% better compression. CDN handles this; verify it's on.
React Server Components don't ship JS for non-interactive UI. For most content sites, this drops the bundle dramatically.
Page speed often bottlenecks on the API. Frontend optimisation can't help if the API takes 3 seconds.
For most user-facing applications in 2026, server time accounts for 30-60% of the total time-to-LCP. Optimising it pays.
You cannot optimise what you don't measure. Two layers:
These run on controlled machines / networks. Useful for relative comparisons; less useful for predicting real-user experience.
RUM tells you what real users experience. If lab tests look great but RUM is bad, real-world conditions (slow networks, low-end devices, browser variance) are revealing themselves.
For production apps, both. Synthetic catches regressions; RUM tells you whether the site actually feels fast.
For a site with poor performance:
Most sites can hit Core Web Vitals targets with a week of focused work. Continuous discipline keeps them there.