Measure, Diagnose, and Optimize Core Web Vitals
Practical 15 - Measure, Diagnose, and Optimize Core Web Vitals
Related: Chapter 15 · Lecture slides
Objective
Diagnose, instrument, and remediate a deliberately degraded municipal web application suffering from severe real-world performance defects:
- Poor LCP (4.8s): An uncompressed, non-prioritized hero banner image buried in an asynchronous client waterfall.
- Severe CLS (0.38): Layout shifts caused by unsized images and late-injected municipal emergency announcements.
- Sluggish INP (380ms): A long task on the main thread executing expensive synchronous sorting on every filter keystroke.
- DOM Bloat & Memory Jitter: A 5,000-row table rendering tens of thousands of active DOM nodes.
You will formulate hypotheses, capture baseline DevTools traces under 4x CPU throttling, apply targeted architectural remedies, and verify that the 75th percentile (p75) metrics meet Google Core Web Vitals standards.
Workspace Setup
Set up a local performance testing sandbox:
Stage-by-Stage Implementation
Stage 1: The Bottleneck Baseline & Native Instrumentation
In src/telemetry.ts, implement in-browser Core Web Vitals monitoring using the native PerformanceObserver API:
Baseline Capture Instructions:
- Start the development server (
npx vite). - Open Chrome DevTools, open the Performance tab, and configure:
- CPU: 4x slowdown (simulating a mid-tier Android device).
- Network: Fast 3G.
- Record a 5-second trace while reloading the page, typing “commercial” into the search bar, and scrolling the permit table.
- Record your baseline metrics:
- LCP: ~4,800 ms (Poor)
- INP: ~380 ms (Poor)
- CLS: ~0.380 (Poor)
Stage 2: Remediating LCP (Largest Contentful Paint)
The Diagnosis:
In the Performance trace, the LCP element is a municipal hero banner. The trace reveals a massive Resource Load Delay: the browser does not discover the image URL until after app.js downloads, parses, and fetches /api/config.json.
The Architectural Fix:
- Hoist the LCP image into the initial static HTML document.
- Add
<link rel="preload">in the document<head>. - Set
fetchpriority="high"and supply responsive modern formats:
Stage 3: Eliminating Cumulative Layout Shift (CLS)
The Diagnosis:
The trace reveals two major layout shifts:
- The hero image has no reserved aspect ratio, collapsing to 0px height before abruptly expanding to 400px when the image decodes.
- A municipal emergency notification banner is injected dynamically at the top of the viewport 800ms after load, pushing all main content down by 75 pixels.
The Architectural Fix:
- Enforce aspect-ratio reservation in CSS.
- Reserve dedicated layout slots for late-injected dynamic content:
Stage 4: Taming Interaction to Next Paint (INP)
The Diagnosis:
When a user types into the permit filter input, the browser freezes for 320ms. The Performance flame chart shows a single Long Task (>50ms) executing expensive regex matching and sorting over an array of 5,000 objects synchronously on the main thread during the keydown event.
The Architectural Fix:
- Provide immediate, zero-latency visual feedback for the user’s keystroke.
- Yield execution back to the browser’s rendering engine using
scheduler.yield()(or asetTimeout(0)microtask fallback) so the browser can paint the typed letter before calculating the heavy list:
Stage 5: Virtualizing the Large Permit Table
The Diagnosis:
Rendering 5,000 table rows generates 35,000 DOM nodes. Every DOM manipulation triggers heavy recalculations, and scrolling stutters at 24fps.
The Architectural Fix:
Implement a Virtual Windowed Scroller that renders only the ~30 visible rows currently inside the viewport:
Verification and Testing Matrix
Record your Before and After measurements under identical throttling conditions (4x CPU, Fast 3G):
| Performance Metric | Baseline (Broken) | Target Threshold | Remediated (Measured) | Status |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | 4,800 ms | $\le$ 2,500 ms | ~1,650 ms | PASS |
| INP (Interaction to Next Paint) | 380 ms | $\le$ 200 ms | ~55 ms | PASS |
| CLS (Cumulative Layout Shift) | 0.380 | $\le$ 0.100 | 0.012 | PASS |
| Active DOM Node Count | 35,420 nodes | $\le$ 1,500 nodes | 420 nodes | PASS |
| Total Blocking Time (TBT) | 890 ms | $\le$ 200 ms | ~40 ms | PASS |
Deliverables & Submission Checklist
-
src/telemetry.ts: Working nativePerformanceObserverimplementation for LCP, INP, and CLS. -
index.html: Optimized LCP markup with<link rel="preload">,fetchpriority="high", and AVIF/WebP<picture>. -
src/styles.css: CSS rules enforcingaspect-ratioandmin-heightslot reservations. -
src/searchEngine.ts: Yielding filter function eliminating long tasks usingscheduler.yield(). -
src/virtualTable.ts: Functional virtual scroller maintaining active DOM nodes under 1,000. - Completed Verification and Testing Matrix documenting verified p75 improvements.