Capstone: Auditing and Fixing a Slow Page

Performance & Core Web Vitals — Capstone: Auditing and Fixing a Slow Page
Performance & Core Web Vitals
Chapter 9 · Capstone: Auditing and Fixing a Slow Page

🏁 Capstone: Auditing and Fixing a Slow Page

Every previous chapter built one piece in isolation. This capstone walks one deliberately slow example page through a real audit — LCP, then CLS, then INP, then monitoring — using every technique from the course, in the order a real audit would actually apply them.

The Starting Page

A blog-style product page with a realistic pile of problems: a full-resolution JPEG hero with no dimensions and no preload, a render-blocking synchronous script in <head>, an ad slot with no reserved space, a web font causing visible reflow, an eagerly-loaded third-party chat widget, a scroll handler doing layout-thrashing reads/writes, and no monitoring of any kind.

1️⃣ Measure First: Lighthouse Baseline

Before touching anything, run Lighthouse (Chapter 1) to get a baseline and see the sub-metric breakdowns from Chapters 2 and 4: LCP's four-part timeline flags a long resource load delay; CLS flags the hero image and the ad slot; INP flags a long task in the scroll handler.

2️⃣ Fix LCP

<link rel="preload" as="image" href="/hero.webp" fetchpriority="high"> <script src="/bundle.js" defer></script> <picture> <source srcset="hero.avif" type="image/avif"> <source srcset="hero.webp" type="image/webp"> <img src="hero.jpg" width="1200" height="600" alt="..."> </picture>

Preloading and format-optimizing the hero image (Chapters 2 & 5), and deferring the render-blocking script (Chapter 2), directly attack the long resource load delay Lighthouse flagged.

3️⃣ Fix CLS

.ad-slot { min-height: 250px; } /* Ch.3 */ @font-face { font-family: "BodyFont"; font-display: swap; size-adjust: 97%; /* Ch.3 — narrows the fallback-font metric gap */ }

The hero image's width/height from Step 2 already reserved its space; the ad slot's min-height and the font's size-adjust close the remaining two CLS sources (Chapter 3).

4️⃣ Fix INP

// Facade for the chat widget — Ch.7 chatButton.addEventListener("click", async () => { const { initChat } = await import("./chat-widget.js"); initChat(); }); // Batched, rAF-throttled scroll handler — Ch.4 & Ch.7 let ticking = false; window.addEventListener("scroll", () => { if (!ticking) { requestAnimationFrame(() => { updateProgressBar(); ticking = false; }); ticking = true; } });

The chat widget's code no longer loads (or competes for the main thread) until a user actually wants it — the same facade pattern from Chapter 7, combined with Chapter 6's dynamic import. The scroll handler's layout-thrashing reads/writes are batched and throttled to one update per frame.

5️⃣ Add Monitoring

import { onLCP, onINP, onCLS } from "web-vitals"; onLCP(sendToAnalytics); onINP(sendToAnalytics); onCLS(sendToAnalytics); // Ch.8

Wiring up the web-vitals library confirms these fixes hold up for real visitors, not just in Lighthouse — and a CI performance budget (Chapter 8) prevents the page from quietly regressing again.

MetricBeforeAfter
LCP4.8s (Poor)2.1s (Good)
CLS0.34 (Poor)0.05 (Good)
INP620ms (Poor)180ms (Good)

The Reusable Methodology

Measure (lab) → fix LCP → fix CLS → fix INP → add monitoring (field) → set a budget — this order isn't specific to this one page; it's a repeatable process for auditing any real page.

What Changed the Experience

The page went from a slow, jumpy, laggy experience to one where content appears fast, nothing moves unexpectedly, and every tap responds instantly.

💻 Coding Challenges

Challenge 1: Add a Below-the-Fold Image Fix

The page also has a below-the-fold gallery image with no loading attribute and no dimensions. Write the corrected <img> tag, citing which two chapters' techniques it combines.

Goal: Practice applying two independent fixes to the same element correctly.

→ Solution

Challenge 2: Explain Why Re-Measuring After Step 2 Matters

After deferring the render-blocking script in Step 2, explain why re-running Lighthouse before moving to Step 3 is a good idea, rather than assuming the remaining CLS/INP issues are unaffected.

Goal: Practice reasoning about how fixes can interact rather than stack purely independently.

→ Solution

Challenge 3: Apply the Methodology to a New Page

A completely different page — a documentation site with a large syntax-highlighted code sample above the fold and a heavy search-as-you-type widget — is slow. Walk through this chapter's five-step methodology at a high level for this page, without writing code.

Goal: Practice applying the reusable audit process to a page this course never specifically covered.

→ Solution

⚠️ Gotcha: Fixing Everything, Then Measuring Once at the End

It's tempting to apply every fix from this chapter in one batch and check Lighthouse only once, at the end. Fixes can interact: deferring the render-blocking script in Step 2 changes exactly when other resources are discovered, which — per Chapter 2's own point that the LCP candidate can change mid-load — could shift which element actually becomes the LCP element, sometimes revealing a NEW problem that wasn't visible before (an image that only now becomes the LCP candidate, and needs its own preload). Re-audit after each round of fixes, not just once at the end — treat this five-step process as iterative, not a single linear pass.

🎓 Course Complete

This closes out Performance & Core Web Vitals — from why the three metrics exist, through LCP, CLS, and INP individually, image optimization and lazy loading, JavaScript performance at the page level, measuring lab and field data, and finally this capstone auditing a real page end to end.