Part 10 · 4 chapters · ~30 min
Putting It Together
Five diagnoses walked end to end as chains of panels ending in numbers: a slow load, a janky scroll with three causes, memory growth from a cache without a bound, a cache broken by a directive on the wrong resource, and a hydration mismatch. Then the course in one table.
26
Diagnosis 1: a slow load
the chain
- Lighthouse and the field: "the listing page takes forever on my phone". Field p75: LCP 4.8 s, INP 180 ms, CLS 0.03. LCP is the problem; the others are not. Lab audits: render-blocking CSS, an unsized 3.2 MB hero, 380 KB of unused JavaScript. Hypothesis: the hero.
- Network on Slow 4G: TTFB 600 ms; two render-blocking stylesheets, one on a font origin paying DNS, TCP and SSL; the hero discovered late (Initiator: a CSS background in a late stylesheet), 3.2 MB, Low priority behind twelve thumbnails. DCL 2.1 s, load 6.4 s.
- Performance, record and reload at 4× CPU: Parse HTML blocked 400 ms by a sync head script; a 700 ms Evaluate Script for the vendor bundle; LCP at 4.6 s. The breakdown: TTFB 600, resource load delay 1,900, resource load time 1,800, render delay 300. Two phases are the whole problem.
- Coverage: vendor.js 91% unused on this route; app.css 88% unused. Not the LCP cause; the TBT cause.
- Fixes, one per number: the hero as an
<img fetchpriority="high">in the HTML with a preload (load delay 1,900 → ~0); a sized 800 px WebP via srcset (load time 1,800 → 150); defer the head script, split the vendor bundle by route, preconnect the font origin, inline the critical CSS. - Measure again at the same throttle: lab LCP 1.9 s; field p75 over the following 28 days: 2.3 s. Before and after traces on the PR.
the sentence
"LCP was 4.6 s because the hero was discovered late (1.9 s) and too big (1.8 s); it is 1.9 s now because it is in the HTML, prioritised and sized." The PR description is the diagnosis.
go to the lab
- Run this chain on
/perf/lcp-phasesfor each variant: Lighthouse mobile, then Network on Slow 4G, then Performance record-and-reload with the LCP breakdown. Match each variant to its dominant phase before reading the source. Then do the same on/images/lcp: three strategies, three breakdowns.
DIAGNOSIS 1: A SLOW LOAD
from "the page takes forever" to three numbers and three fixes
swipe the figure sideways, or tap expand for full screen
1/6
the complaint
The complaint: "the listing page takes forever on my phone". Lighthouse mobile: Performance 41; field LCP p75 4.8 s, INP 180 ms, CLS 0.03. The field says LCP is the problem; INP and CLS are fine. The lab audits list render-blocking CSS (1.1 s), an unsized hero at 3.2 MB, 380 KB of unused JavaScript.
27
Diagnosis 2: a janky scroll
the chain
- Rendering drawer first, no recording: Scrolling performance issues shows the list container red with "has a non-passive wheel listener"; Paint flashing shows the sticky header flashing on every scroll step; Layer borders show one layer. Two causes in thirty seconds.
- Performance, a scroll at 4× CPU: Frames red throughout; Interactions shows each wheel event with a long input-delay whisker (the compositor waiting for the listener); Main per frame: a 30 ms wheel task, Recalculate Style, Layout, a Paint of 1440 × 80 (the header), and a 12 ms React render.
- Bottom-Up by self time over the scroll range: the listener's work function, Paint, and a React render from a scroll position stored in state (a render per event: the React course part 10). Three causes with three numbers: 30, 6, 12 ms per event.
- Fix one:
passive: true(or move the work off the scroll path). Record: no input delay; frames yellow rather than red; the thread still busy. Smooth to the hand, still a problem for a slower device. - Fix two and three: the header's scroll-dependent shadow → promote it or animate an opacity pseudo-element (Paint gone); scroll position in state → a ref with rAF-throttled reads, or a store with a rarely-changing selector (the render gone).
- Measure again: no red at 4× CPU; no input delay; Main idle between frames.
the sentence
"Scroll jank had three causes: a blocking listener, a repainting header, a render per event. Each had a number; each is gone." One fix would have felt better and measured badly on the slower phone.
go to the lab
- Run the chain across three routes that are each one cause:
/paint/non-passive-scroll(the listener),/paint/hover-shadow(the paint),/layout/content-visibilityat 10,000 rows without the toggle (layout cost per scroll). Record each; name the cause from the trace alone; then apply the toggle and record again.
DIAGNOSIS 2: A JANKY SCROLL
red frames, a busy main thread, and three different causes that look the same
swipe the figure sideways, or tap expand for full screen
1/6
Rendering drawer
The complaint: "scrolling the list stutters on my laptop". Rendering drawer first: Scrolling performance issues shows red on the list container with "has a non-passive wheel listener"; Paint flashing shows the sticky header flashing on every scroll step; Layer borders show one layer for the whole page. Two causes already, in thirty seconds, no recording.
28
Diagnoses 3, 4, 5: memory growth, a broken cache, a hydration mismatch
3: memory growth
- Performance monitor during a ten-minute manual session: heap and DOM nodes step up on every detail-panel open and never return after a forced GC. Rate: ~500 nodes and 1.2 MB per open; 400 opens a day is 480 MB. The rate predicts the crash.
- Three snapshots over two rounds of ten opens; compare 3 to 2; Detached; sort by Retained; an HTMLDivElement's retainers: an array in a Map in a module-level
panelCachethat keeps each panel's root "for fast reopening" and never evicts. By shape a cache; by size a leak. - Fix: a bounded LRU of five with the node released on eviction. Round three grows by 0 MB. "A cache without a bound leaked 1.2 MB per open; bounded, growth is zero."
4: a broken cache
- Network, Disable cache off, reload:
index.html"(disk cache)" withCache-Control: max-age=3600from the CDN; the new hashedapp.jsis referenced only by the new HTML the browser never fetched. The directive is on the wrong resource. - Fix: HTML
no-cachewith an ETag (a 304 per load, ~50 ms); assets/assets/app-[hash].jswithmax-age=31536000, immutable. Purge the CDN once. Verify in Network: HTML 304 on reload; assets from memory or disk; a new hash fetched exactly once after a deploy. "The HTML revalidates; the assets never do."
5: a hydration mismatch
- Console: "Hydration failed because the server rendered HTML didn't match the client", with the diff: "Updated 3 minutes ago" versus "4 minutes ago"; a different random id on a field. React DevTools (Components) highlights the subtree React discarded and rebuilt: the flicker. Performance: a 180 ms hydration task and a second render of the subtree.
- Fix: relative time set after mount (or an absolute timestamp formatted identically on both sides);
useIdinstead ofMath.random; browser-only branches behind a mounted flag. Verify: no error, no flicker, no second render; hydration 40 ms. "Two values differed between server and client; they are stable; hydration is silent." The React course part 13 has the mismatch sources.
go to the lab
- Memory: run the full chain on
/js/detached-list: monitor, three snapshots, retainers, release, a fourth snapshot showing zero growth. - Cache: on
/network/cold-vs-warmwith the server, treat thenostoreandmaxage60assets as "the HTML" and "the asset" and write down which directive each should have had; confirm with a second reload. - Hydration: the lab is client-rendered, so reproduce this one in any SSR app you own: render
new Date().toLocaleTimeString()in a component and load the page; read the console diff and find the subtree in React DevTools.
DIAGNOSES 3, 4, 5: MEMORY GROWTH, A BROKEN CACHE, A HYDRATION MISMATCH
three shorter chains, each ending in one number
swipe the figure sideways, or tap expand for full screen
1/6
memory: monitor
Memory, the complaint: "the ops dashboard crashes after a day". Performance monitor open during a ten-minute manual session: JS heap and DOM nodes step up every time a detail panel opens and never return after a forced GC. Rate: ~500 nodes and ~1.2 MB per open. Over a day of 400 opens: 200k nodes, 480 MB. The tab dies.
29
The course, in one page
| symptom | first look | the recording | the number | the course part with the fix |
|---|---|---|---|---|
| Slow load | Lighthouse + field data | Network (Slow 4G), Performance reload with the LCP breakdown, Coverage | LCP phases in ms; unused bytes | Browser 1, 2, 12; FSD M3; Architecture (splitting) |
| Slow interaction | Interactions track | Performance at 4× CPU; Bottom-Up by self time | Input delay, processing, presentation in ms | FSD M2 (workers, chunking); React 5 (transitions); Browser 4 (forced layout) |
| Janky scroll or animation | Rendering drawer (scroll issues, paint flashing, layers) | Performance: Frames, Interactions, per-frame events | Per-frame ms by cause | Browser 5 (compositing); React 10 (renders per event) |
| Memory growth | Performance monitor | Three snapshots; retainers | Bytes and nodes per action; growth in round two | JS 5; React 11 (cleanup); FSD M1 (bounded caches) |
| Wrong or stale data | Network Size column; Application storage tree | Headers; the storage tier's contents | Which cache answered; which tier holds what | Browser 1 (HTTP cache), 7 (SW), 8 (storage) |
| Looks wrong | Elements: Computed, box model, overlays | (none needed) Styles for the losing rule | The winning declaration and its source | Browser 3, 4 |
| Behaves wrong | Console errors and Issues; Event Listeners pane | Sources: a breakpoint of the least-disruptive kind | The stack at the moment | JS 8, 10; React 13 (hydration) |
| Fails cross-origin or offline | Console reason; Application → Service Workers, Frames | Network headers; the SW's Cache Storage | The missing header; the missing cache entry | Browser 7, 9 |
what to keep
- Look before you guess: the panel is the mechanism made visible; the recording comes before the theory.
- Name the stage; the stage names the course part; the course part names the fix family.
- Change one thing, measure again, keep both traces. The number is the proof and the PR description.
- The field decides; the lab explains. Lighthouse and your laptop are hypotheses about what users have.
the exercise, for the course
Take the next bug report that says "slow", "jumps", "grows", "stale" or "wrong". Before reading any code, open the panel this table names, record, and write the number. Then write the sentence. If you cannot write the sentence, you have not finished looking.