Overview

Core Web Vitals cover three metrics; page speed has at least six worth tracking. TTFB, FCP, image weight, font loading, and JS payload all decide whether a page hits its LCP and INP targets. The fixes are cheap on a new build and expensive on a legacy one; ship them at architecture time, not after a Lighthouse audit. For the end-to-end CWV optimization walkthrough, see optimize-core-web-vitals.

Cut TTFB to under 600 milliseconds

Time to First Byte is server response time plus network latency. It is the floor under every other metric; a 1.5-second TTFB caps LCP at 2.5+ seconds before the browser does any work.

  • Static-host whenever possible. GitHub Pages, Vercel, Netlify, and Cloudflare Pages serve from a CDN edge by default.
  • For dynamic responses, cache HTML at the edge with s-maxage and stale-while-revalidate. A 60-second SWR window absorbs traffic spikes. See cloudflare-cache-rules for the rule syntax.
  • Use a CDN with at least 100 PoPs. cloudflare gets TTFB under 100 ms for most international traffic.
  • Database round-trips inside the request handler are the most common TTFB killer. Cache reads, batch queries, or move the page to static.

Hit First Contentful Paint under 1.8 seconds

FCP is the first pixel of content rendered. It is a proxy for “is anything happening?” and decides whether users abandon the page.

  • Inline critical CSS (the styles needed for above-the-fold content) directly in <head>. 14 KB or less; anything larger blocks the first packet.
  • Defer non-critical CSS with <link rel="preload" as="style" onload="this.rel='stylesheet'">.
  • Render the first paint server-side. A blank <div id="root"> waiting for client React makes FCP equal to JS parse time.
  • Preconnect to the CDN and font origin: <link rel="preconnect" href="https://fonts.example.com" crossorigin>.

Serve images in modern formats with lazy loading

Images dominate page weight on most sites. Modern formats cut bytes by 30 to 70 percent at the same visual quality.

  • AVIF first, WebP second, JPEG as fallback. Use <picture> with <source type="image/avif"> and <source type="image/webp">.
  • Lazy-load every image below the fold with loading="lazy". Native browser support is universal as of 2023.
  • Eager-load the LCP image with fetchpriority="high" and a <link rel="preload"> in <head>.
  • Specify width and height on every <img> to reserve layout space; CLS depends on it. See image-seo for the full image SEO checklist.

Load fonts with font-display: swap and self-host

Web fonts block text rendering by default. The browser holds the first paint until the font arrives or a timeout fires.

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-var.woff2") format("woff2-variations");
  font-display: swap;
  font-weight: 100 900;
}
  • font-display: swap paints fallback text immediately, then swaps in the web font.
  • Self-host fonts to avoid third-party DNS and TLS round-trips. Google Fonts via CSS adds 100 to 300 ms.
  • Subset the font to Latin glyphs (or whatever the site needs). A full Inter variable font is 300 KB; a Latin subset is 60 KB.
  • Use size-adjust, ascent-override, and descent-override in the fallback @font-face to prevent CLS on swap.

Inline critical CSS and defer the rest

CSS is render-blocking by default. The browser parses the entire stylesheet before painting.

  • Extract the above-the-fold CSS with Critical, Critters, or Next.js’s built-in critical CSS plugin.
  • Inline it in <style> inside <head>. Keep it under 14 KB to fit in the first TCP round-trip.
  • Defer the rest with <link rel="preload" as="style" onload="this.rel='stylesheet'"> or media="print" swap.
  • Audit unused CSS with Coverage in Chrome DevTools. Sites often ship 80% unused rules.

Defer JavaScript with async, defer, or interaction-gating

JS parse and execute time blocks the main thread. Every kilobyte of JS costs roughly 1 ms on a mid-tier phone.

  • First-party JS: ship as a module with type="module" (gets defer semantics for free).
  • Analytics, chat widgets, A/B testing, cookie banners: load on requestIdleCallback or after the first user interaction.
  • Audit third-party JS quarterly. A typical marketing site carries 600 KB of vendor JS, half of which is never used.
  • Tree-shake aggressively. A 200 KB Lodash import becomes 4 KB with named imports.

Measure both field data and synthetic budgets

Synthetic tools find regressions before they ship; field data confirms real-world impact.

  • Lighthouse CI in the build pipeline. Fail the build on regressions over 5%.
  • WebPageTest with throttled 4G and a mid-tier phone profile. Lab runs on a Mac M-series do not reflect users.
  • Field data through the web-vitals library and CrUX. See core-web-vitals for the field-data setup.
  • Set per-page budgets in lighthouserc.json. A homepage budget of 200 KB JS, 80 KB CSS, and 300 KB images is achievable.