[{"data":1,"prerenderedAt":1148},["ShallowReactive",2],{"content:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002Fusing-103-early-hints-behind-a-cdn":3,"surroundings:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002Fusing-103-early-hints-behind-a-cdn":1139},{"id":4,"title":5,"body":6,"description":1120,"extension":1121,"meta":1122,"navigation":362,"path":1134,"seo":1135,"stem":1137,"__hash__":1138},"content\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002Fusing-103-early-hints-behind-a-cdn\u002Findex.md","Using 103 Early Hints Behind a CDN",{"type":7,"value":8,"toc":1112},"minimark",[9,14,42,45,50,87,238,242,248,254,260,266,270,276,541,544,549,749,756,762,776,856,860,866,869,971,978,982,1007,1016,1025,1048,1057,1072,1076,1097,1102,1105,1108],[10,11,13],"h1",{"id":12},"how-to-use-103-early-hints-behind-a-cdn","How to Use 103 Early Hints Behind a CDN",[15,16,17,18,23,24,28,29,33,34,37,38,41],"p",{},"This walkthrough extends ",[19,20,22],"a",{"href":21},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002F","Resource Hints & Early Hints"," in ",[19,25,27],{"href":26},"\u002Fadvanced-caching-strategies-cdn-architecture\u002F","Advanced Caching Strategies & CDN Architecture",". Your origin takes ",[30,31,32],"code",{},"500ms"," to render a personalised page. During that half second the browser has an open connection and nothing to do. A ",[30,35,36],{},"103 Early Hints"," response fills that window: the server sends ",[30,39,40],{},"Link"," headers immediately, the browser starts fetching the CSS and fonts, and by the time the HTML arrives those resources are already in flight or complete.",[15,43,44],{},"The technique is only worth deploying where that dead window actually exists, and it has one failure mode — hinting the previous deploy's filenames — that is worth designing against up front.",[46,47,49],"h2",{"id":48},"rapid-diagnosis-is-there-dead-time-to-fill","Rapid Diagnosis: Is There Dead Time to Fill?",[51,52,53,65,75,81],"ul",{},[54,55,56,60,61,64],"li",{},[57,58,59],"strong",{},"Measure TTFB per route."," Below roughly ",[30,62,63],{},"200ms"," there is nothing to fill and the extra informational response can cost more than it saves.",[54,66,67,70,71,74],{},[57,68,69],{},"Separate cache hits from misses."," An edge-cached page has near-zero think time; the same route on a miss may have ",[30,72,73],{},"600ms",". Early Hints pays off on the miss.",[54,76,77,80],{},[57,78,79],{},"Check what the browser does during the wait."," In the Network panel, a long green \"Waiting (TTFB)\" bar with no other activity is exactly the window Early Hints targets.",[54,82,83,86],{},[57,84,85],{},"Confirm end-to-end support."," The client, the CDN and any intermediate proxy must all forward informational responses. One HTTP\u002F1.1 hop in the chain can drop them silently.",[88,89,96,97,104,105,104,109,104,113,104,122,104,129,104,136,104,140,104,144,104,150,104,152,104,154,104,158,104,163,104,169,104,173,104,176,104,181,104,188,104,192,104,198,104,206,104,211,104,216,104,220,104,225,104,228,104,231,104,234,104],"svg",{"xmlns":90,"viewBox":91,"width":92,"role":93,"ariaLabel":94,"style":95},"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","0 0 760 254","100%","img","Sequence diagram of a browser, CDN and origin where the CDN returns a 103 response with Link headers while the origin is still rendering, letting the browser fetch CSS and fonts before the 200 response arrives","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)","\n  ",[98,99],"rect",{"className":100,"x":102,"y":102,"width":92,"height":92,"fill":103},[101],"svg-canvas","0","#ffffff"," ",[106,107,108],"title",{},"Early Hints across browser, CDN and origin",[110,111,112],"desc",{},"The browser requests a page, the CDN immediately returns a 103 with Link headers and forwards the request to the origin; the browser fetches the hinted CSS and font while the origin renders, then receives the 200 response.",[98,114],{"x":115,"y":115,"width":116,"height":117,"rx":118,"fill":119,"stroke":120,"style":121},"1","758","252","10","none","currentColor","stroke-opacity:0.18",[123,124,128],"text",{"x":125,"y":126,"fill":120,"style":127},"24","32","font-size:16px;font-weight:700","What happens during the 500ms wait",[123,130,135],{"x":131,"y":132,"fill":120,"style":133,"textAnchor":134},"90","60","font-size:11.5px;font-weight:700;","middle","Browser",[123,137,139],{"x":138,"y":132,"fill":120,"style":133,"textAnchor":134},"380","CDN edge",[123,141,143],{"x":142,"y":132,"fill":120,"style":133,"textAnchor":134},"670","Origin",[145,146],"line",{"x1":131,"y1":147,"x2":131,"y2":148,"stroke":120,"style":149},"70","230","stroke-opacity:0.25",[145,151],{"x1":138,"y1":147,"x2":138,"y2":148,"stroke":120,"style":149},[145,153],{"x1":142,"y1":147,"x2":142,"y2":148,"stroke":120,"style":149},[145,155],{"x1":131,"y1":131,"x2":156,"y2":131,"stroke":120,"style":157},"366","stroke-opacity:0.5",[159,160],"polygon",{"points":161,"fill":120,"style":162},"374,90 364,85 364,95","fill-opacity:0.6",[123,164,168],{"x":165,"y":166,"fill":120,"style":167,"textAnchor":134},"228","84","font-size:10.5px;","GET \u002Faccount",[145,170],{"x1":138,"y1":171,"x2":172,"y2":171,"stroke":120,"style":157},"112","656",[159,174],{"points":175,"fill":120,"style":162},"664,112 654,107 654,117",[123,177,180],{"x":178,"y":179,"fill":120,"style":167,"textAnchor":134},"518","106","forwarded",[145,182],{"x1":183,"y1":184,"x2":185,"y2":184,"stroke":186,"style":187},"374","134","98","#b8860b","stroke-opacity:0.9",[159,189],{"points":190,"fill":186,"style":191},"90,134 100,129 100,139","fill-opacity:0.85",[123,193,197],{"x":194,"y":195,"fill":186,"style":196,"textAnchor":134},"236","128","font-size:10.5px;font-weight:700;","103 Early Hints + Link",[98,199],{"x":200,"y":201,"width":148,"height":202,"rx":203,"fill":204,"stroke":204,"style":205},"120","146","26","4","#0466c8","fill-opacity:0.18;stroke-opacity:0.5",[123,207,210],{"x":208,"y":209,"fill":120,"style":167,"textAnchor":134},"235","164","fetching CSS + font",[98,212],{"x":213,"y":201,"width":214,"height":202,"rx":203,"fill":120,"stroke":120,"style":215},"596","150","fill-opacity:0.06;stroke-opacity:0.3",[123,217,219],{"x":218,"y":209,"fill":120,"style":167,"textAnchor":134},"671","still rendering",[145,221],{"x1":222,"y1":223,"x2":224,"y2":223,"stroke":120,"style":157},"664","196","388",[159,226],{"points":227,"fill":120,"style":162},"380,196 390,191 390,201",[145,229],{"x1":183,"y1":230,"x2":185,"y2":230,"stroke":120,"style":157},"218",[159,232],{"points":233,"fill":120,"style":162},"90,218 100,213 100,223",[123,235,237],{"x":194,"y":236,"fill":120,"style":167,"textAnchor":134},"212","200 OK — CSS already parsed",[46,239,241],{"id":240},"root-cause-analysis-why-deployments-get-this-wrong","Root Cause Analysis: Why Deployments Get This Wrong",[15,243,244,247],{},[57,245,246],{},"1. Enabling it where TTFB is already fast."," On an edge-cached page the HTML arrives in tens of milliseconds. The informational response adds protocol overhead with no window to exploit, and measurements show no improvement — which then gets misread as \"Early Hints does not work\".",[15,249,250,253],{},[57,251,252],{},"2. Hinting stale filenames."," Asset filenames are content-hashed and change on every build. Hints written by hand, or cached at the edge from a previous deploy, point at files that no longer exist — producing high-priority 404s during the most latency-sensitive moment of the page load.",[15,255,256,259],{},[57,257,258],{},"3. Hinting too much."," The temptation is to list every asset. Each hint competes with the HTML response that is about to arrive, and a dozen hinted files on a mobile connection will delay the document itself.",[15,261,262,265],{},[57,263,264],{},"4. Assuming the chain preserves it."," Informational responses require HTTP\u002F2 or HTTP\u002F3 support along the whole path, and some proxies drop them. Without an end-to-end check you may be sending hints nobody receives.",[46,267,269],{"id":268},"step-by-step-resolution","Step-by-Step Resolution",[15,271,272,275],{},[57,273,274],{},"1. Generate the hint list from the build manifest."," Never write asset paths by hand.",[277,278,283],"pre",{"className":279,"code":280,"language":281,"meta":282,"style":282},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F build-hints.mjs — one Link header value per route, from the real manifest.\nimport { readFileSync, writeFileSync } from 'node:fs';\nconst manifest = JSON.parse(readFileSync('dist\u002Fmanifest.json', 'utf8'));\n\nconst hints = Object.fromEntries(Object.entries(manifest.routes).map(([route, a]) => [\n  route,\n  [\n    `\u003C${a.css}>; rel=preload; as=style`,\n    `\u003C${a.font}>; rel=preload; as=font; type=\"font\u002Fwoff2\"; crossorigin`,\n    '\u003Chttps:\u002F\u002Fimg.example-cdn.com>; rel=preconnect'\n  ].join(', ')\n]));\nwriteFileSync('dist\u002Fearly-hints.json', JSON.stringify(hints, null, 2));\n\u002F\u002F trade-off: this couples the edge config to the build output, so a deploy that\n\u002F\u002F updates one without the other breaks the hints. Ship them together as one\n\u002F\u002F artifact and version them with the same hash as the assets.\n","javascript","",[30,284,285,293,314,357,364,412,418,424,443,460,466,483,489,523,529,535],{"__ignoreMap":282},[286,287,289],"span",{"class":145,"line":288},1,[286,290,292],{"class":291},"sjfSM","\u002F\u002F build-hints.mjs — one Link header value per route, from the real manifest.\n",[286,294,296,300,304,307,311],{"class":145,"line":295},2,[286,297,299],{"class":298},"sPARh","import",[286,301,303],{"class":302},"saISM"," { readFileSync, writeFileSync } ",[286,305,306],{"class":298},"from",[286,308,310],{"class":309},"sZ8jY"," 'node:fs'",[286,312,313],{"class":302},";\n",[286,315,317,320,324,327,330,333,337,340,343,345,348,351,354],{"class":145,"line":316},3,[286,318,319],{"class":298},"const",[286,321,323],{"class":322},"sPXB4"," manifest",[286,325,326],{"class":298}," =",[286,328,329],{"class":322}," JSON",[286,331,332],{"class":302},".",[286,334,336],{"class":335},"smZ65","parse",[286,338,339],{"class":302},"(",[286,341,342],{"class":335},"readFileSync",[286,344,339],{"class":302},[286,346,347],{"class":309},"'dist\u002Fmanifest.json'",[286,349,350],{"class":302},", ",[286,352,353],{"class":309},"'utf8'",[286,355,356],{"class":302},"));\n",[286,358,360],{"class":145,"line":359},4,[286,361,363],{"emptyLinePlaceholder":362},true,"\n",[286,365,367,369,372,374,377,380,383,386,389,392,395,399,401,403,406,409],{"class":145,"line":366},5,[286,368,319],{"class":298},[286,370,371],{"class":322}," hints",[286,373,326],{"class":298},[286,375,376],{"class":302}," Object.",[286,378,379],{"class":335},"fromEntries",[286,381,382],{"class":302},"(Object.",[286,384,385],{"class":335},"entries",[286,387,388],{"class":302},"(manifest.routes).",[286,390,391],{"class":335},"map",[286,393,394],{"class":302},"(([",[286,396,398],{"class":397},"sQw3B","route",[286,400,350],{"class":302},[286,402,19],{"class":397},[286,404,405],{"class":302},"]) ",[286,407,408],{"class":298},"=>",[286,410,411],{"class":302}," [\n",[286,413,415],{"class":145,"line":414},6,[286,416,417],{"class":302},"  route,\n",[286,419,421],{"class":145,"line":420},7,[286,422,423],{"class":302},"  [\n",[286,425,427,430,432,434,437,440],{"class":145,"line":426},8,[286,428,429],{"class":309},"    `\u003C${",[286,431,19],{"class":302},[286,433,332],{"class":309},[286,435,436],{"class":302},"css",[286,438,439],{"class":309},"}>; rel=preload; as=style`",[286,441,442],{"class":302},",\n",[286,444,446,448,450,452,455,458],{"class":145,"line":445},9,[286,447,429],{"class":309},[286,449,19],{"class":302},[286,451,332],{"class":309},[286,453,454],{"class":302},"font",[286,456,457],{"class":309},"}>; rel=preload; as=font; type=\"font\u002Fwoff2\"; crossorigin`",[286,459,442],{"class":302},[286,461,463],{"class":145,"line":462},10,[286,464,465],{"class":309},"    '\u003Chttps:\u002F\u002Fimg.example-cdn.com>; rel=preconnect'\n",[286,467,469,472,475,477,480],{"class":145,"line":468},11,[286,470,471],{"class":302},"  ].",[286,473,474],{"class":335},"join",[286,476,339],{"class":302},[286,478,479],{"class":309},"', '",[286,481,482],{"class":302},")\n",[286,484,486],{"class":145,"line":485},12,[286,487,488],{"class":302},"]));\n",[286,490,492,495,497,500,502,505,507,510,513,516,518,521],{"class":145,"line":491},13,[286,493,494],{"class":335},"writeFileSync",[286,496,339],{"class":302},[286,498,499],{"class":309},"'dist\u002Fearly-hints.json'",[286,501,350],{"class":302},[286,503,504],{"class":322},"JSON",[286,506,332],{"class":302},[286,508,509],{"class":335},"stringify",[286,511,512],{"class":302},"(hints, ",[286,514,515],{"class":322},"null",[286,517,350],{"class":302},[286,519,520],{"class":322},"2",[286,522,356],{"class":302},[286,524,526],{"class":145,"line":525},14,[286,527,528],{"class":291},"\u002F\u002F trade-off: this couples the edge config to the build output, so a deploy that\n",[286,530,532],{"class":145,"line":531},15,[286,533,534],{"class":291},"\u002F\u002F updates one without the other breaks the hints. Ship them together as one\n",[286,536,538],{"class":145,"line":537},16,[286,539,540],{"class":291},"\u002F\u002F artifact and version them with the same hash as the assets.\n",[15,542,543],{},"Expected outcome: hints can never point at a filename the current deploy does not have.",[15,545,546],{},[57,547,548],{},"2. Emit the 103 at the edge, before the origin responds.",[277,550,552],{"className":279,"code":551,"language":281,"meta":282,"style":282},"\u002F\u002F Cloudflare Workers-style: hint immediately, then proxy the real request.\nimport hints from '.\u002Fearly-hints.json' with { type: 'json' };\n\nexport default {\n  async fetch(request, env, ctx) {\n    const route = new URL(request.url).pathname;\n    const link = hints[route] ?? hints['\u002F'];\n    \u002F\u002F The platform emits 103 with these headers while the origin still works.\n    const response = await fetch(request);\n    const out = new Response(response.body, response);\n    out.headers.set('Link', link);        \u002F\u002F also honoured by clients ignoring 103\n    return out;\n  }\n};\n\u002F\u002F trade-off: hints[route] is an exact match, so dynamic routes fall back to the\n\u002F\u002F home entry. Normalise the path to a route PATTERN before lookup, or the most\n\u002F\u002F personalised pages — the ones with the longest think time — get generic hints.\n",[30,553,554,559,583,587,598,612,631,655,660,677,694,713,721,726,733,738,743],{"__ignoreMap":282},[286,555,556],{"class":145,"line":288},[286,557,558],{"class":291},"\u002F\u002F Cloudflare Workers-style: hint immediately, then proxy the real request.\n",[286,560,561,563,566,568,571,574,577,580],{"class":145,"line":295},[286,562,299],{"class":298},[286,564,565],{"class":302}," hints ",[286,567,306],{"class":298},[286,569,570],{"class":309}," '.\u002Fearly-hints.json'",[286,572,573],{"class":298}," with",[286,575,576],{"class":302}," { type: ",[286,578,579],{"class":309},"'json'",[286,581,582],{"class":302}," };\n",[286,584,585],{"class":145,"line":316},[286,586,363],{"emptyLinePlaceholder":362},[286,588,589,592,595],{"class":145,"line":359},[286,590,591],{"class":298},"export",[286,593,594],{"class":298}," default",[286,596,597],{"class":397}," {\n",[286,599,600,603,606,609],{"class":145,"line":366},[286,601,602],{"class":298},"  async",[286,604,605],{"class":335}," fetch",[286,607,608],{"class":397},"(request, env, ctx) ",[286,610,611],{"class":302},"{\n",[286,613,614,617,620,622,625,628],{"class":145,"line":414},[286,615,616],{"class":298},"    const",[286,618,619],{"class":322}," route",[286,621,326],{"class":298},[286,623,624],{"class":298}," new",[286,626,627],{"class":335}," URL",[286,629,630],{"class":302},"(request.url).pathname;\n",[286,632,633,635,638,640,643,646,649,652],{"class":145,"line":420},[286,634,616],{"class":298},[286,636,637],{"class":322}," link",[286,639,326],{"class":298},[286,641,642],{"class":302}," hints[route] ",[286,644,645],{"class":298},"??",[286,647,648],{"class":302}," hints[",[286,650,651],{"class":309},"'\u002F'",[286,653,654],{"class":302},"];\n",[286,656,657],{"class":145,"line":426},[286,658,659],{"class":291},"    \u002F\u002F The platform emits 103 with these headers while the origin still works.\n",[286,661,662,664,667,669,672,674],{"class":145,"line":445},[286,663,616],{"class":298},[286,665,666],{"class":322}," response",[286,668,326],{"class":298},[286,670,671],{"class":298}," await",[286,673,605],{"class":335},[286,675,676],{"class":302},"(request);\n",[286,678,679,681,684,686,688,691],{"class":145,"line":462},[286,680,616],{"class":298},[286,682,683],{"class":322}," out",[286,685,326],{"class":298},[286,687,624],{"class":298},[286,689,690],{"class":335}," Response",[286,692,693],{"class":302},"(response.body, response);\n",[286,695,696,699,702,704,707,710],{"class":145,"line":468},[286,697,698],{"class":302},"    out.headers.",[286,700,701],{"class":335},"set",[286,703,339],{"class":302},[286,705,706],{"class":309},"'Link'",[286,708,709],{"class":302},", link);        ",[286,711,712],{"class":291},"\u002F\u002F also honoured by clients ignoring 103\n",[286,714,715,718],{"class":145,"line":485},[286,716,717],{"class":298},"    return",[286,719,720],{"class":302}," out;\n",[286,722,723],{"class":145,"line":491},[286,724,725],{"class":302},"  }\n",[286,727,728,731],{"class":145,"line":525},[286,729,730],{"class":397},"}",[286,732,313],{"class":302},[286,734,735],{"class":145,"line":531},[286,736,737],{"class":291},"\u002F\u002F trade-off: hints[route] is an exact match, so dynamic routes fall back to the\n",[286,739,740],{"class":145,"line":537},[286,741,742],{"class":291},"\u002F\u002F home entry. Normalise the path to a route PATTERN before lookup, or the most\n",[286,744,746],{"class":145,"line":745},17,[286,747,748],{"class":291},"\u002F\u002F personalised pages — the ones with the longest think time — get generic hints.\n",[15,750,751,752,755],{},"Expected outcome: hinted resources begin downloading during origin render, typically starting ",[30,753,754],{},"300–500ms"," earlier on a slow route.",[15,757,758,761],{},[57,759,760],{},"3. Keep the list to three or four entries."," Critical CSS, one font, and one preconnect covers most pages. Anything else competes with the HTML.",[15,763,764,767,768,771,772,775],{},[57,765,766],{},"4. Make the cache key correct."," If your CDN caches the ",[30,769,770],{},"103"," separately from the ",[30,773,774],{},"200",", a deploy can leave them out of sync. Ensure both are keyed together, and verify after a deploy that changes the manifest:",[277,777,781],{"className":778,"code":779,"language":780,"meta":282,"style":282},"language-bash shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","# Confirm the hinted asset paths match the ones the HTML actually references.\ncurl -sI --http2 https:\u002F\u002Fexample.com\u002Faccount | grep -i '^link:'\ncurl -s https:\u002F\u002Fexample.com\u002Faccount | grep -o '\u002Fassets\u002F[a-z0-9.-]*\\.\\(css\\|woff2\\)' | sort -u\n# trade-off: curl reports the Link header on the final response, not the 103.\n# Use a browser trace or a protocol-level tool to confirm the informational\n# response itself is being sent and forwarded.\n","bash",[30,782,783,788,814,841,846,851],{"__ignoreMap":282},[286,784,785],{"class":145,"line":288},[286,786,787],{"class":291},"# Confirm the hinted asset paths match the ones the HTML actually references.\n",[286,789,790,793,796,799,802,805,808,811],{"class":145,"line":295},[286,791,792],{"class":397},"curl",[286,794,795],{"class":322}," -sI",[286,797,798],{"class":322}," --http2",[286,800,801],{"class":309}," https:\u002F\u002Fexample.com\u002Faccount",[286,803,804],{"class":298}," |",[286,806,807],{"class":397}," grep",[286,809,810],{"class":322}," -i",[286,812,813],{"class":309}," '^link:'\n",[286,815,816,818,821,823,825,827,830,833,835,838],{"class":145,"line":316},[286,817,792],{"class":397},[286,819,820],{"class":322}," -s",[286,822,801],{"class":309},[286,824,804],{"class":298},[286,826,807],{"class":397},[286,828,829],{"class":322}," -o",[286,831,832],{"class":309}," '\u002Fassets\u002F[a-z0-9.-]*\\.\\(css\\|woff2\\)'",[286,834,804],{"class":298},[286,836,837],{"class":397}," sort",[286,839,840],{"class":322}," -u\n",[286,842,843],{"class":145,"line":359},[286,844,845],{"class":291},"# trade-off: curl reports the Link header on the final response, not the 103.\n",[286,847,848],{"class":145,"line":366},[286,849,850],{"class":291},"# Use a browser trace or a protocol-level tool to confirm the informational\n",[286,852,853],{"class":145,"line":414},[286,854,855],{"class":291},"# response itself is being sent and forwarded.\n",[46,857,859],{"id":858},"verification","Verification",[15,861,862,863,865],{},"Confirm three things in order. First, that the ",[30,864,770],{}," is actually emitted and received — in Chrome DevTools the hinted requests start before the document response completes, which is visually unmistakable in the waterfall. Second, that the hinted resources are the right ones: every hinted URL should also appear in the HTML. Third, that TTFB-heavy routes improved and cached routes did not regress.",[15,867,868],{},"Track the win where it lands. Early Hints reduces the LCP resource-load-delay phase, not TTFB itself — TTFB is unchanged by design, because the origin still takes as long as it took. A team measuring only TTFB will conclude nothing happened.",[277,870,874],{"className":871,"code":872,"language":873,"meta":282,"style":282},"language-json shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","{ \"ci\": { \"assert\": { \"assertions\": {\n  \"largest-contentful-paint\": [\"error\", { \"maxNumericValue\": 2500 }],\n  \"render-blocking-resources\": [\"error\", { \"maxNumericValue\": 200 }],\n  \"server-response-time\": [\"warn\", { \"maxNumericValue\": 600 }]\n} } } }\n","json",[30,875,876,899,925,944,966],{"__ignoreMap":282},[286,877,878,881,885,888,891,893,896],{"class":145,"line":288},[286,879,880],{"class":302},"{ ",[286,882,884],{"class":883},"sZBmE","\"ci\"",[286,886,887],{"class":302},": { ",[286,889,890],{"class":883},"\"assert\"",[286,892,887],{"class":302},[286,894,895],{"class":883},"\"assertions\"",[286,897,898],{"class":302},": {\n",[286,900,901,904,907,910,913,916,919,922],{"class":145,"line":295},[286,902,903],{"class":883},"  \"largest-contentful-paint\"",[286,905,906],{"class":302},": [",[286,908,909],{"class":309},"\"error\"",[286,911,912],{"class":302},", { ",[286,914,915],{"class":883},"\"maxNumericValue\"",[286,917,918],{"class":302},": ",[286,920,921],{"class":322},"2500",[286,923,924],{"class":302}," }],\n",[286,926,927,930,932,934,936,938,940,942],{"class":145,"line":316},[286,928,929],{"class":883},"  \"render-blocking-resources\"",[286,931,906],{"class":302},[286,933,909],{"class":309},[286,935,912],{"class":302},[286,937,915],{"class":883},[286,939,918],{"class":302},[286,941,774],{"class":322},[286,943,924],{"class":302},[286,945,946,949,951,954,956,958,960,963],{"class":145,"line":359},[286,947,948],{"class":883},"  \"server-response-time\"",[286,950,906],{"class":302},[286,952,953],{"class":309},"\"warn\"",[286,955,912],{"class":302},[286,957,915],{"class":883},[286,959,918],{"class":302},[286,961,962],{"class":322},"600",[286,964,965],{"class":302}," }]\n",[286,967,968],{"class":145,"line":366},[286,969,970],{"class":302},"} } } }\n",[15,972,973,974,977],{},"Keep the ",[30,975,976],{},"server-response-time"," warning in place even after shipping Early Hints. Filling the think time is a mitigation; shortening it is the real fix, and it is easy to stop working on that once the symptom is masked.",[46,979,981],{"id":980},"faq","FAQ",[983,984,987,991],"details",{"className":985},[986],"faq-item",[988,989,990],"summary",{},"Does Early Hints replace preload in the HTML?",[15,992,993,994,996,997,999,1000,1003,1004,1006],{},"No — keep both. Clients that ignore the informational response still need the hints, and the ",[30,995,40],{}," headers on the final ",[30,998,774],{}," (or the ",[30,1001,1002],{},"\u003Clink>"," tags in the head) cover them. The ",[30,1005,770],{}," is an accelerator for clients that support it, not a replacement for the hints themselves. Sending the same list twice costs nothing, because the browser deduplicates by URL.",[983,1008,1010,1013],{"className":1009},[986],[988,1011,1012],{},"Can I send Early Hints for a page served from cache?",[15,1014,1015],{},"You can, and it will rarely help. A cached page's HTML arrives so quickly that the browser discovers the same resources moments later on its own. The overhead is small, so it is not harmful, but the measured gain will be within noise — spend the configuration effort on the uncacheable, personalised routes where think time is real.",[983,1017,1019,1022],{"className":1018},[986],[988,1020,1021],{},"What happens if a hinted file no longer exists?",[15,1023,1024],{},"The browser issues a high-priority request that returns 404, wasting bandwidth and a connection slot at the worst possible moment, and the console logs an unused-preload warning as well. This is why the hint list must be generated from the same manifest that names the assets, and shipped in the same artifact — a manual list will eventually drift, and the failure is silent until someone reads the waterfall.",[983,1026,1028,1031],{"className":1027},[986],[988,1029,1030],{},"How much improvement should I expect?",[15,1032,1033,1034,1036,1037,1040,1041,1043,1044,1047],{},"Roughly the origin think time you manage to fill, bounded by how long the hinted resources take to fetch. On a route with a ",[30,1035,73],{}," TTFB where the critical CSS takes ",[30,1038,1039],{},"250ms"," to download, you recover close to that ",[30,1042,1039],{}," — the download finishes before the HTML even arrives. On a route with a ",[30,1045,1046],{},"120ms"," TTFB there is nothing to recover. Measure TTFB per route first and enable the feature where the dead window is worth filling.",[983,1049,1051,1054],{"className":1050},[986],[988,1052,1053],{},"Can Early Hints hurt anything?",[15,1055,1056],{},"Two ways, both avoidable. Hinting too many resources means the informational response starts downloads that compete with the HTML itself, which arrives moments later. And hinting a stale filename produces a high-priority 404 during the most latency-sensitive part of the load. Keep the list to three or four entries generated from the build manifest, and neither failure mode can occur.",[983,1058,1060,1063],{"className":1059},[986],[988,1061,1062],{},"Do all clients and intermediaries support it?",[15,1064,1065,1066,1068,1069,1071],{},"Support is broad among current browsers and major CDNs, but the whole path must cooperate: an intermediate proxy that does not understand informational responses may drop them silently, and HTTP\u002F1.1 hops cannot carry them at all. Because unsupported clients simply ignore the ",[30,1067,770],{}," and use the repeated ",[30,1070,40],{}," headers on the final response, the failure mode is a lost optimisation rather than a broken page — which makes it safe to enable and worth verifying with a real trace rather than assuming.",[46,1073,1075],{"id":1074},"related","Related",[51,1077,1078,1083,1090],{},[54,1079,1080,1082],{},[19,1081,22],{"href":21}," — the hint vocabulary and the priority rules behind it.",[54,1084,1085,1089],{},[19,1086,1088],{"href":1087},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002F","CDN Edge Caching Configuration"," — cache keys, hit ratio, and where the hints are injected.",[54,1091,1092,1096],{},[19,1093,1095],{"href":1094},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002Fpreload-vs-preconnect-when-to-use-each\u002F","Preload vs preconnect: when to use each"," — choose what belongs in the hint list at all.",[1098,1099,1101],"script",{"type":1100},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"Deploy 103 Early Hints behind a CDN\",\n  \"description\": \"Generate hints from the build manifest, emit an informational 103 response at the edge during origin render, keep the list short, and verify the hinted assets match the deployed HTML.\",\n  \"step\": [\n    { \"@type\": \"HowToStep\", \"position\": 1, \"name\": \"Generate hints from the manifest\", \"text\": \"Produce per-route Link header values from the build output so hashed filenames can never go stale.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002Fusing-103-early-hints-behind-a-cdn\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 2, \"name\": \"Emit 103 at the edge\", \"text\": \"Send the informational response with Link headers while the origin is still rendering, and repeat them on the final response.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002Fusing-103-early-hints-behind-a-cdn\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 3, \"name\": \"Keep the list short\", \"text\": \"Limit hints to critical CSS, one font and one preconnect so they do not compete with the HTML response.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002Fusing-103-early-hints-behind-a-cdn\u002F#step-by-step-resolution\" },\n    { \"@type\": \"HowToStep\", \"position\": 4, \"name\": \"Verify after every deploy\", \"text\": \"Confirm the hinted URLs match the assets the HTML references and that LCP resource load delay improved on slow routes.\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002Fusing-103-early-hints-behind-a-cdn\u002F#verification\" }\n  ]\n}\n",[1098,1103,1104],{"type":1100},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Use 103 Early Hints Behind a CDN\",\n  \"description\": \"Fill origin think time with useful downloads by emitting Link headers in an informational 103 response, without hinting stale assets or wasting effort on cached routes.\",\n  \"datePublished\": \"2026-08-01\",\n  \"dateModified\": \"2026-08-01\",\n  \"author\": { \"@type\": \"Organization\", \"name\": \"frontend-performance.com\" },\n  \"publisher\": { \"@type\": \"Organization\", \"name\": \"frontend-performance.com\" },\n  \"mainEntityOfPage\": { \"@type\": \"WebPage\", \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002Fusing-103-early-hints-behind-a-cdn\u002F\" }\n}\n",[1098,1106,1107],{"type":1100},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    { \"@type\": \"ListItem\", \"position\": 1, \"name\": \"Home\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 2, \"name\": \"Advanced Caching Strategies & CDN Architecture\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 3, \"name\": \"Resource Hints & Early Hints\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 4, \"name\": \"Using 103 Early Hints Behind a CDN\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002Fusing-103-early-hints-behind-a-cdn\u002F\" }\n  ]\n}\n",[1109,1110,1111],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .sZBmE, html code.shiki .sZBmE{--shiki-default:#024C1A;--shiki-dark:#72F088;--shiki-light:#024C1A}",{"title":282,"searchDepth":295,"depth":295,"links":1113},[1114,1115,1116,1117,1118,1119],{"id":48,"depth":295,"text":49},{"id":240,"depth":295,"text":241},{"id":268,"depth":295,"text":269},{"id":858,"depth":295,"text":859},{"id":980,"depth":295,"text":981},{"id":1074,"depth":295,"text":1075},"Fill server think time with useful downloads — and avoid the two traps: hinting a cached deploy's assets, and enabling it where TTFB is already fast.","md",{"slug":1123,"type":1124,"breadcrumb":1125,"datePublished":1133,"dateModified":1133},"using-103-early-hints-behind-a-cdn","article",[1126,1129,1130,1131],{"name":1127,"url":1128},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1132},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002Fusing-103-early-hints-behind-a-cdn\u002F","2026-08-01","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002Fusing-103-early-hints-behind-a-cdn",{"title":5,"description":1136},"Send Link headers during server think time: when 103 Early Hints pays off, how CDNs cache and forward it, and how to keep hinted assets in sync with your build manifest.","advanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002Fusing-103-early-hints-behind-a-cdn\u002Findex","vYKmOFLPEFJ1EZqXjerKtJuLoOA81cXcltSSdWi5HNE",[1140,1144],{"title":1141,"path":1142,"stem":1143,"children":-1},"Preload vs Preconnect: When to Use Each","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002Fpreload-vs-preconnect-when-to-use-each","advanced-caching-strategies-cdn-architecture\u002Fresource-hints-and-early-hints\u002Fpreload-vs-preconnect-when-to-use-each\u002Findex",{"title":1145,"path":1146,"stem":1147,"children":-1},"Service Worker Caching Strategies","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies","advanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002Findex",1785622405979]