Web Performance Optimization

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.

Core Web Vitals

MetricWhat it measuresTargetWhat hurts it
LCP (Largest Contentful Paint)When the biggest visible element appears< 2.5sSlow server, large unoptimised images, render-blocking JS/CSS
INP (Interaction to Next Paint)Latency from user action to next render< 200msLong-running JS, layout thrash, blocking the main thread
CLS (Cumulative Layout Shift)Visual stability< 0.1Images without dimensions, fonts loading late, ads inserting

These three correlate with revenue, engagement, and SEO ranking. They're worth the engineering attention.

LCP: making the page appear fast

LCP measures when the largest above-the-fold element rendered. Optimising it:

Server-side render the critical content

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.

Optimise images

Images are usually the LCP element on content sites. Optimisations:

Preload key resources

<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."

Reduce render-blocking resources

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.

Cache aggressively

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: keeping interactions responsive

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.

Don't block the main thread

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:

Avoid layout thrashing

Reading layout properties (offsetWidth, getBoundingClientRect) and then writing styles forces synchronous reflow. Loop of read-write-read-write = layout thrash. Batch reads, then writes.

Defer non-critical work

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', ...));
}

Reduce JS bundle size

Less JS = faster parse + execute. See bundle size below.

CLS: avoiding layout shifts

CLS measures unexpected movements. Caused by:

Fixes:

Most CLS issues are dimension-attribute issues. Fix those first.

Bundle size

For SPAs and React apps specifically, JS bundle size is a major lever.

Track:

Tactics:

Code splitting

const AdminPanel = React.lazy(() => import('./AdminPanel'));

Routes / heavy features load only when navigated to. Webpack / Vite handle this with dynamic imports.

Tree shaking

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).

Replace heavy dependencies

Common offenders:

A webpack-bundle-analyzer (or Vite equivalent) report shows the targets.

Minification + compression

Standard. Brotli over gzip for ~15% better compression. CDN handles this; verify it's on.

Server Components for SSR

React Server Components don't ship JS for non-interactive UI. For most content sites, this drops the bundle dramatically.

Network

Database / API performance

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.

Measurement

You cannot optimise what you don't measure. Two layers:

Synthetic monitoring

These run on controlled machines / networks. Useful for relative comparisons; less useful for predicting real-user experience.

Real User Monitoring (RUM)

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.

Anti-patterns

A pragmatic optimisation playbook

For a site with poor performance:

  1. Measure with Lighthouse + RUM. Identify which Vital is failing.
  2. For LCP: check image sizes; check server response time; check render-blocking resources.
  3. For INP: profile main-thread work on common interactions; identify long tasks.
  4. For CLS: identify which elements shift; add dimensions / reservations.
  5. Set budgets in CI (Lighthouse CI) so regressions don't ship.
  6. Track in production with Web Vitals + your analytics; iterate.

Most sites can hit Core Web Vitals targets with a week of focused work. Continuous discipline keeps them there.

Further reading