Skip to content
  • CS-11
  • Reference
  • Last checked November 7, 2026

Website Performance Metrics and Headers Cheat Sheet

Core Web Vitals thresholds and what moves them, supporting metrics, Cache-Control directives, validators and Vary, Cloudflare cache statuses, resource hints, responsive images and font-display.

Run the health check

Performance work has two halves: measuring the right thing (Core Web Vitals from real users, backed by lab tests to find causes) and controlling what the browser and caches do (HTTP caching headers, resource hints, image and font markup). This sheet covers both. Official thresholds come from Google’s web.dev documentation; anything that is guidance rather than an official threshold is labeled.

Placeholders: EXAMPLE.COM is your domain, /PATH/hero.webp and similar paths are your own files, and CDN.EXAMPLE.COM is a third-party or CDN origin. Check headers on any URL with curl -sI https://EXAMPLE.COM/ (see CS-08).

Core Web Vitals

Metric Measures Good Needs improvement Poor
LCP (Largest Contentful Paint) Loading: when the largest image or text block in the viewport is rendered 2.5 s or less Over 2.5 s up to 4.0 s Over 4.0 s
INP (Interaction to Next Paint) Responsiveness: delay from a click, tap or key press to the next frame, across the visit (close to the worst interaction) 200 ms or less Over 200 ms up to 500 ms Over 500 ms
CLS (Cumulative Layout Shift) Visual stability: unexpected movement of content, scored by size and distance of shifts 0.1 or less Over 0.1 up to 0.25 Over 0.25
Rule Detail
75th percentile A page passes a metric when at least 75% of page views meet the “good” threshold, assessed separately for mobile and desktop.
All three must pass The Core Web Vitals assessment in PageSpeed Insights and Search Console needs all three metrics to be good.
INP replaced FID INP became a Core Web Vital in March 2024, replacing First Input Delay. FID only measured the delay before the first interaction was handled.
CLS windows Shifts are grouped in session windows (up to 5 s, with gaps under 1 s). CLS is the worst window, not the total of the whole visit. Shifts within 500 ms of a user input do not count.

What affects each metric

Metric Common causes Typical fixes
LCP Slow server response (TTFB), LCP image discovered late (CSS background, lazy-loaded, injected by JavaScript), large unoptimized image, render-blocking CSS and JS, web fonts hiding text, client-side rendering Page caching and a CDN, fetchpriority="high" on the LCP image, never lazy-load it, preload if it is a CSS background, serve AVIF or WebP at the right size, inline critical CSS, defer non-critical JS
LCP breakdown web.dev splits LCP into four parts: TTFB, resource load delay, resource load duration and element render delay Find the biggest part in Chrome DevTools (Performance panel) or PageSpeed Insights and fix that one first
INP Long JavaScript tasks on the main thread (over 50 ms), heavy third-party scripts (chat, tag managers, ads), large DOM size, expensive event handlers, layout thrashing Remove or delay third-party scripts, break up long tasks (yield to the main thread), reduce DOM size, avoid running work on every scroll or input event
INP breakdown Input delay, processing duration and presentation delay Input delay means the main thread was busy; processing means slow handlers; presentation means heavy rendering afterward
CLS Images, videos and iframes without dimensions, ads and embeds without reserved space, banners and cookie notices injected above content, web fonts swapping with different metrics, animations of layout properties width and height attributes or CSS aspect-ratio, reserve space with min-height, overlay banners instead of pushing content, font fallback metric overrides, animate transform and opacity only

Field data vs lab data

Compare Field (real users, RUM) Lab (synthetic)
Source Chrome UX Report (CrUX) or your own real-user monitoring Lighthouse, PageSpeed Insights lab section, WebPageTest, DevTools
Devices and networks Your actual visitors One simulated device and connection
Measures INP? Yes No: there is no real user interacting. Use TBT as a proxy.
Used for Google’s page experience assessment Yes (CrUX) No
Speed of feedback Slow: CrUX is a rolling 28-day window, so a fix takes up to four weeks to show fully Immediate
Best for Knowing whether you have a problem Finding the cause and testing a fix
Tool Shows
PageSpeed Insights CrUX field data for the URL and the origin (if there is enough traffic), plus a Lighthouse lab run
Search Console, Core Web Vitals report CrUX data grouped by similar URLs, for the whole property
CrUX API, CrUX BigQuery dataset Raw field data for scripts and history
Chrome DevTools, Performance panel Live LCP, INP and CLS as you use the page, plus traces to find causes
web-vitals JavaScript library Collect the same metrics from your own visitors and send them to your analytics

CrUX only includes Chrome users who have opted in to sharing usage statistics, and only URLs and origins with enough traffic. A low-traffic site may have no field data at all; PageSpeed Insights then shows only the lab run.

Supporting metrics

Metric Measures Target Status of target
TTFB (Time to First Byte) From navigation start to the first byte of the HTML response, including redirects, DNS, connection and TLS Good 0.8 s or less, poor over 1.8 s Guidance from web.dev (“a rough guide”), not a Core Web Vital
FCP (First Contentful Paint) When the first text or image is painted Good 1.8 s or less, poor over 3.0 s web.dev threshold at the 75th percentile, not a Core Web Vital
TBT (Total Blocking Time) Lab only: total time between FCP and Time to Interactive where long tasks blocked the main thread (the part of each task over 50 ms) Lighthouse shows 0 to 200 ms as green on mobile (150 ms on desktop) Lighthouse scoring band, lab proxy for INP
Speed Index Lab only: how quickly the visible area fills in, from a video of the load Lighthouse shows 0 to 3.4 s as green on mobile (1.3 s on desktop) Lighthouse scoring band

The Lighthouse score (0 to 100) is a weighted blend of lab metrics and changes between Lighthouse versions. It is a diagnostic aid, not the Core Web Vitals assessment, and a page can score well in the lab and still fail in the field (or the reverse).

HTTP caching: Cache-Control

Cache-Control (defined in RFC 9111) tells browsers and shared caches (CDNs, proxies) whether and for how long to keep a response. Directives are comma-separated: Cache-Control: public, max-age=31536000, immutable.

Directive Meaning Typical use
public Any cache may store it, even if the request had an Authorization header Static assets
private Only the user’s browser may store it, not shared caches Pages personalized for a logged-in user
no-cache Caches may store it but must revalidate with the server before every use. It does not mean “do not cache”. HTML that must always be current but can use 304 responses
no-store Do not store at all Account pages, checkout, sensitive data
max-age=N Fresh for N seconds from the time of the response 31536000 (one year) for versioned assets
s-maxage=N Like max-age but only for shared caches, overriding max-age there Let the CDN keep HTML for minutes while browsers revalidate
stale-while-revalidate=N After expiry, a cache may serve the stale copy for up to N more seconds while it fetches a fresh one in the background (RFC 5861) Fast responses for content that can be slightly out of date
stale-if-error=N May serve a stale copy for N seconds if the origin returns an error or cannot be reached (RFC 5861) Resilience during origin outages, where the cache supports it
immutable The response will never change while fresh, so the browser should not revalidate it even on reload (RFC 8246) Files with a version or hash in the URL
must-revalidate Once stale, it must not be used without successful revalidation (no serving stale on error) Content where stale data would be wrong
proxy-revalidate Same as must-revalidate, for shared caches only Rare
no-transform Intermediaries must not modify the body (for example recompress images) When a proxy’s optimization breaks files

Recommended patterns

Resource Header
Versioned CSS, JS, fonts, images (style.css?ver=6.8, app.3f9a1c.js) Cache-Control: public, max-age=31536000, immutable
Unversioned images and media Cache-Control: public, max-age=86400 (pick a lifetime you can live with: a changed file keeps its old copy in browsers until it expires)
Public HTML pages behind a CDN Cache-Control: public, max-age=0, s-maxage=600, stale-while-revalidate=60, or rely on the CDN’s own HTML cache rules
Logged-in or personalized HTML Cache-Control: private, no-cache
Checkout, account, admin Cache-Control: no-store

Validators and Vary

Header Role
ETag: "abc123" An identifier for this version of the resource. The browser sends it back as If-None-Match; if unchanged, the server answers 304 Not Modified with no body.
ETag: W/"abc123" Weak validator: the content is equivalent but not necessarily byte-identical.
Last-Modified Date the resource last changed. The browser sends If-Modified-Since; the server can answer 304. ETag takes precedence when both are present.
Age: 120 Seconds the response has been in a shared cache. A rising Age on repeat requests proves the CDN is caching.
Expires Older absolute expiry date. Ignored when Cache-Control: max-age is present.
Vary: Accept-Encoding Keep separate cached copies per compression (gzip, br, none). Needed whenever compression is on.
Vary: Cookie or Vary: User-Agent One copy per distinct value, which can make a cache almost useless. Avoid on public pages.
Vary: Origin Needed when CORS responses differ by requesting origin.

Multiple servers behind a load balancer can produce different ETags for the same file (for example when the ETag includes an inode number), which breaks revalidation. Use content-based ETags or Last-Modified in that case.

Cloudflare cache status (cf-cache-status)

Value Meaning What to do
HIT Served from Cloudflare’s cache without contacting the origin Working as intended
MISS Not in this data center’s cache, fetched from the origin (and stored if cacheable) Request again: it should become HIT. Each data center has its own cache.
EXPIRED Was cached, but had expired, so it was fetched again from the origin Consider a longer edge TTL
STALE An expired copy was served because the origin could not be reached or returned an error Investigate the origin
BYPASS Eligible for caching, but caching was skipped: the origin sent no-cache, private or max-age=0, a rule said bypass, or the origin set a cookie Check origin headers and Set-Cookie on that URL
DYNAMIC Not eligible for caching under the current settings. By default Cloudflare does not cache HTML, so normal pages show DYNAMIC. Add a Cache Rule if you want HTML cached (and exclude logged-in users, carts and admin)
REVALIDATED The stale copy was revalidated with the origin (for example with a 304) and served Normal
UPDATING A stale copy was served while the cache fetched a fresh one in the background Normal, especially with stale-while-revalidate behavior

Other CDNs and caches use their own headers, often X-Cache: HIT or MISS, plus Age. Server page caches add headers such as X-LiteSpeed-Cache: hit. Check the provider’s documentation for exact values.

Resource hints and priority

Hint Example Use for Caution
preload <link rel="preload" href="/PATH/hero.webp" as="image" fetchpriority="high"> Critical resources the browser finds late: a CSS background LCP image, a font referenced only in CSS Needs a correct as value or the file may download twice. Every preload competes with other downloads, so keep the list short. An unused preload triggers a console warning.
Preload a font <link rel="preload" href="/PATH/font.woff2" as="font" type="font/woff2" crossorigin> The main text font crossorigin is required even for same-origin fonts, because fonts are fetched in CORS mode.
Preload a responsive image <link rel="preload" as="image" imagesrcset="hero-800.webp 800w, hero-1600.webp 1600w" imagesizes="100vw"> Responsive LCP images Must match the srcset and sizes on the image itself.
preconnect <link rel="preconnect" href="https://CDN.EXAMPLE.COM"> Opens DNS, TCP and TLS early for a third-party origin you will use soon Limit to a few critical origins. Add crossorigin for origins fetched in CORS mode (fonts).
dns-prefetch <link rel="dns-prefetch" href="https://CDN.EXAMPLE.COM"> Cheaper than preconnect: DNS lookup only Useful for less critical origins.
modulepreload <link rel="modulepreload" href="/PATH/app.js"> ES modules: fetches, parses and compiles the module ahead of use For type="module" scripts only.
prefetch <link rel="prefetch" href="/next-page/"> Low-priority fetch of something likely needed for the next navigation Wastes data if the guess is wrong.
fetchpriority <img src="/PATH/hero.webp" fetchpriority="high" alt=""> Raise the LCP image; lower off-screen carousel images with low. Values: high, low, auto. A hint, not a command. Works on img, link and script elements.
HTTP Link header Link: </PATH/style.css>; rel=preload; as=style Send hints in the response headers, or in a 103 Early Hints response where the server and CDN support it Same rules as the HTML version.

Images

Format Best for Notes
AVIF Photos and complex images at the smallest size Supports transparency and HDR. Slower to encode. Supported in all current major browsers.
WebP Photos and graphics, lossy or lossless Transparency and animation. Very broad support. WordPress accepts WebP uploads and, since 6.5, AVIF if the server’s image library supports it.
JPEG Photos, maximum compatibility No transparency. Progressive JPEG renders in passes.
PNG Screenshots, graphics needing lossless quality Large for photos.
SVG Logos, icons, illustrations Scales to any size. Text-based, so compress it. Uploading SVGs needs sanitizing because they can contain scripts.
GIF Legacy animation Replace with a muted, looping <video> or animated WebP for a fraction of the size.

srcset and sizes

<img src="/PATH/photo-800.jpg"
     srcset="/PATH/photo-400.jpg 400w,
             /PATH/photo-800.jpg 800w,
             /PATH/photo-1600.jpg 1600w"
     sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 600px"
     width="1600" height="1067"
     alt="DESCRIPTION"
     loading="lazy" decoding="async">
Part Meaning
400w Width descriptor: the file’s real pixel width. The browser picks a file using the slot width and the screen’s pixel density.
sizes How wide the image will display, as media conditions checked in order; the last value is the default. Without it, the browser assumes 100vw and often downloads too large a file.
1x, 2x Density descriptors, for fixed-size images: srcset="logo.png 1x, logo@2x.png 2x". Do not mix with w in one srcset.
width and height Intrinsic size, so the browser reserves the right aspect ratio before the file loads (prevents CLS). Pair with CSS height: auto.
loading="lazy" Defers off-screen images. Never on the LCP image or anything above the fold. WordPress skips lazy loading on the first content images it detects.
sizes="auto" For lazy-loaded images, lets the browser use the laid-out width. Supported in Chromium-based browsers at the time of writing; other browsers fall back to the rest of the sizes list. WordPress 6.7 and later prepends auto to sizes on lazy-loaded images.
decoding="async" Lets the browser decode the image off the critical path.

Serving modern formats with fallback:

<picture>
  <source type="image/avif" srcset="/PATH/hero.avif">
  <source type="image/webp" srcset="/PATH/hero.webp">
  <img src="/PATH/hero.jpg" width="1600" height="900" alt="DESCRIPTION" fetchpriority="high">
</picture>

Web fonts: font-display

Set in the @font-face rule. The block period hides text (invisible fallback); the swap period shows fallback text and swaps in the web font when it arrives.

Value Block period Swap period Result
auto Browser decides Browser decides Usually behaves like block
block Short (typically 3 s) Infinite Invisible text first, which hurts FCP and LCP. Suits icon fonts.
swap Extremely small Infinite Text shows at once in the fallback, then swaps. Good for LCP, can cause layout shift.
fallback Extremely small (around 100 ms) Short (around 3 s) If the font is late, the fallback stays for that page view.
optional Extremely small None Uses the web font only if it is already available (or arrives almost instantly). Best for CLS; the font may not appear on the first visit.

Reduce font-swap layout shift with fallback metric overrides (size-adjust, ascent-override, descent-override, line-gap-override) on a local fallback @font-face. Self-host WOFF2 files and subset them to the characters you use to cut their size.

Common recipes

Goal Steps
Find the LCP element PageSpeed Insights diagnostics (“Largest Contentful Paint element”), or the DevTools Performance panel live metrics
Speed up a hero image LCP Use an img (not a CSS background), add fetchpriority="high", remove loading="lazy", serve AVIF or WebP with srcset, make sure the page is cached so TTFB is low
Prove caching works curl -sI URL | grep -iE 'cache-control|age|cf-cache-status|x-cache|etag' twice; expect a HIT and a rising Age on the second
Find what causes CLS DevTools Performance panel layout shift markers, or the PageSpeed Insights “Avoid large layout shifts” diagnostic; then add dimensions or reserved space
Improve INP on a WordPress site Audit third-party and plugin scripts in the DevTools Performance panel, delay non-essential scripts until interaction, remove unused plugins that add front-end JS
Check a fix is working in the field Watch the PageSpeed Insights URL data or the Search Console report over the following 28 days; use the web-vitals library for faster feedback

Gotchas

Problem Explanation
Lighthouse score is good but Core Web Vitals fail The lab test uses one device and network; real visitors may be on slower phones. Field data decides the assessment.
A fix does not show in PageSpeed Insights field data CrUX is a 28-day rolling window. The lab section updates immediately; the field section changes gradually.
no-cache pages still cached Correct behavior: no-cache means revalidate, not “do not store”. Use no-store to forbid storage.
CDN always shows BYPASS on pages WordPress or a plugin sets a cookie (sessions, carts, consent) or sends no-cache headers on every page.
Long max-age on unversioned files Visitors keep the old file until it expires. Version file names or query strings before using a one-year lifetime.
Preloading everything Preloads compete with the LCP image and CSS. Preload only what the browser would otherwise discover late.
Lazy-loading the hero image Delays LCP because the browser waits for layout before fetching it. Remove loading="lazy" above the fold.
Font preload without crossorigin The preloaded copy is not reused and the font downloads twice.
Missing Vary: Accept-Encoding A cache can serve a compressed response to a client that cannot decode it.

Need the full fix?

Cheat sheets are quick reference. The field guides walk you through each emergency step by step.

Open the Emergency Room

Search the field manual

What is going wrong?

Press / to search from any page, Esc to close