Web Performance
← Back to Notes

Web Performance

A complete executive deep-dive on Core Web Vitals, measurement, and optimization — written for a VP who needs the full picture, not buzzwords.

Why this matters at the executive level: performance is not a developer hobby. It is a measurable lever on revenue, SEO ranking, ad CPMs, conversion, bounce, and brand perception. Every 100 ms slower has been correlated with measurable drops in conversion at Amazon, Walmart, Pinterest, BBC, and others. This page explains every metric, every tool, and every optimization technique — in business language and engineering depth.

01. Introduction & Course Overview

The course this page summarizes (Todd Gardner, Frontend Masters) is structured around a single loop you will repeat forever inside your engineering organization:

  1. Measure — capture real user data, not lab opinions.
  2. Diagnose — find the metric that is hurting the worst user experience.
  3. Fix — apply the smallest change that moves that metric.
  4. Re-measure — confirm impact in production, not on a developer laptop.

Performance work fails in two predictable ways inside companies:

The Example Project — "Developer Stickers Online"

The course uses a fictional e-commerce site that demonstrates the three universally painful patterns:

These are the three categories Google rewards or penalizes you for. Memorize this triad: render speed, visual stability, responsiveness.

02. Importance of Web Performance

User Expectations of Performance

There are three independent reasons performance matters: user experience, SEO, and online advertising. They reinforce each other — a faster site ranks higher, gets more traffic, converts that traffic better, and pays less per ad click.

Research on user perception (Nielsen, Akamai, Google) consistently lands on roughly these numbers:

< 1 second
Instant
Feels seamless. User flow is uninterrupted.
1–3 seconds
Noticeable
User waits but stays engaged.
3–10 seconds
Annoyed
Bounce rate climbs steeply. Trust drops.
> 10 seconds
Abandoned
User assumes failure and leaves.

Often-cited statistics:

Performance Benefits, SEO & Advertising

SEO ranking

Since 2021, Google's Page Experience signal uses Core Web Vitals (LCP, CLS, INP) as a ranking factor. Specifically:

Online advertising

VP take-away
Performance shows up on three lines of the P&L: acquisition cost (ads), organic traffic (SEO), and conversion (UX). A 200 ms improvement is usually cheaper to ship than a 1% paid-media increase — and compounds forever.

Waterfall Charts

A waterfall chart is the foundational visualization for understanding how a page loads. Each row is one network request; the row's position shows when it started and how long it took. Stacked vertically, the rows reveal sequential dependencies (what was blocking what) and parallelism (what loaded simultaneously).

index.html
280ms
main.css
200ms
app.js
440ms
fonts.woff2
280ms
hero.jpg
560ms
analytics.js
160ms

A waterfall makes three things obvious:

Measuring DOMContentLoaded & Load Events

Two original browser events were the first generation of "page is ready" signals:

DOMContentLoaded
Fires when the initial HTML document has been completely parsed and synchronous <script> tags have executed. Stylesheets, images, and subframes are NOT required.
load
Fires when the entire page, including all dependent resources (images, stylesheets, scripts, iframes), has finished downloading and rendering.
// Legacy way to measure
document.addEventListener('DOMContentLoaded', () => {
  console.log('DOM ready at', performance.now());
});
window.addEventListener('load', () => {
  console.log('Page fully loaded at', performance.now());
});

The Problem with Legacy Metrics

These metrics were honest in 2005 when pages were static. In 2010+ the web moved to client-side rendering (React, Vue, Angular). Now:

Why this matters strategically
Any dashboard, vendor, or report still anchored on load/DOMContentLoaded is measuring the wrong thing. Modern decisions must use Core Web Vitals, which measure what the user actually experiences, not what the browser internally finished.

03. Core Web Vitals & Other Performance Metrics

Core Web Vitals (CWV) are Google's three user-experience metrics that gate Search ranking. Each metric is bucketed into Good / Needs Improvement / Poor based on the p75 of real users.

MetricWhat it capturesGoodNeeds workPoor
LCPRender speed of biggest visual≤ 2.5s≤ 4.0s> 4.0s
CLSVisual stability≤ 0.1≤ 0.25> 0.25
INPInteraction responsiveness≤ 200ms≤ 500ms> 500ms

Largest Contentful Paint (LCP)

What it measures: the time from navigation start to when the largest content element visible in the viewport is rendered. It is a proxy for "when did the user perceive the main content?"

What counts as the "largest element"?

Rules & gotchas

The four phases of LCP

To diagnose LCP you must decompose it. Google divides LCP into four sub-phases — fixing the biggest phase first gives the biggest win:

  1. TTFB (Time to First Byte) — server response time.
  2. Resource Load Delay — gap between TTFB and when the LCP resource starts downloading.
  3. Resource Load Duration — how long the LCP image/file took to download.
  4. Element Render Delay — gap between the resource finishing and the browser actually painting it.
// Measuring LCP with the PerformanceObserver API
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('LCP candidate:', entry.startTime, entry.element);
  }
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
Targets
Good: ≤ 2.5s at p75. Above 4.0s is "Poor" and meaningfully hurts ranking. For e-commerce hero pages, world-class teams target sub-1.8s on 4G.

Cumulative Layout Shift (CLS)

What it measures: how much visible content unexpectedly moves around during the lifetime of the page. CLS rewards visual stability. A shift counts only if the element was previously visible.

How CLS is calculated

For every layout-shift event, Google computes:

shift_score = impact_fraction × distance_fraction

CLS sums shift scores within a 5-second session window, then takes the worst window on the page. This stops long-lived pages from accumulating noise.

Common causes

Target
Good: ≤ 0.1 at p75. Anything above 0.25 is Poor. Many publishers report a CLS of 0 is fully achievable with discipline around image dimensions and reserved ad slots.

CLS Q&A

Flame Charts

A flame chart visualizes JavaScript execution over time. The x-axis is time; the y-axis is the call stack. Each block is a function; nested blocks are functions called by their parent. Width = time the function took.

In Chrome DevTools the colors map to event types:

Why VPs should care: long flat yellow bars are why your "Add to Cart" button feels frozen. They are usually traceable to a single third-party tag, an oversized React render, or a heavy synchronous computation.

Interaction to Next Paint (INP)

What it measures: the latency between a user interaction (click, tap, keypress) and the very next visual update. INP reports the worst-case interaction the user experienced on the page (with some allowance for outliers above 50 interactions).

The anatomy of an interaction

  1. Input delay — the gap between the user's tap and when your handler can run. Caused by the main thread being busy with other JS (long tasks).
  2. Processing time — how long your event handler(s) take to run.
  3. Presentation delay — time the browser takes to recalculate styles, layout, paint, and composite the next frame.

INP = the maximum of these three for the slowest interaction on the page.

What counts as an interaction

Pure scroll and hover do NOT count.

Good
≤ 200ms
User perceives instant response.
Needs work
200–500ms
Noticeable lag, frustration begins.
Poor
> 500ms
UI feels broken. Users double-tap, give up.

Device capability matters enormously. A budget Android phone has 1/10th the single-thread performance of an iPhone Pro. Your INP at p75 on real users is dominated by mid-tier mobile, not your laptop.

First Input Delay (FID) — Legacy

FID was the original interactivity metric and measured only the input delay of the first interaction on a page. It was retired in March 2024 in favor of INP because:

Time to First Byte (TTFB)

TTFB is the duration from the user clicking a link to the browser receiving the first byte of the HTML response. It is not a Core Web Vital but it is the floor for every other metric — you cannot start rendering before bytes arrive.

What's inside TTFB

  1. Redirects — each 301/302 adds a full round trip.
  2. DNS lookup — resolving the hostname.
  3. TCP connection — 3-way handshake.
  4. TLS negotiation — cipher suite, certificate exchange.
  5. Request — browser sends headers and any body.
  6. Server processing — your application code, database queries, cache lookups.
  7. First byte received — the response begins streaming back.
Targets
Good TTFB is ≤ 800 ms at p75. World-class is <200 ms via edge rendering / CDN HTML caching. Anything above 1.8s is Poor.

First Contentful Paint (FCP)

FCP is the moment any text, image, SVG, or non-white canvas appears in the browser window. It is the first visual signal to the user that something is happening.

FCP is bounded by:

Targets
Good FCP is ≤ 1.8s at p75. Poor is > 3.0s.

04. Capturing Performance Metrics

Performance API

The browser exposes a built-in performance object that lets you capture timing data programmatically. Three key surfaces:

performance.now()
High-resolution monotonic timestamp (sub-millisecond) since page load. Use for ad-hoc instrumentation.
performance.getEntries()
Returns array of every resource the page loaded with full network timings (DNS, TCP, TLS, response).
performance.mark / .measure
Drop named markers in code and compute durations between them — the basis of custom business metrics ("time to product card visible").
// Custom business metric: time to "Buy Now" button visible
performance.mark('route-changed');
// ... React renders, data fetches, etc.
performance.mark('buy-button-visible');
performance.measure('route-to-buy', 'route-changed', 'buy-button-visible');

const [entry] = performance.getEntriesByName('route-to-buy');
console.log('Took', entry.duration, 'ms');

PerformanceObserver

Calling performance.getEntries() from your own code has an "observer effect" — it runs on the main thread and can itself cause jank. PerformanceObserver solves this by letting you subscribe to entry types and receive them asynchronously when the browser is idle.

// Observe layout shifts
const obs = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      sendBeacon('cls', entry.value);
    }
  }
});

obs.observe({
  type: 'layout-shift',
  buffered: true  // also deliver entries that already happened
});

Important: pass buffered: true so that you don't miss entries that fired before your observer attached — critical for LCP and CLS, which often start accumulating before your monitoring code loads.

Browser Support for Performance Metrics

Browser support is uneven and matters for what you can measure for whom:

MetricChrome / EdgeFirefoxSafari
DOMContentLoaded / load
TTFB
FCP✓ (recent)
LCPpartial×
CLSpartial×
INP××

Because Core Web Vitals are Chromium-only for measurement purposes, Google's ranking signal is based on Chrome data — which is fine since Chromium represents ~70% of global browser share. But your Safari users won't show up in CrUX data; you need a RUM tool with its own collection layer to see them.

05. Testing & Tools

Testing Performance — Three Data Types

TypeWhat it isStrengthWeakness
LabSingle, controlled test (Lighthouse on your laptop)Reproducible, debuggableOne device, one network, one user — not reality
SyntheticLab tests run on a schedule from cloud locationsTrend over time, alerts on regressionsStill not real users
Field (RUM)Real users, real networks, real devicesGround truth — this is what Google ranks onNoisy, requires statistical analysis
Org-design implication
Engineering should own a synthetic system that catches regressions in CI. Product/analytics should own RUM that reflects user reality. If only one of these exists, you have a blind spot.

Statistics — Why Averages Lie

The mean (average) of a long-tail distribution is meaningless for performance:

Strategic rule
Set goals on p75. Investigate p95 for systemic issues. Ignore the mean unless executives demand a single number — in which case report p75 as "what 3 of 4 users experience" instead.

Google Lighthouse

Lighthouse is the built-in Chrome DevTools auditor (also available as CLI, Node module, and PageSpeed Insights). It runs your site under simulated mobile + slow 4G and produces:

You can configure the run:

Lighthouse caveats
Lighthouse is lab data — one run on one device. INP cannot be measured by Lighthouse (it requires real interactions). A perfect Lighthouse score does NOT guarantee good CrUX scores. Use Lighthouse for diagnosis and regressions; use CrUX/RUM for ranking and reality.

Analyzing Results in the Performance Tab

Chrome's Performance panel records a full trace of everything the browser did during a recording session. After Lighthouse generates a report, click into the trace to see:

The Memory tab exists in the same panel but is not commonly used for production diagnosis — memory leaks rarely show up cleanly in short recordings. Use specialized RUM-level memory tracking instead.

Web Vitals Chrome Extension

A free Google extension that displays live LCP, CLS, and INP for the page you are currently on. Key features:

This is the first tool every developer in your org should install.

Chrome User Experience Report (CrUX)

CrUX is Google's public dataset of real Chrome user metrics. It is the dataset Google uses to determine Page Experience for ranking. You can access it via:

Limits to know:

WebPageTest.org

The most flexible synthetic testing tool, originally built by Pat Meenan (now Catchpoint). Compared to PageSpeed Insights it offers:

Real User Monitoring (RUM)

RUM is a small JavaScript snippet you put on every page that captures performance data from your actual users (not just Chrome, not just opt-ins). The snippet uses PerformanceObserver internally and beacons data to a backend you can query and chart.

RUM categories and example vendors:

Request Metrics — Example RUM Tool

The course author (Todd Gardner) built Request Metrics, and uses it to illustrate what a good RUM dashboard provides:

Buying decision framework
Pick a RUM tool if you can answer "yes" to: do we ship at least monthly, do we have > 100k page views per month, and do we care about non-Chrome users? Otherwise, free CrUX + Lighthouse-in-CI is usually enough.

06. Setting Performance Goals

How Fast Should Your Site Be?

"Fast" is subjective. The same 3-second wait feels different depending on context:

Determining Performance Goals

Three inputs drive your goal:

1. UX research
User intent
What is the user's goal and patience profile?
2. Competitors
~20% faster
Below this gap, users can't perceive the difference. Above, they prefer you.
3. SEO
Google p75
CWV thresholds: LCP 2.5s, CLS 0.1, INP 200ms.

Concrete process:

  1. Test your top 5 competitors in PageSpeed Insights. Note their CrUX p75 values.
  2. For each metric, your goal is the better of: (a) Google's "Good" threshold, (b) 20% faster than the best competitor.
  3. Convert goals into a quarterly OKR: "Reduce p75 LCP on /product pages from 3.4s to 2.2s by Q3."

Understanding Your Users

Before optimizing, know who you're optimizing for. From global stats:

Then layer in your users via analytics:

07. Improving Time to First Byte

The Performance Improvement Loop

The optimization workflow is deterministic:

  1. Measure in production with RUM.
  2. Pick the worst metric at p75. Don't try to fix everything.
  3. Pick the easiest fix for that metric. Cheap wins build momentum.
  4. Find ways to do fewer things between the two events the metric measures.
  5. Ship, measure again, repeat.

Baseline TTFB

Measure TTFB in Chrome DevTools → Network → click the HTML document request → Timing tab. You'll see DNS, Initial connection, SSL, Waiting (TTFB), Content download.

The "Waiting" segment is your server's response time. If it's 500 ms+, the win is server-side. If "Initial connection" + "SSL" are huge, the win is network/CDN.

Google targets:

Good
≤ 800ms
Needs work
≤ 1.8s
Poor
> 1.8s

Enabling Gzip & Brotli Compression

Text-based responses (HTML, CSS, JS, JSON, SVG) compress 70–90%. Two standard algorithms:

AlgorithmCompression ratioCPU costNotes
gzip~70%LowUniversal support since 1990s. Default fallback.
Brotli~80%Higher (but pre-compressible)Better ratio. Supported by all modern browsers. Use level 11 for static assets at build time, level 4 for dynamic.
# Nginx example
gzip            on;
gzip_types      text/css application/javascript application/json image/svg+xml;
brotli          on;
brotli_types    text/css application/javascript application/json image/svg+xml;
brotli_static   on;   # serve pre-compressed .br files when present
Quick win
Enabling Brotli often cuts HTML/CSS/JS payloads by an additional 15–20% versus gzip. This typically reduces TTFB by tens of milliseconds and LCP by hundreds. It is a one-line config change in most reverse proxies.

Efficient Protocols: HTTP/1.1 vs HTTP/2 vs HTTP/3

HTTP/1.1HTTP/2HTTP/3
Year199720152022
TransportTCPTCPQUIC (UDP)
MultiplexingOne req per connectionMany streams per connectionMany streams, no head-of-line blocking
Header compressionNoneHPACKQPACK
Setup RTTTCP+TLS = ~2-3 RTTTCP+TLS = ~2-3 RTTQUIC = 1 RTT (0 RTT on resume)
Recovery from packet lossBlocks whole connectionBlocks all streams (TCP HOL)Only the affected stream

HTTP/2 made the big win: instead of opening 6 separate TCP connections to your domain (browser limit), the browser opens one and multiplexes all requests over it. This eliminates connection-setup overhead and lets the server prioritize critical resources.

HTTP/3 goes further by replacing TCP with QUIC (over UDP). The TCP "head-of-line blocking" problem — one dropped packet pauses all streams on the connection — is solved because QUIC streams are independent. HTTP/3 also reuses prior TLS session keys to achieve 0-RTT reconnection.

Using HTTP/2 & HTTP/3

You don't need to change application code. Enable at the server / CDN:

The browser will first connect over HTTPS+HTTP/2, then discover HTTP/3 via the Alt-Svc response header, and use UDP for subsequent connections.

Host Capacity & Proximity

Two structural levers on TTFB beyond software:

1. Right-size the host

If your server is CPU- or memory-pinned, requests queue and TTFB climbs. Provision enough capacity to keep CPU below 60% at peak, then scale horizontally with a load balancer.

2. Reduce distance with a CDN

The speed of light is non-negotiable. A request from Sydney to Virginia is ~200 ms round-trip just from physics. A CDN places edge servers in dozens to hundreds of cities, terminating TLS and (often) serving cached HTML close to the user.

08. Improving First Contentful Paint

Baseline FCP & the Three Tactics

FCP improvement always uses some combination of three tactics:

  1. Remove sequence chains — flatten dependency trees so resources start earlier.
  2. Preload critical resources — tell the browser about important assets before the parser would discover them.
  3. Lazy load non-critical resources — push everything that isn't needed for first paint to later.

Removing Sequence Chains (Collapsing Dependencies)

A "sequence chain" is a request that can only start after another request finishes. Classic example: HTML → CSS → font file. The font cannot be requested until CSS is parsed, which cannot start until CSS is downloaded, which cannot start until HTML is parsed enough to find it.

<!-- BAD: sequence chain -->
<link rel="stylesheet" href="theme.css">
<!-- inside theme.css: @import url('fonts.css'); -->
<!-- inside fonts.css: @font-face { src: url('Inter.woff2'); } -->

<!-- GOOD: bundled and preloaded -->
<link rel="preload" href="/fonts/Inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/bundle.css"> <!-- font-face inlined -->

A bundler (Webpack, Vite, esbuild) collapses these by inlining or co-locating dependencies so the browser sees one request instead of three sequential ones.

Preloading Resources

Three relationships to a future request you can hint to the browser:

HintWhat it doesWhen to use
dns-prefetchResolves the DNS for a hostnameThird-party domain you'll need soon
preconnectDNS + TCP + TLS setupCritical third party (fonts, analytics, CDN)
preloadActually fetches the resource immediately at high priorityKnown critical resource the parser would discover late (LCP image, hero font)
prefetchFetches at low priority for likely next navigation"User will probably click this link" predictions
<!-- Google Fonts pattern -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter&display=swap">

Lazy Loading Scripts

By default, a <script src="..."> in the HTML blocks parsing until it downloads and runs. There are two attributes to fix this:

AttributeDownloadExecutionOrder preserved?
(none)Blocks parserImmediately, blockingYes
asyncParallel, non-blockingAs soon as downloaded, blockingNo
deferParallel, non-blockingAfter HTML parsed, before DOMContentLoadedYes
type="module"ParallelDeferred by defaultYes
<!-- Analytics, A/B testing, anything not needed for first paint -->
<script src="/analytics.js" defer></script>
<script src="/abtest.js" async></script>
Rule of thumb
Default new <script> tags to defer. Use async only when execution order doesn't matter and the script is fully self-contained (typical of analytics).

09. Improving Largest Contentful Paint

Baseline LCP & Its Components

Recall the four sub-phases of LCP:

  1. TTFB — covered above.
  2. Resource load delay — reduce by preloading the LCP element.
  3. Resource load duration — reduce by smaller, better-formatted images.
  4. Element render delay — reduce by avoiding render-blocking CSS/JS that holds back paint.

Lazy Loading Non-LCP Images

Every image below the fold competes for bandwidth with your LCP element. Mark them as lazy so the browser defers their download until they're near the viewport:

<img src="/sticker-1.png" alt="Sticker" loading="lazy" width="240" height="240">

Critical caveat: never put loading="lazy" on the LCP image itself — it will be deprioritized and your LCP will get worse, not better.

Eager Loading the LCP Image

Two ways to tell the browser an image is the priority:

<!-- 1. fetchpriority attribute -->
<img src="/hero.jpg" alt="Hero" fetchpriority="high" width="1200" height="600">

<!-- 2. preload in the head -->
<link rel="preload" as="image" href="/hero.jpg" fetchpriority="high">

The difference: fetchpriority works once the parser discovers the image; preload works before the parser gets there. For above-the-fold heroes, use both for belt-and-braces.

Image Formats

HTTP compression does NOT help images — they are already compressed. The win comes from choosing the right format:

FormatUseAvg. file sizeTransparencyAnimation
JPEGPhotos100% (baseline)NoNo
PNGDiagrams, logos~150% of JPEG for photosYesNo
WebPGeneral-purpose modern~70% of JPEGYesYes
AVIFModern, best compression~50% of JPEGYesYes
SVGVector graphics (icons, logos)Tiny if simpleYesYes

Responsive Images

Don't ship a 4000-pixel desktop image to a 400-pixel phone. Use srcset + sizes or the <picture> element to serve the right size:

<!-- srcset -->
<img
  src="/hero-1200.jpg"
  srcset="/hero-400.jpg 400w, /hero-800.jpg 800w, /hero-1200.jpg 1200w"
  sizes="(max-width: 600px) 100vw, 50vw"
  alt="Hero"
  width="1200" height="600">

Optimizing Images with <picture>

The <picture> element gives full control over format + size negotiation with fallback:

<picture>
  <source type="image/avif" srcset="/hero-400.avif 400w, /hero-800.avif 800w" sizes="100vw">
  <source type="image/webp" srcset="/hero-400.webp 400w, /hero-800.webp 800w" sizes="100vw">
  <img src="/hero-800.jpg" alt="Hero" width="1200" height="600">
</picture>

The browser picks the first <source> with a format it supports, falling back to the <img> for legacy browsers. The width/height on the <img> reserve layout space (good for CLS, see below).

Workflow:

  1. Source files in highest quality (JPEG/PNG).
  2. Build script generates multiple resolutions of each, in WebP and AVIF.
  3. Use a CDN that does this on-the-fly (Cloudinary, Imgix, Cloudflare Images, Next/Image).

Caching

Caching turns repeat visits free for both the server (no compute) and the user (no download). Two layers:

Server caching

Cache rendered HTML, API responses, database queries at the server / CDN. Redis, Memcached, or built into the CDN edge.

Browser response caching

HTTP headers tell the browser how long it can re-use a response without asking the server. Two relevant headers:

Cache-Control
Modern header. e.g. Cache-Control: public, max-age=31536000, immutable — cache for a year, never re-check.
Expires
Legacy header with absolute date. Cache-Control wins if both present.
ETag / Last-Modified
Validation headers. Allow browser to ask "did this change?" and get a tiny 304 Not Modified response if not.

Enabling Caching Headers

Static, fingerprinted assets

Cache-Control: public, max-age=31536000, immutable

This is for files like /app.abc123.js where the hash changes if content changes. Cache forever; bust by URL change.

HTML

Cache-Control: public, max-age=0, must-revalidate
ETag: "abc123"

HTML changes often; revalidate every visit, but allow 304 responses to save bandwidth.

API responses

Cache-Control: private, max-age=60, stale-while-revalidate=600

Cache for the current user for 60 seconds; serve stale for up to 10 minutes while fetching a fresh version in the background.

Easy executive win
Most underperforming sites have either no Cache-Control headers at all or have no-store on assets that never change. Enabling proper headers can cut returning-user LCP by 50%+ with zero code changes — it's purely server config.

10. Improving CLS & INP

Layout Size Hints (CLS)

Almost all CLS comes from elements that arrive after the initial render and push other content around. The fix is to reserve their space upfront so the browser doesn't have to reflow when they appear.

For images and videos

Always set width and height attributes. The browser uses the ratio to reserve a properly proportioned box even before the image downloads.

<!-- Browser reserves the correct aspect ratio -->
<img src="/product.jpg" alt="Product" width="800" height="600">

For aspect-ratio CSS

.hero {
  width: 100%;
  aspect-ratio: 16 / 9;
}

For ads and embeds

Reserve a fixed slot. If an ad doesn't fill, leave the empty box rather than collapse:

.ad-slot {
  width: 300px;
  min-height: 250px;
}

For dynamically-injected content (banners, cookie bars, alerts)

Insert at the bottom of the viewport (overlay), not at the top of the document. Use position: fixed so it doesn't displace existing flow.

For web fonts

Use font-display: swap with carefully chosen size-adjust and ascent-override on the fallback font so the swap doesn't change line metrics:

@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 107%;
  ascent-override: 90%;
}

Improving Interaction to Next Paint

INP is about how quickly the browser can paint after the user does something. To improve it, you must:

  1. Reduce input delay — keep the main thread free at the moment of interaction.
  2. Reduce handler runtime — do less work synchronously in your event handlers.
  3. Reduce presentation delay — minimize the work the browser must do after your handler returns.

Two browser APIs are essential:

setTimeout(fn, 0)
Yields to the browser; runs fn on the next macrotask. Use to defer non-critical work until after paint.
requestAnimationFrame(fn)
Runs fn just before the next paint. Use to schedule visual updates so they happen on the right side of a frame.
scheduler.yield()
Newer API (Chrome 129+) that lets a long task yield to the browser explicitly, then resume.

Yielding the Main Thread — A Worked Example

Consider an "Add to Cart" handler that does five things synchronously:

// BAD: blocks the main thread for ~600ms
addToCartBtn.addEventListener('click', async () => {
  const response = await fetch('/api/cart/add', { method: 'POST', body: ... });
  updateCartIcon();             // re-render badge
  trackAnalyticsEvent();        // expensive third-party call
  showSuccessAnimation();       // large DOM mutation
  refreshRecommendations();     // fetch + render carousel
  disableButton();              // visual feedback
});

The user clicks but sees nothing change until all five complete. Rewrite to give immediate feedback, then yield for the rest:

// GOOD: instant visual feedback, deferred non-essentials
addToCartBtn.addEventListener('click', () => {
  // 1. Synchronous visual feedback — happens on the next paint
  requestAnimationFrame(() => {
    addToCartBtn.disabled = true;
    addToCartBtn.textContent = 'Adding...';
  });

  // 2. Critical async work after paint
  setTimeout(async () => {
    await fetch('/api/cart/add', { method: 'POST', body: ... });
    updateCartIcon();
    showSuccessAnimation();
  }, 0);

  // 3. Non-essential work, far later
  setTimeout(() => {
    trackAnalyticsEvent();
    refreshRecommendations();
  }, 200);
});

The user perceives the button change instantly — INP drops from ~600 ms to ~30 ms even though total work is identical.

Other INP tactics

11. Wrapping Up — Executive Summary

If you take away nothing else, take this:

The performance playbook
  1. Measure real users, not your laptop. Field data > lab data.
  2. Track p75, not averages. Google ranks on p75; tail users are your churn.
  3. Focus on the worst metric on the worst page. Don't optimize what's already good.
  4. Ship the easiest fix first. Compounded small wins beat one giant rewrite.
  5. Validate in production. Lab improvements don't always translate to field.
  6. Treat performance as a budget, not a project. Set per-page budgets in CI; block PRs that exceed them.

VP Cheatsheet — The 12 Numbers to Memorize

MetricGoodNeeds workPoorWhat it captures
LCP≤ 2.5s≤ 4.0s> 4.0sMain content visible
CLS≤ 0.10≤ 0.25> 0.25Layout stability
INP≤ 200ms≤ 500ms> 500msInteraction responsiveness
TTFB≤ 800ms≤ 1.8s> 1.8sServer speed
FCP≤ 1.8s≤ 3.0s> 3.0sFirst visual signal

Org-level checklist


Source course: Web Performance Fundamentals by Todd Gardner (Frontend Masters). This page is an executive-level expansion with additional industry context.