Back to Performance
Performance
easy
mid

What techniques do you use to ensure performance and responsiveness in a web app?

Performance (loads fast) and responsiveness (feels snappy after load) need different fixes. Performance: SSR/SSG, code split, CDN, image optimization, preload critical, lazy below-fold. Responsiveness: avoid long tasks (>50ms blocks INP), useTransition for non-urgent updates, virtualize lists, debounce inputs, web workers for CPU work, requestAnimationFrame for animations. Measure both — LCP for load, INP for responsiveness — at p75 from real users.

8 min read·~5 min to think through

"Performance" lumps two things together: load speed (LCP, FCP, TTI) and interaction snappiness (INP, smooth scroll/drag). They need different techniques.

Load speed

The page needs to arrive fast.

Server:

  • SSR/SSG so HTML has content immediately.
  • Edge SSR (Cloudflare Workers, Vercel Edge) to bring TTFB to ~100ms globally.
  • CDN with long max-age on hashed assets.
  • Brotli compression on text.
  • HTTP/2 or HTTP/3.

Bundle:

  • Code split per route + per heavy widget.
  • Tree-shake unused exports.
  • Modern JS for modern browsers.
  • Audit deps quarterly; replace heavy with light.

Resources:

  • Preload LCP image + critical fonts.
  • AVIF/WebP for images, with srcset + sizes.
  • Inline critical CSS; async-load the rest.
  • Defer/async non-critical scripts.
  • Lazy-load below-fold images and iframes.

Don't lazy-load the LCP image — it's the #1 LCP anti-pattern.

Responsiveness

After the page loads, the user expects clicks, taps, and inputs to respond within 100ms. INP measures this.

Main-thread hygiene:

  • Long tasks (>50ms) block INP. Split them with scheduler.yield() or chunked setTimeout.
  • Don't do CPU-heavy work in event handlers — push to web workers.
  • Avoid layout thrash (interleaved layout reads + style writes).

Render perf:

  • Virtualize long lists.
  • React.memo + stable refs on expensive subtrees.
  • useTransition for non-urgent updates so input stays responsive:
jsx
const [filter, setFilter] = useState('');
const [isPending, startTransition] = useTransition();
function onChange(e) {
  setFilter(e.target.value); // urgent — input stays snappy
  startTransition(() => {
    setResults(slowFilter(items, e.target.value)); // deferred
  });
}

Animations:

  • Use CSS transform + opacity (compositor-only, no layout/paint).
  • requestAnimationFrame for JS-driven animations.
  • will-change: transform to promote to its own layer (use during animation, remove after).

Inputs:

  • Debounce search/autocomplete.
  • Throttle scroll/drag/mousemove handlers (or RAF-coalesce).
  • Cancel previous in-flight requests on new keystrokes.

Combined: scrolling a long list

A long list both loads slowly and scrolls poorly without intervention.

  • Virtualize (windowing) → constant DOM size regardless of data length.
  • Paginate or cursor-fetch data → bounded memory.
  • Memoize row components → no re-render on parent updates.
  • content-visibility: auto on offscreen sections → browser skips render.
  • AbortController on data fetches → cancel stale requests.

Web workers for CPU

js
const worker = new Worker('parse.js');
worker.postMessage(rawCsv);
worker.onmessage = e => setData(e.data);

Heavy parse, compression, image manipulation off the main thread. Comlink wraps the postMessage API nicely.

Concurrent React features

  • useDeferredValue — defer dependent re-render until user stops typing.
  • useTransition — mark state updates as low-priority.
  • Suspense — show fallback while data/code loads, with concurrent rendering avoiding "render-then-suspend" jank.

What to measure

  • Load: LCP, FCP, TTFB, total bytes, time to first paint.
  • Responsiveness: INP, total blocking time, long-task count.
  • Per device class + network + geo + route.
  • p75 and p95.

Process

  1. Ship RUM.
  2. Find worst metric × worst page × worst segment.
  3. Profile (DevTools Performance + Coverage).
  4. Identify dominant cost: bundle parse, image, third-party, render.
  5. Apply the matching technique above.
  6. Validate metric movement on real users.

Common pitfalls

  • Optimizing LCP but ignoring INP — users complain about laggy interactions even when load is fast.
  • Memoizing everything in React — overhead without payoff.
  • Lazy-loading the LCP image.
  • Big third-party scripts (analytics, ads, chat) eating the perf budget.
  • No CI guard — wins regress without anyone noticing.

Mental model

Two metrics, two playbooks. LCP/FCP fixes: ship less, ship earlier, serve faster. INP fixes: keep the main thread free, defer work, slice long tasks. Plan for both; measure both; never trade one off without knowing it.

Follow-up questions

  • What's the difference between LCP and INP investigation?
  • How does useTransition help INP?
  • When should you reach for a web worker?
  • What's content-visibility and when does it help?

Common mistakes

  • Treating perf as one metric instead of load + responsiveness.
  • Optimizing only Lighthouse score; ignoring INP.
  • Lazy-loading LCP image.
  • Animating top/left instead of transform.
  • Memoizing everything in React.
  • No CI perf budget.

Performance considerations

  • Compounding wins: smaller bundle → faster TTI → less long-task pressure → better INP → happier users. Web workers and virtualization decouple main-thread budget from data volume. Concurrent React features hide work from the user.

Edge cases

  • Low-end mobile devices have very different perf profiles — test on a throttled CPU.
  • Background tabs throttle timers but receive WS messages — design for that.
  • Service workers can serve cached content offline — perf doesn't apply the same way.
  • Bfcache (back/forward cache) makes return visits feel instant — design pages to be bfcache-eligible.
  • Low-power mode on iOS throttles JS aggressively.

Real-world examples

  • Linear's snappiness comes from local-first sync + tight INP discipline.
  • Figma uses Canvas/WebGL for the editor to bypass DOM render bottlenecks.
  • Pinterest cut both LCP and INP in their PWA rewrite.

Senior engineer discussion

Seniors plan for both load and responsiveness from the start. They measure with RUM, set budgets in CI, and pick techniques per problem. They also know the third-party tax (analytics, ads, chat widgets) often dominates and design containment from day one.

Related questions