Site Speed & Performance
TTFB, hosting choice, CDN, image optimization, minification, caching, render-blocking resources, and critical CSS — the levers that move LCP.
Site speed is not one number; it is a chain of seven sequential bottlenecks, and every fix above the slowest one is wasted effort. The order is DNS → TCP → TLS → TTFB → asset transfer → render → hydration, and the SEO consequence sits squarely in the middle of that chain at LCP. Fixing the wrong link is how teams spend three months on image compression while their TTFB hovers at 1.4 seconds because the origin still routes through Heroku US-East.
TL;DR
- TTFB under 600ms is the floor, under 200ms is the target. Anything above 800ms means LCP cannot pass even with perfect frontend optimization.
- Compression is two layers, not one. Minification trims source bytes; Brotli/Gzip compresses transfer bytes. You need both, and Brotli beats Gzip by 15–25% on text assets.
- Render-blocking resources are the cheapest LCP win. Removing or deferring one synchronous third-party script typically beats two days of image work.
The mental model
Site speed is like a relay race where the baton is your HTML. Every runner — DNS lookup, TCP handshake, TLS negotiation, your origin server, the CDN edge, the browser parser, your hydration code — has to receive the baton and pass it. The race is only as fast as the slowest runner, and adding a faster runner anywhere else doesn’t help.
The first three runners (DNS, TCP, TLS) finish in roughly 100–300ms on a warm connection and are mostly out of your hands once you’ve picked a host. The fourth runner — TTFB, time to first byte — is yours. It includes everything the origin or CDN does to produce your HTML: database queries, framework rendering, edge functions, cache lookups. The fifth runner is the network shipping bytes; the sixth is the browser parsing and painting; the seventh is your JavaScript hydrating.
Most teams start optimizing at runner six. The win is almost always at runner four or five.
Deep dive: the 2026 reality
Google’s measurable SEO threshold for server response is TTFB ≤ 800ms at the 75th percentile of CrUX. That number is forgiving by modern infrastructure standards — a Cloudflare-fronted static site averages 50–150ms TTFB worldwide; a WordPress on shared hosting averages 600–1500ms. You do not want to be near the threshold.
The 2026 hosting landscape, from fastest TTFB to slowest, for a typical content site:
| Stack | Median TTFB | Cost shape | Notes |
|---|---|---|---|
| Static + Cloudflare / Vercel Edge / Netlify Edge | 30–80ms | Free to low | Astro/Eleventy/Hugo dominate this tier |
| Next.js on Vercel (RSC + edge cache) | 80–200ms | Per-request, autoscaled | Hot path: ISR with stale-while-revalidate |
| Cloud Run / Fly.io regional | 100–300ms | Per-second compute | Good for dynamic + multi-region |
| Heroku / Render web dyno | 300–800ms | Fixed dyno | Cold starts hurt; single region |
| Shared cPanel hosting | 600–2000ms | Cheap, single host | Common WordPress floor |
| Origin without CDN, far from user | 1500ms+ | Variable | Always lose this race |
The single highest-leverage move for a typical site is putting a CDN in front of the origin. Cloudflare’s free tier alone pulls TTFB down 40–70% for global audiences. Vercel, Netlify, Fastly, and Bunny CDN all do the equivalent with minor pricing differences.
Brotli vs Gzip in 2026. Every major CDN and origin should serve Brotli at level 4–6 for dynamic content and level 11 for static, with Gzip as fallback for the long-tail of clients that don’t accept br. The compression difference: a 200 KB minified JavaScript bundle is roughly 70 KB Gzip and 55 KB Brotli — a 21% reduction, all on the wire. Cloudflare auto-Brotli is on by default; Nginx requires the ngx_brotli module.
Image strategy by 2026 default. AVIF for hero and gallery imagery (saves 30–50% over WebP at equal quality), WebP as fallback, JPEG/PNG only for ancient-browser support. Use <picture> with type hints rather than srcset alone, and serve from an image-transformation CDN (Cloudflare Images, Vercel Image Optimization, Imgix, Cloudinary) so you don’t ship the wrong size to the wrong device. Lazy-load everything below the fold with loading="lazy"; never lazy-load the LCP candidate.
Critical CSS is one of the few render-blocking optimizations still worth doing manually in 2026. Inline the ~14 KB needed for above-the-fold layout in a <style> block in the head, defer the rest with media="print" onload="this.media='all'". Tools like critical (Addy Osmani’s) and critters (used by Next.js, Astro) extract this automatically; for hand-tuned templates the rule is “anything that affects LCP layout.”
The AI crawlers — GPTBot, ClaudeBot, PerplexityBot, OAI-SearchBot — all impose render budgets shorter than Googlebot’s. A page that takes 6 seconds to fully paint will be sampled with partial content for AI Overviews and grounded responses. Speed is no longer just a Google Page Experience input; it is the gating factor for AI citation eligibility.
Visualizing it
flowchart LR
A[User clicks link] --> B[DNS lookup 20-100ms]
B --> C[TCP + TLS 50-200ms]
C --> D{Edge cache hit?}
D -->|Yes| E[CDN serves HTML 30-80ms TTFB]
D -->|No| F[Origin renders HTML 200-1500ms TTFB]
E --> G[Browser parses HTML]
F --> G
G --> H[Discover critical resources]
H --> I[Fetch CSS, fonts, LCP image]
I --> J[First paint]
J --> K[LCP target under 2.5s]
K --> L[Hydrate JS, INP-eligible]
Bad vs. expert
The bad approach
# WordPress on shared hosting, no CDN, no compression configured
server {
listen 443 ssl;
server_name example.com;
root /var/www/example.com;
index index.php;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
include fastcgi_params;
}
# No gzip, no brotli, no cache headers, no HTTP/2
}
<!-- Synchronous everything in the head -->
<head>
<link rel="stylesheet" href="/style.css" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto" />
<script src="https://cdn.tracker-a.com/loader.js"></script>
<script src="https://cdn.tracker-b.com/pixel.js"></script>
<script src="/main.js"></script>
</head>
The Nginx config ships uncompressed bytes over HTTP/1.1 with no caching. Each request hits PHP synchronously. The HTML serializes five render-blocking requests in the head before the browser can paint anything; LCP cannot be under 2.5s on a real mobile connection. This is the modal performance profile of a typical WordPress site in 2026, and it is also the modal “we don’t understand why our rankings dropped” ticket.
The expert approach
# Origin behind Cloudflare; HTTP/2 + Brotli + cache + immutable assets
server {
listen 443 ssl http2;
server_name example.com;
root /var/www/example.com/dist;
brotli on;
brotli_comp_level 6;
brotli_static on;
brotli_types text/plain text/css application/javascript application/json image/svg+xml;
gzip on;
gzip_comp_level 6;
gzip_vary on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
# Hashed assets: cache forever, immutable
location ~* \.(?:css|js|woff2|avif|webp)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# HTML: short TTL, revalidate at edge
location / {
add_header Cache-Control "public, max-age=0, s-maxage=600, stale-while-revalidate=86400";
try_files $uri $uri/ /index.html;
}
}
<!-- Critical path optimized: preconnect, preload, inline critical CSS, defer the rest -->
<head>
<link rel="preconnect" href="https://cdn.example.com" crossorigin />
<link rel="preload" as="image" href="/hero-1280.avif" fetchpriority="high" />
<link rel="preload" as="font" href="/fonts/inter-var.woff2" type="font/woff2" crossorigin />
<style>
/* Critical CSS, inlined: layout, type, and hero only — extracted by critters */
body{margin:0;font-family:Inter,system-ui,sans-serif}
.hero{aspect-ratio:16/9;background:#0b1020}
.nav{display:flex;height:56px;align-items:center;padding:0 24px}
</style>
<!-- Non-critical CSS, deferred via media-swap trick -->
<link rel="preload" href="/css/site.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />
<noscript><link rel="stylesheet" href="/css/site.css" /></noscript>
<!-- All third-party scripts deferred -->
<script src="/main.js" defer></script>
</head>
Brotli compresses transfer bytes 20–25% better than Gzip on text. Setting Cache-Control: immutable on hashed assets eliminates revalidation round-trips on repeat visits. preconnect warms the TLS handshake to your image CDN before the parser discovers the asset. fetchpriority="high" on the preload pulls the LCP image to the front of the queue. Inlined critical CSS removes one render-blocking round-trip. The media="print" onload trick (or its modern preload equivalent) defers non-critical CSS without breaking the unstyled flash.
Do this today
- Run WebPageTest at
webpagetest.orgagainst your top URL with profile Mobile 4G — Moto G Power. Read the waterfall first row: that is your TTFB. Anything over 600ms is the first thing you fix. - Open PageSpeed Insights, scroll to Diagnostics, and look for “Eliminate render-blocking resources” — every listed resource is a candidate for
defer,async, or media-swap deferral. - In Cloudflare (or your CDN), enable Brotli, HTTP/3, and Auto Minify for HTML/CSS/JS. Set Browser Cache TTL for static assets to 1 year and enable Always Use HTTPS.
- Audit images with Squoosh (
squoosh.app) orcwebp -q 80/avifenc --speed 4 --min 24 --max 30. Convert hero images to AVIF first, WebP fallback. Add explicitwidthandheightto every<img>. - Replace Google Fonts CDN links with self-hosted woff2 files. Use
font-display: swapand<link rel="preload">for the variable font you actually use. Drop unused weights. - In Chrome DevTools → Coverage (open with
Cmd+Shift+P → Show Coverage), reload the page. Any CSS or JS marked >70% unused is a candidate to split or remove. - Configure critical CSS extraction: in Astro, this is
inlineStylesheets: 'auto'; in Next.js,next/fontandoptimizeCss: true; in WordPress, plugins likePerfmattersorWP Rockethandle it. - Set stale-while-revalidate on dynamic HTML at your CDN. On Vercel, that’s
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400returned from the route handler. The first visitor pays for the rebuild; everyone else gets cached HTML. - Audit third-party scripts with Request Map Generator at
requestmap.webperf.tools. Anything outside your domain that isn’t strictly required for the first interaction goes behind arequestIdleCallbackorpartytownworker. - Re-test in WebPageTest after each change. The metric to watch is First Contentful Paint for TTFB-side wins and Largest Contentful Paint for render-side wins. Aim for FCP < 1.5s and LCP < 2.5s on Mobile 4G.
Mark complete
Toggle to remember this module as mastered. Saved to your browser only.
More in this part
Part 5: Technical SEO
- 026 Technical SEO Fundamentals 12m
- 027 Site Architecture 20m
- 028 Crawling & Indexing 17m
- 029 robots.txt Deep Dive 15m
- 030 XML Sitemaps 12m
- 031 Canonical Tags 20m
- 032 Meta Robots & X-Robots-Tag 13m
- 033 HTTP Status Codes 15m
- 034 Crawl Budget Management 16m
- 035 JavaScript SEO 26m
- 036 Core Web Vitals 17m
- 037 Site Speed & Performance You're here 19m
- 038 HTTPS & Site Security 12m
- 039 Mobile SEO & Mobile-First Indexing 14m
- 040 Structured Data & Schema Markup 17m
- 041 International SEO (hreflang) 19m
- 042 Pagination 12m
- 043 Faceted Navigation 26m
- 044 Duplicate Content 13m
- 045 Site Migrations 24m