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
  1. 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.
  2. 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.
  3. 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.
  4. Coverage: vendor.js 91% unused on this route; app.css 88% unused. Not the LCP cause; the TBT cause.
  5. 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.
  6. 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
  1. Run this chain on /perf/lcp-phases for 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
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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).
  6. 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
  1. Run the chain across three routes that are each one cause: /paint/non-passive-scroll (the listener), /paint/hover-shadow (the paint), /layout/content-visibility at 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
  1. 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.
  2. 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 panelCache that keeps each panel's root "for fast reopening" and never evicts. By shape a cache; by size a leak.
  3. 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
  1. Network, Disable cache off, reload: index.html "(disk cache)" with Cache-Control: max-age=3600 from the CDN; the new hashed app.js is referenced only by the new HTML the browser never fetched. The directive is on the wrong resource.
  2. Fix: HTML no-cache with an ETag (a 304 per load, ~50 ms); assets /assets/app-[hash].js with max-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
  1. 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.
  2. Fix: relative time set after mount (or an absolute timestamp formatted identically on both sides); useId instead of Math.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
  1. Memory: run the full chain on /js/detached-list: monitor, three snapshots, retainers, release, a fourth snapshot showing zero growth.
  2. Cache: on /network/cold-vs-warm with the server, treat the nostore and maxage60 assets as "the HTML" and "the asset" and write down which directive each should have had; confirm with a second reload.
  3. 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

symptomfirst lookthe recordingthe numberthe course part with the fix
Slow loadLighthouse + field dataNetwork (Slow 4G), Performance reload with the LCP breakdown, CoverageLCP phases in ms; unused bytesBrowser 1, 2, 12; FSD M3; Architecture (splitting)
Slow interactionInteractions trackPerformance at 4× CPU; Bottom-Up by self timeInput delay, processing, presentation in msFSD M2 (workers, chunking); React 5 (transitions); Browser 4 (forced layout)
Janky scroll or animationRendering drawer (scroll issues, paint flashing, layers)Performance: Frames, Interactions, per-frame eventsPer-frame ms by causeBrowser 5 (compositing); React 10 (renders per event)
Memory growthPerformance monitorThree snapshots; retainersBytes and nodes per action; growth in round twoJS 5; React 11 (cleanup); FSD M1 (bounded caches)
Wrong or stale dataNetwork Size column; Application storage treeHeaders; the storage tier's contentsWhich cache answered; which tier holds whatBrowser 1 (HTTP cache), 7 (SW), 8 (storage)
Looks wrongElements: Computed, box model, overlays(none needed) Styles for the losing ruleThe winning declaration and its sourceBrowser 3, 4
Behaves wrongConsole errors and Issues; Event Listeners paneSources: a breakpoint of the least-disruptive kindThe stack at the momentJS 8, 10; React 13 (hydration)
Fails cross-origin or offlineConsole reason; Application → Service Workers, FramesNetwork headers; the SW's Cache StorageThe missing header; the missing cache entryBrowser 7, 9
what to keep
  1. Look before you guess: the panel is the mechanism made visible; the recording comes before the theory.
  2. Name the stage; the stage names the course part; the course part names the fix family.
  3. Change one thing, measure again, keep both traces. The number is the proof and the PR description.
  4. 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.