Web Performance Optimization: A Field Guide

Web Performance Optimization: A Field Guide

Performance work fails when it starts from tips instead of measurements. This guide is ordered the way real optimization goes: measure, fix the biggest class of waste, measure again.

Measure like a user

Lab tools are for debugging; field data is for deciding. The three numbers worth tracking:

  • Largest contentful paint — when the main content appears.
  • Interaction to next paint — how long taps and clicks feel.
  • Cumulative layout shift — how much the page jumps around.
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    report('LCP', entry.startTime);
  }
}).observe({ type: 'largest-contentful-paint', buffered: true });

Instrument first. Every later section assumes you can see the effect of a change.

Ship less JavaScript

The most reliable optimization is deletion. After that, split what remains along user intent:

const Editor = lazyImport(() => import('./editor.js'));

Route-level splitting is table stakes; the deeper wins hide in conditional features — admin panels, charts below the fold, third-party widgets behind consent.

Audit your dependency graph quarterly. A date library here, a utility belt there, and the baseline bundle doubles without any feature shipping.

Get images under control

Images dominate transfer size on most marketing pages. The checklist:

  1. Serve modern formats with fallbacks.
  2. Size images to their rendered dimensions — srcset and sizes, not CSS scaling.
  3. Lazy-load below the fold, but never the hero image.
  4. Reserve space with width/height attributes to kill layout shift.
<img src="/hero-1200.avif" srcset="/hero-600.avif 600w, /hero-1200.avif 1200w" sizes="(max-width: 640px) 100vw, 1200px" width="1200" height="640" alt="Product dashboard" />

Respect the main thread

Long tasks make fast pages feel slow. Break work into scheduler-sized pieces:

async function processRows(rows) {
  for (const chunk of partition(rows, 500)) {
    render(chunk);
    await scheduler.yield();
  }
}

Move parsing and transformation to a worker when chunks alone are not enough. The main thread’s job is responding to the user, not crunching data.

Cache with intent

  • Immutable, hashed assets: cache for a year.
  • HTML: revalidate always.
  • APIs: stale-while-revalidate where UX tolerates it.

The mistake to avoid is the accidental middle: assets cached long enough to go stale, not long enough to save requests.

Conclusion

Measure in the field, delete before you optimize, split by intent, discipline your images, and keep the main thread free. Performance is not a sprint before launch — it is the habit of checking the numbers after every meaningful change.