Improving performance 102


Part 1 was the theory: the rendering engine flow — parsing → render tree → layout → paint. Picking this back up (a few years later 😅) — this is part 2, where we actually measure what matters and turn those numbers into faster load times.

you can’t improve what you don’t measure

There are two kinds of performance data, and you want both:

  • Lab data — synthetic, collected in a controlled environment (Lighthouse, WebPageTest). Reproducible and great for debugging, but it doesn’t reflect the janky phone on 3G your real user is holding.
  • Field data (RUM) — real user monitoring, collected from actual visitors (the Chrome UX Report, the web-vitals library). Messy, but it’s the truth.

Lab data tells you why it’s slow. Field data tells you whether it matters.

the metrics that matter (core web vitals)

Forget vanity metrics like “page fully loaded” — users care about perceived speed. Google’s Core Web Vitals capture that, and they map almost one-to-one onto the rendering steps from part 1:

  • LCP (Largest Contentful Paint) — when the biggest thing in the viewport (hero image, headline) finishes painting. This is the “ok, the page is here” moment. That’s the paint step. Aim for < 2.5s.
  • CLS (Cumulative Layout Shift) — how much stuff jumps around as it loads. That’s layout / reflow happening visibly and annoyingly. Aim for < 0.1.
  • INP (Interaction to Next Paint) — replaced FID as a Core Web Vital in 2024. How snappy the page feels when you click or tap. Aim for < 200ms.

Supporting cast worth watching:

  • TTFB (Time to First Byte) — how long the server takes to even start responding. Everything else waits on this.
  • FCP (First Contentful Paint) — the first pixels of content.
  • TTI (Time to Interactive) — when the main thread is finally quiet enough to respond to input.

how to measure

In the browser, nothing to install:

chrome devtools → Lighthouse tab → Analyze page load

You get a score plus a prioritized list of opportunities. Run it in an incognito window so extensions don’t skew the result.

The Performance panel is the one that ties back to part 1 — record a page load and you literally see the parsing, layout, and paint events on a timeline, plus exactly where the main thread is blocked.

For field data, drop in the web-vitals library:

import { onLCP, onINP, onCLS } from "web-vitals";

onLCP(console.log);
onINP(console.log);
onCLS(console.log);

Ship those values to your analytics and watch the 75th percentile, not the average — averages hide your slowest users, which are the ones bouncing.

Or go lower level with the raw PerformanceObserver API:

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry.name, entry.startTime);
  }
}).observe({ type: "largest-contentful-paint", buffered: true });

the low-hanging fruit

Like I said in part 1 — start with the quick wins that move the needle most. Almost every one of them comes down to give the rendering engine less work, or let it start sooner.

Send less, send it faster (TTFB + parsing)

  • compress with gzip/brotli, minify your js + css
  • cache aggressively — a CDN + sane cache headers is the single biggest TTFB win
  • code-split so you don’t ship the whole app just to render one page

Don’t block the parser

The HTML parser stops dead on a synchronous <script>. Let it keep going:

<script src="/app.js" defer></script>

Inline your critical CSS and lazy-load the rest — render-blocking CSS holds up the entire render tree.

Stop layout thrashing (layout / reflow)

Always reserve space for images so the page doesn’t jump (this is most of your CLS):

<img src="hero.jpg" width="1200" height="630" alt="..." />

And batch your DOM reads and writes. Reading offsetHeight right after a write forces a synchronous reflow — do all your reads, then all your writes.

Optimize images (usually the biggest LCP win)

Images are almost always the largest contentful paint element, so this is where the LCP needle actually moves:

  • right-size them and serve modern formats (webp/avif)
  • lazy-load anything below the fold:
<img src="..." loading="lazy" alt="..." />

the loop

Performance isn’t a one-time fix, it’s a budget you defend:

  1. measure with lab data (Lighthouse) to find the biggest opportunity
  2. fix one thing
  3. confirm with field data that real users actually felt it
  4. repeat

Then set a perf budget (say, LCP < 2.5s and < 200kb of JS) and wire it into CI so a regression fails the build before it ships, not after a user complains.

further reading:

That’s the journey — from “what happens when a page loads” all the way to “how do I make it fast”.

← Back to blog