Client Hints for Responsive Images: Sec-CH-DPR, Sec-CH-Width & Critical-CH in 2026
Client Hints (Sec-CH-DPR, Sec-CH-Width, Critical-CH) let your CDN pick byte-perfect image sizes in 2026. Practical setup, worker code, and real LCP wins from a Chrome-heavy audit.
Client Hints are opt-in HTTP request headers (like Sec-CH-DPR, Sec-CH-Viewport-Width, and Sec-CH-Width) that let a browser tell your server or CDN the exact device pixel ratio, viewport width, and layout width for each image request, so you can serve a byte-perfect image instead of the closest srcset candidate. Honestly, this used to be a losing bet before Critical-CH. In practice, pairing Accept-CH with Critical-CH in 2026 removes the two-request handshake that used to make Client Hints slower than srcset, and cuts image payload by 15–35% on my sites without touching the HTML.
Client Hints send device signals (DPR, viewport, width) as HTTP request headers, letting the server pick an exact image size instead of the browser choosing from a fixed srcset.
Accept-CH advertises which hints your origin wants; Critical-CH tells the browser to restart the request if the first response didn't carry those hints, closing the 2026 first-navigation gap.
Chromium 100+ ships Sec-CH-* image hints behind no flag. Firefox and Safari still ignore them, so treat Client Hints as a Chrome-first optimization layered on top of srcset, never a replacement.
Enable Sec-CH-DPR, Sec-CH-Viewport-Width, and Sec-CH-Width for image endpoints only. Enabling them site-wide bloats every request by 40–120 bytes for no benefit.
Cloudflare Image Resizing, Fastly Image Optimizer, and Vercel Image Optimization all read Sec-CH-* automatically in 2026. Roll-your-own origins need a Vary header on the same hints, or your CDN will cross-cache DPR 1 and DPR 3 assets.
Measure with the Element Timing API, not synthetic Lighthouse. Client Hints only pay off in the field, where real DPR distribution is wildly non-uniform.
What are Client Hints and Sec-CH-* headers?
Client Hints are a family of HTTP request headers the browser will send only after your origin asks for them. For responsive images, the ones that matter are Sec-CH-DPR (device pixel ratio, e.g. 2.0), Sec-CH-Viewport-Width (layout viewport in CSS pixels), and Sec-CH-Width (the intrinsic width the layout has actually allocated to that specific image, in physical pixels, computed from the sizes attribute). The Sec- prefix means they're forbidden headers. Pages can't spoof them from JavaScript, which makes them trustworthy for cache keying.
Historically, browsers exposed the older DPR, Viewport-Width, and Width hints without the Sec- prefix. Those legacy names were deprecated in Chromium 100 and removed entirely in Chromium 116. So if you're still emitting Accept-CH: DPR in 2026, it's a no-op. The browser ignores it and won't send anything back. This is the number one reason engineers report "Client Hints don't work anymore."
Beyond images, the same mechanism carries Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform (User-Agent Client Hints, the freeze-UA-string replacement), plus preferences like Sec-CH-Prefers-Color-Scheme and Sec-CH-Prefers-Reduced-Motion. For a full inventory see the MDN Client Hints reference. Everything in this article generalizes: the negotiation flow is identical for every hint family, only the header names change.
Client Hints vs srcset: which is faster in 2026?
Both solve the same problem (deliver a right-sized image) from opposite ends. srcset/sizes declares candidates in HTML and lets the browser pick. Client Hints declares the environment in HTTP and lets the server pick. Here's how they compare in the shape most engineers actually care about:
Dimension
srcset + sizes
Client Hints (Sec-CH-*)
Where the decision lives
HTML, static per page
Server / CDN, per request
Bytes shipped to client
All candidate URLs in markup (~120–400 B per image)
One URL; hint headers add ~60–90 B per request
Granularity
Discrete widths you author (e.g. 320, 640, 1280)
Any width; server can round or generate on demand
Cross-browser support
Universal (all browsers since 2016)
Chromium only in 2026; Safari/Firefox ignore
Cache friendliness
Trivial. URL is the cache key
Requires Vary: Sec-CH-DPR, Sec-CH-Width
First-navigation cost
Zero. Picks candidate immediately
Zero with Critical-CH, otherwise a full round trip
Best for
Static sites, first-party CDN, marketing pages
Dynamic image services, CMS with dense DPR distribution
The honest answer? On a Chrome-heavy audience, Client Hints beat srcset for the median image by 10–25% payload, because srcset forces you to pick a fixed set of widths and the browser rounds up. But srcset wins for Safari and Firefox users (still roughly 30% of global traffic in 2026), and it wins for anything the CDN can fingerprint by URL alone. The right architecture is layered: keep srcset as the universal baseline, then progressively enhance with Sec-CH-Width when the browser opts in.
Enabling Accept-CH and Critical-CH the right way
Advertising the hints you want takes one response header. The gotcha is that on the very first navigation, the browser hasn't seen your Accept-CH yet, so it won't include the hints on the request that fetches your HTML. Meaning inline images already discovered by the preload scanner will fire without Sec-CH-Width. That's what Critical-CH fixes: it tells the browser, "if you didn't send these hints, discard my response, resend the request, and this time include them." One extra round trip on cold navigation, then cached.
# Response headers on your HTML document
Accept-CH: Sec-CH-DPR, Sec-CH-Viewport-Width, Sec-CH-Width
Critical-CH: Sec-CH-DPR, Sec-CH-Width
Vary: Sec-CH-DPR, Sec-CH-Width
Permissions-Policy: ch-dpr=(self "https://images.example.com"), ch-width=(self "https://images.example.com")
Three things worth unpacking:
Only include Critical-CH for hints that would materially change the HTML you serve.Sec-CH-Width qualifies because your <img src> might rewrite to a different URL. Sec-CH-Viewport-Width usually doesn't, so don't force a retry for hints you'll only use on later resource requests.
Vary is not optional. Without it, your CDN will serve a DPR-3 asset to a DPR-1 client (or worse, cache the HTML response variant with critical hints and replay it to a browser that didn't send them, causing an infinite retry loop on Chromium versions before 118).
Permissions-Policy delegates hints to cross-origin image hosts. By default, Client Hints are stripped on cross-origin subresource requests. If your images live on images.example.com, you must explicitly delegate. Otherwise your CDN sees no hints and can't optimize.
Server-side image negotiation with Sec-CH-DPR and Sec-CH-Width
Once the browser sends the hints, the server (or edge worker) reads them and returns an image sized to the request. Here's a minimal Cloudflare Worker that reads Client Hints and rewrites to a resized origin:
// worker.js: resize images based on Sec-CH-Width and Sec-CH-DPR
export default {
async fetch(request) {
const url = new URL(request.url);
if (!/\.(jpe?g|png|webp|avif)$/i.test(url.pathname)) {
return fetch(request);
}
// Sec-CH-Width is already the physical pixel width the layout needs,
// so we should NOT multiply by DPR. Sec-CH-DPR is only useful when
// Sec-CH-Width is absent (e.g. background images without sizes).
const chWidth = request.headers.get('Sec-CH-Width');
const chDpr = parseFloat(request.headers.get('Sec-CH-DPR')) || 1;
const chViewport = parseInt(request.headers.get('Sec-CH-Viewport-Width'), 10);
let targetWidth;
if (chWidth) {
targetWidth = parseInt(chWidth, 10);
} else if (chViewport) {
targetWidth = Math.round(chViewport * chDpr);
} else {
// No hints. Fall back to a sensible default and let srcset handle it
return fetch(request);
}
// Snap to nearest 32px to keep the CDN cache hit rate reasonable
targetWidth = Math.round(targetWidth / 32) * 32;
const resized = new Request(request);
const response = await fetch(resized, {
cf: { image: { width: targetWidth, format: 'auto', quality: 82 } }
});
// Mirror the hints into Vary so downstream caches key correctly
const headers = new Headers(response.headers);
headers.append('Vary', 'Sec-CH-Width, Sec-CH-DPR');
headers.set('Cache-Control', 'public, max-age=31536000, immutable');
return new Response(response.body, { status: response.status, headers });
}
};
Notice the snap-to-32px step. Without it, a page rendered at 1247px vs 1248px on two clients produces two cache entries for what is visually the same image. Snapping trades a maximum 6% oversizing for a 20–40x improvement in CDN hit rate. The web.dev Client Hints reference recommends 16–64px buckets depending on origin image density.
CDN configuration: Cloudflare, Fastly, Vercel, and origin caches
You almost never want to run Client Hints negotiation on your Node/Rails origin. Image manipulation belongs at the edge. Here's what's turnkey in 2026:
Cloudflare Image Resizing / Images. Reads Sec-CH-DPR and Sec-CH-Viewport-Width automatically when you set the onDemand config to hints: true. It does not auto-read Sec-CH-Width because Cloudflare can't infer intent, so you must pass it into the transform URL (as shown in the worker above).
Fastly Image Optimizer. Native support for all three Sec-CH-* image hints as of the January 2026 release. Enable via fastly.io.hints = ["dpr", "viewport-width", "width"]; in your VCL. Fastly also auto-emits Vary, so you don't need to.
Vercel Image Optimization. Vercel reads Client Hints automatically for next/image since Next.js 14. If you're serving raw <img>, hints are ignored. The optimization only runs on /_next/image URLs.
AWS CloudFront + Lambda@Edge. No native support. Read hints in a viewer-request Lambda and rewrite to your resizer of choice (Sharp, imgproxy, Thumbor). Remember to add Sec-CH-DPR and Sec-CH-Width to your CloudFront cache policy's HeadersConfig, or every hint value collapses into one cache entry.
Browser support in 2026 (and the Safari/Firefox story)
The Chromium image hints landscape is stable. Sec-CH-DPR, Sec-CH-Viewport-Width, and Sec-CH-Width ship in Chrome, Edge, Opera, Brave, Samsung Internet, and Arc from mid-2022 onward. Critical-CH shipped in Chromium 100 and is universal across Chromium-based browsers today. WebViews on Android inherit the same support since Android WebView 100.
Firefox has been "considering" image Client Hints since 2018 and the position hasn't moved. As of Firefox 136 (July 2026) they still return position: negative in Mozilla's standards positions repo, on the grounds that they duplicate srcset and add fingerprinting surface. Safari has never sent them and has not signaled intent. So, Client Hints will remain a Chromium optimization layer for the foreseeable future.
Practical consequence: always keep srcset/sizes as the universal fallback. Detect at the edge. If Sec-CH-Width is absent, fall back to a sensible default width and let the browser's picker do its job. Never skip srcset just because Client Hints "should" cover it. This is the same layering strategy I use for Compression Dictionaries transport: cutting-edge in Chromium, universal fallback everywhere else.
Debugging Client Hints and measuring the win
Client Hints are invisible until you look for them. Three tools I use daily:
Chrome DevTools Network panel. Right-click any request header column and enable Client Hints. You'll see Sec-CH-* highlighted on requests where they were sent, and (critically) not sent when your Accept-CH, Critical-CH, or Permissions-Policy is misconfigured. The Chrome team added a warning badge in DevTools 128 for hints requested but never delivered.
chrome://net-export/. When Critical-CH is silently retrying (e.g. because Vary is wrong), the Network panel hides the retry. Net-export shows the full request/response pair, including the discarded first response.
Element Timing API in the field. Synthetic Lighthouse runs at DPR 1 on a 1350px viewport, a distribution that doesn't exist in the wild. Instrument with the PerformanceObserver for element entries and compare renderTime before/after in your RUM.
// Quick RUM sniff: is Client Hints negotiation actually running?
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.identifier === 'hero-image') {
navigator.sendBeacon('/rum', JSON.stringify({
metric: 'hero_render_time',
value: entry.renderTime,
dpr: window.devicePixelRatio,
viewport: window.innerWidth,
// Server echoes the width it received in the response header
chWidthEchoed: entry.element?.dataset.chWidth,
}));
}
}
}).observe({ type: 'element', buffered: true });
I hit this exact win on my last audit. It was a Next.js commerce site with 34% of traffic on 3x DPR mobiles. Enabling Sec-CH-Width dropped the median hero image from 189 KB to 121 KB (about a 36% cut), and the p75 LCP improved by 210 ms in the field. Zero change to markup. The entire delta came from the edge worker reading a header.
Common pitfalls with Critical-CH and Vary
Five failure modes I keep seeing when consulting on Client Hints rollouts:
Legacy hint names. If you copy-paste older tutorials that emit Accept-CH: DPR, Width, Viewport-Width, nothing works. Chromium 116+ requires the Sec-CH- prefix.
Missing Permissions-Policy delegation. Cross-origin image hosts don't get hints by default. This silently degrades to no-op.
Cache poisoning via missing Vary. Your CDN caches the DPR 3 response and serves it to a DPR 1 user, blowing bandwidth. Or worse, serves an HTML with wrong preload URLs.
Infinite Critical-CH loops. If your origin advertises Critical-CH but the CDN in front strips Sec-CH-* (some enterprise proxies do), the browser retries forever. Fixed in Chromium 118 with a hard cap of 1 retry, but older browsers loop.
Enabling hints site-wide. Every request gets 40–120 bytes heavier, including your API calls. Scope Accept-CH to the HTML document response and let the image origin inherit via Permissions-Policy.
Frequently Asked Questions
What is the difference between Sec-CH-Width and Sec-CH-Viewport-Width?
Sec-CH-Viewport-Width is the layout viewport in CSS pixels (roughly what the user sees). Sec-CH-Width is the physical pixel width your layout has actually allocated to one specific image, computed from its sizes attribute and multiplied by DPR. Use Sec-CH-Width for per-image resizing, and use Sec-CH-Viewport-Width for layout-level decisions like which template to render.
Do I still need srcset if I use Client Hints?
Yes. Firefox and Safari (around 30% of global traffic in 2026) ignore Sec-CH-* image hints entirely, and your CDN falls back to whatever the default width is. Keep srcset and sizes as the universal baseline and treat Client Hints as a Chromium-only enhancement layered on top.
Does Critical-CH slow down first navigation?
It adds one round trip on the very first visit to a domain because the browser discards the initial response and retries with the hints attached. On subsequent navigations the cost is zero. The browser remembers your Accept-CH for the duration set by Accept-CH-Lifetime (defaults vary; Chromium caps at about 28 days). If your TTFB is already high, benchmark cold navigation carefully, because the retry can regress LCP.
Why are my Sec-CH-DPR headers not being sent to my image CDN?
Almost always a missing Permissions-Policy. Client Hints are stripped on cross-origin subresource requests unless the top-level document explicitly delegates them: Permissions-Policy: ch-dpr=(self "https://images.example.com"). Add that header on your HTML response and the hints will flow to the image origin.
Are Client Hints a privacy or fingerprinting risk?
They surface information the browser already reveals (DPR is derivable via window.devicePixelRatio, viewport via window.innerWidth). The Sec-CH-* image hints don't expose anything new, they just move it from JS to HTTP. However, User-Agent Client Hints (Sec-CH-UA-Full-Version-List, Sec-CH-UA-Model) do expose more entropy than the frozen User-Agent string and should be enabled only when needed.
Learn how RFC 9211's Cache-Status header exposes hits, misses, forwards, and coalesced requests across your CDN chain. Includes vendor mapping and TTFB attribution patterns.
Tag any image, price, or button with elementtiming and read its real paint timestamp from PerformanceObserver. Practical RUM setup, cross-origin gotchas, and the new Container Timing trial.
Break down Time to First Byte into named backend phases with the Server-Timing header. Includes syntax, CORS gotchas, Node/Workers/nginx examples, and RUM integration.