Three lessons, about 35 minutes total, each ending with something you can measure. No framework required — everything here runs against a page you already have, in a browser you already have open.
Pick numbers before you optimise, or you will never know when to stop. The Core Web Vitals thresholds are the sensible default: Largest Contentful Paint at or under 2.5 seconds, Interaction to Next Paint at or under 200 milliseconds, Cumulative Layout Shift at or under 0.1, all measured at the 75th percentile of real visits. Add a transfer budget too — a first-load JavaScript ceiling somewhere around 150-200 KB compressed is a target most teams can actually hold.
Estimated time: 10 minutes
Open DevTools, set CPU throttling to 4x and network to Slow 4G, and record a Performance profile of a cold load. Read the main-thread flame chart, not the score. Then instrument the real thing: new PerformanceObserver(list => ...).observe({type:'largest-contentful-paint', buffered:true}) gives you the LCP element, and observing 'long-animation-frame' tells you which script blocked which interaction. Note that INP replaced First Input Delay as a Core Web Vital in March 2024, so old FID dashboards are measuring the wrong thing — INP looks at the full interaction, including rendering, across the whole visit rather than only the first tap.
Estimated time: 15 minutes
The cheapest byte is the one you never send. Split anything not needed for first paint behind await import('./chart.js') and load it on interaction or on IntersectionObserver. Then stop blocking: any task over 50 ms is a long task, and a handler that reads offsetHeight inside a loop that also writes styles forces a synchronous layout on every iteration — batch all reads, then all writes. For long handlers, await scheduler.yield() (or a setTimeout(0) fallback) hands control back so the browser can paint. On long pages, content-visibility: auto with contain-intrinsic-size skips rendering work for off-screen sections entirely.
Estimated time: 10 minutes
Take two heap snapshots in the Memory panel with the same interaction in between and compare retained size — detached DOM nodes held by an event listener or a closure are the classic leak, and the fix is a disconnectedCallback or an AbortController passed as the listener's signal. Use WeakMap for element-keyed metadata so entries die with the node. For genuinely expensive work — parsing a large JSON payload, image processing, diffing — move it to a Web Worker and transfer an ArrayBuffer rather than structured-cloning a megabyte of objects. Virtualise any list over a few hundred rows instead of rendering all of it.
Wire the measurement into CI so regressions fail loudly: Lighthouse CI with asserted budgets on LCP, total byte weight and unused JavaScript, plus the web-vitals library reporting real-user numbers from production. Lab scores tell you what changed; field data tells you whether it mattered. Re-run lesson 1 monthly — a page that met budget in January is usually 40 KB heavier by March, one dependency at a time.
Free tools, guides, and resources across the SPUNK13 network.
Visit spunk.bet400+ Free Tools