JavaScript Performance at the Page Level

Performance & Core Web Vitals — JavaScript Performance at the Page Level
Performance & Core Web Vitals
Chapter 7 · JavaScript Performance at the Page Level

⚙️ JavaScript Performance at the Page Level

Chapter 4 introduced long tasks and INP; Chapter 6 covered what code loads and when. This chapter is about the JS that does run — specifically, high-frequency event handlers — applying JS Advanced's debounce and throttle at the whole-page level, where the stakes are the entire page's Core Web Vitals score, not just one widget.

The Problem: Naive High-Frequency Handlers

A scroll event can fire dozens of times per second. If a handler does anything expensive — DOM queries, layout reads, re-renders — on every single fire, that's dozens of expensive operations competing for the exact main thread Chapter 4 already flagged as the INP bottleneck.

⏸️ Debounce and Throttle, Revisited

Debounce

Wait until events stop firing for a pause, then run once. Good for "final state" work — a resize handler recalculating layout only once the user has actually finished resizing.

Throttle

Run at most once per fixed interval, no matter how many events fire. Good for "ongoing feedback" work — a scroll-based progress bar that needs periodic updates during the scroll, not just after it.

Same concepts from JS Advanced — but here the stakes are the whole page's measured responsiveness, not one component quietly glitching.

Layout Thrashing

A particularly damaging pattern: reading a layout property (offsetHeight, getBoundingClientRect()) then writing a style change, then reading again — interleaved in a loop. Each read after a write forces the browser to synchronously recalculate layout, since it can no longer batch reads once a write has invalidated them:

Interleaved vs Batched

Thrashing (Interleaved)

Read height, write a new width, read height again, write again — each read forces a fresh, synchronous layout recalculation.

Batched

Read every needed value first, then perform every write afterward — the browser recalculates layout once, not once per element.

requestAnimationFrame

For anything visual, wrapping DOM writes in requestAnimationFrame aligns them with the browser's actual paint cycle instead of fighting it — and pairs naturally with throttling:

rAF-Throttled Scroll Handler

let ticking = false; window.addEventListener("scroll", () => { if (!ticking) { requestAnimationFrame(() => { updateProgressBar(); // runs at most once per frame ticking = false; }); ticking = true; } });

📦 Third-Party Scripts, Revisited

Chapter 4 flagged third-party scripts as a common, easy-to-overlook INP cost. This chapter is where something actually gets done about it: loading with async/defer instead of blocking, and using a facade pattern for expensive embeds — a static thumbnail image that only loads the real player's JS on click, instead of loading it upfront for every visitor regardless of whether they ever interact with it.

💻 Coding Challenges

Challenge 1: Debounce a Resize Handler

Write a debounced version of a resize event handler that calls recalculateLayout() only after resizing has stopped for 200ms.

Goal: Practice implementing debounce for a "final state" use case at the page level.

→ Solution

Challenge 2: Fix a Layout-Thrashing Loop

Given a loop that reads el.offsetHeight and immediately writes el.style.width for each of 50 elements in sequence, rewrite it to batch all reads before all writes.

Goal: Practice eliminating layout thrashing by separating read and write phases.

→ Solution

Challenge 3: Build a Facade for an Embedded Video

Write HTML/JS for a video embed that initially shows only a clickable thumbnail image, and only inserts the real (heavy) video iframe into the DOM after the user clicks it.

Goal: Practice implementing the facade pattern for a genuinely expensive third-party embed.

→ Solution

⚠️ Gotcha: Debounce and Throttle Are Not Interchangeable

Choosing the wrong one doesn't just make something "less optimal" — it produces a subtly broken experience. Debouncing a scroll-based progress indicator means the user never sees continuous feedback while scrolling, defeating the entire point of a progress bar — they'd only see it update once scrolling stops. Throttling a search-as-you-type input, meanwhile, fires periodically during typing, triggering wasted, flickering intermediate searches for partial queries the user never intended to search — debounce, waiting for the pause in typing, is what that interaction actually wants. Match the technique to whether the interaction needs ongoing feedback (throttle) or a single final result (debounce), not by habit or whichever one comes to mind first.

🎯 What's Next

With the JavaScript itself under control, the next chapter turns to proving it actually worked: Measuring & Monitoring — lab data vs field data, the web-vitals library, and setting a performance budget.