---
title: "Page Speed Beyond Core Web Vitals"
slug: "page-speed"
category: "seo"
tags: ["seo", "performance", "ttfb", "fcp", "image-optimization", "font-loading"]
status: "stable"
last_updated: 2026-05-14
summary: "TTFB, FCP, image optimization, font-display swap, critical CSS, and JS deferral. The metrics and fixes Core Web Vitals does not cover."
related: ["[[seo/core-web-vitals]]", "[[seo/technical]]", "[[seo/image-seo]]", "[[seo/mobile-first]]", "[[frontend/html]]", "[[frontend/css]]", "[[seo/glossary]]", "[[seo/audit-checklist]]"]
---

## 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 [[howto/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 [[ops/cloudflare-cache-rules]] for the rule syntax.
- Use a CDN with at least 100 PoPs. [[ops/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 [[seo/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.

```css
@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 [[seo/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.

## Related

- [[seo/core-web-vitals]]
- [[seo/technical]]
- [[seo/image-seo]]
- [[seo/mobile-first]]
- [[frontend/html]]
- [[frontend/css]]
- [[seo/glossary]]
- [[seo/audit-checklist]]
