PROJECT

Web Performance Lab

Three interactive demos that make the usually-invisible costs of web performance visible: turn on a real pathology, watch a live measurement move, then flip the fix and watch it recover. Everything below runs entirely in your browser — nothing is uploaded, and there is no server involved.

01 — Live demosTOGGLE A CAUSE, WATCH THE EFFECT

Demo 1 — INP playground

A filterable, sortable catalog of 500 items. Toggle real pathologies — a blocking click handler, layout thrashing, a synchronous filter, a heavy synchronous JSON parse — and watch the timeline strip and worst-interaction readout below react as you click, sort, and type. Flip the matching fix to see it recover.

Loading INP playground…

Demo 2 — Font loading strategies

The same article snippet and the same webfont, loaded three ways: blocking, swap with an untuned fallback, and swap with a fallback tuned to the real font's metrics. Cumulative Layout Shift is measured independently inside each frame.

Loading font-loading demo…

Demo 3 — Image pipeline

Encode an image to WebP and AVIF at three quality levels using WASM codecs running in a Web Worker. Nothing is uploaded — it's decoded, encoded, and compared entirely in your browser.

Loading image pipeline…
02 — The problemWHY THIS EXISTS

Performance regressions are usually invisible until someone measures them. A 250ms blocking handler, a filter that re-renders 500 rows on every keystroke, a webfont that shifts a whole article when it swaps in — none of these show up by reading the code. They show up as a number that moves, and by the time that number is a complaint from a real user, the cause is several deploys away.

The three demos above exist to close that gap: each one lets you flip a real pathology on and off and watch the exact metric that pathology damages — INP, Cumulative Layout Shift, transfer size — move in real time, using the same instrumentation (Performance Observer entry types, the web-vitals library) that production monitoring uses.

03 — Decisions & tradeoffsWHAT I CHOSE, AND WHY
  • Catalog rows in the INP demo are deliberately left unmemoized, so the “full re-render on every keystroke” pathology stays honest rather than quietly optimized away by React.memo.
  • There's no dedicated fix control for the 250ms blocking click handler. Some jank causes don't have a clever fix — the fix is not doing that work synchronously at all — and the page says so rather than inventing one.
  • The filter demo presents debounce and useDeferredValue side by side, not as a single “correct” fix. Debounce delays the expensive work; deferring still schedules the same work, just at low priority so React can interrupt it for the next keystroke. They trade off differently.
  • Each demo's JS is lazy-loaded on scroll via an IntersectionObserver-gated dynamic import, so the lab about performance doesn't itself ship a heavy initial bundle. See the numbers below.
  • The image demo fetches each codec's .wasm binary itself from a known static path and hands it to the codec's manual init() override, rather than relying on the package's own import.meta.url-based resolution — that path is unreliable under Turbopack, which this project uses in development.
  • AVIF encoding runs single-threaded here, because this site doesn't set the cross-origin isolation headers (COOP/COEP) that multi-threaded WASM requires. Widening the site's isolation posture just for this demo wasn't worth the tradeoff, so the codec quietly falls back to its single-threaded build instead.
  • The font demo self-hosts its own copy of the site's display font at a demo-only URL, separate from the copy used site-wide, so the throttle replay is measuring a real network fetch instead of a browser cache hit.
04 — Known limitsWHERE THIS LAB IS HONEST ABOUT ITSELF
  • These pathologies are synthetic and isolated on purpose. Real-world jank is usually several smaller causes stacked together, not one clean toggle.
  • INP attribution (input delay / processing / presentation) depends on the Event Timing API, which Safari and Firefox don't support the same way Chromium does — the breakdown may be unavailable outside Chromium browsers, and Long Animation Frame entries are Chromium-only today.
  • WASM AVIF encoding is meaningfully slower than WebP on low-end or older devices, especially since it runs single-threaded here (see Decisions & tradeoffs).
  • The font demo's Cumulative Layout Shift measurement is approximate — it's measuring a small isolated snippet, not a full page, and Safari doesn't implement the Layout Instability API at all, so CLS is unavailable there.
  • Download-time estimates in the image demo assume steady 4G/3G throughput with no other latency on the connection — real networks are burstier than that.
05 — Numbers observedMEASURED, NOT ESTIMATED
  • This route ships roughly 6 KB of route-specific JavaScript on first load; each demo's own code, worker, and (for the image demo) WASM codec is a separate chunk that only loads once you scroll to it — measured from the production build output.
  • With every INP pathology enabled at once, the worst observed interaction in Demo 1 measured well over 1,000ms; with every fix enabled instead, the worst interaction stayed under 100ms on the same machine.
  • At quality “low,” both WebP and AVIF encodes of the bundled sample image came in under a third of the original JPEG's size; AVIF's “high” quality encode was still smaller than the original at visually comparable quality.