[{"data":1,"prerenderedAt":1356},["ShallowReactive",2],{"content:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fpersonalizing-cached-pages-at-the-edge":3,"surroundings:\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fpersonalizing-cached-pages-at-the-edge":1348},{"id":4,"title":5,"body":6,"description":1328,"extension":1329,"meta":1330,"navigation":1341,"path":1342,"seo":1343,"stem":1346,"__hash__":1347},"content\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fpersonalizing-cached-pages-at-the-edge\u002Findex.md","Personalizing Cached Pages at the Edge",{"type":7,"value":8,"toc":1312},"minimark",[9,14,29,37,182,187,228,232,238,246,252,258,388,392,397,407,411,625,628,632,992,995,999,1006,1114,1118,1121,1125,1136,1140,1166,1170,1176,1182,1188,1194,1198,1210,1219,1228,1244,1261,1270,1274,1297,1302,1305,1308],[10,11,13],"h1",{"id":12},"how-to-personalize-cached-pages-at-the-edge","How to Personalize Cached Pages at the Edge",[15,16,17,18,23,24,28],"p",{},"This guide puts ",[19,20,22],"a",{"href":21},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002F","Edge Compute & Dynamic Caching"," into practice within ",[19,25,27],{"href":26},"\u002Fadvanced-caching-strategies-cdn-architecture\u002F","Advanced Caching Strategies & CDN Architecture",". The scenario: a page is identical for everyone except for a few details — a price shown in the visitor's currency, a \"Free delivery to Germany\" banner, a greeting with the user's first name, the experiment arm of a headline test. Rendering all of that at the origin forces the page out of the cache. Varying the cache on cookies fragments it into millions of entries that never warm.",[15,30,31,32,36],{},"Edge personalisation splits the work. The cache stores one shared document (or a handful of variants keyed by low-cardinality values), and an edge function rewrites the small personal parts as the document streams through. Because the rewriting happens on a cached response, close to the user, the origin is not involved and TTFB stays near cache-hit speed — typically well under ",[33,34,35],"code",{},"200ms"," — for every visitor.",[15,38,39],{},[40,41,47,48,47,55,47,59,47,62,47,80,47,88,47,94,47,102,47,109,47,114,47,118,47,124,47,128,47,131,47,134,47,138,47,141,47,144,47,148,47,151,47,154,47,158,47,161,47,170,47,174,47,178,47],"svg",{"viewBox":42,"width":43,"role":44,"ariaLabel":45,"style":46},"0 0 760 149","100%","img","Steps from a request through variant key derivation, cache lookup and streaming HTML rewriting to the personalised response.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[49,50],"rect",{"className":51,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],"svg-canvas","0","#ffffff",[56,57,58],"title",{},"Edge personalisation pipeline",[60,61,45],"desc",{},[63,64,65],"defs",{},[66,67,74],"marker",{"id":68,"viewBox":69,"refX":70,"refY":71,"markerWidth":72,"markerHeight":72,"orient":73},"fac2825292","0 0 10 10","9","5","7","auto-start-reverse",[75,76],"path",{"d":77,"fill":78,"style":79},"M0 0 L10 5 L0 10 z","currentColor","fill-opacity:0.7",[49,81],{"x":82,"y":82,"width":83,"height":84,"rx":85,"fill":86,"stroke":78,"style":87},"1","758","147","10","none","stroke-opacity:0.18",[89,90,58],"text",{"x":91,"y":92,"fill":78,"style":93},"28.0","34.0","font-size:16px;font-weight:700",[49,95],{"x":91,"y":96,"width":97,"height":98,"rx":99,"fill":100,"stroke":100,"style":101},"56.0","108.8","71.0","6","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[89,103,108],{"x":104,"y":105,"fill":78,"style":106,"textAnchor":107},"82.4","78.0","font-size:13px;font-weight:700","middle","Request",[89,110,113],{"x":104,"y":111,"fill":78,"style":112,"textAnchor":107},"96.0","font-size:12px","cookies, geo,",[89,115,117],{"x":104,"y":116,"fill":78,"style":112,"textAnchor":107},"112.0","headers",[49,119],{"x":120,"y":96,"width":97,"height":98,"rx":99,"fill":121,"stroke":122,"style":123},"176.8","#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[89,125,127],{"x":126,"y":105,"fill":78,"style":106,"textAnchor":107},"231.2","Derive key",[89,129,130],{"x":126,"y":111,"fill":78,"style":112,"textAnchor":107},"currency + arm",[49,132],{"x":133,"y":96,"width":97,"height":98,"rx":99,"fill":100,"stroke":100,"style":101},"325.6",[89,135,137],{"x":136,"y":105,"fill":78,"style":106,"textAnchor":107},"380.0","Cache lookup",[89,139,140],{"x":136,"y":111,"fill":78,"style":112,"textAnchor":107},"shared variant",[49,142],{"x":143,"y":96,"width":97,"height":98,"rx":99,"fill":121,"stroke":122,"style":123},"474.4",[89,145,147],{"x":146,"y":105,"fill":78,"style":106,"textAnchor":107},"528.8","Rewrite stream",[89,149,150],{"x":146,"y":111,"fill":78,"style":112,"textAnchor":107},"greeting, banner",[49,152],{"x":153,"y":96,"width":97,"height":98,"rx":99,"fill":100,"stroke":100,"style":101},"623.2",[89,155,157],{"x":156,"y":105,"fill":78,"style":106,"textAnchor":107},"677.6","Response",[89,159,160],{"x":156,"y":111,"fill":78,"style":112,"textAnchor":107},"TTFB ~80ms",[162,163],"line",{"x1":164,"y1":165,"x2":166,"y2":165,"stroke":78,"strokeWidth":167,"style":168,"markerEnd":169},"136.8","91.5","174.8","1.5","stroke-opacity:0.6","url(#fac2825292)",[162,171],{"x1":172,"y1":165,"x2":173,"y2":165,"stroke":78,"strokeWidth":167,"style":168,"markerEnd":169},"285.6","323.6",[162,175],{"x1":176,"y1":165,"x2":177,"y2":165,"stroke":78,"strokeWidth":167,"style":168,"markerEnd":169},"434.4","472.4",[162,179],{"x1":180,"y1":165,"x2":181,"y2":165,"stroke":78,"strokeWidth":167,"style":168,"markerEnd":169},"583.2","621.2",[183,184,186],"h2",{"id":185},"rapid-diagnosis","Rapid Diagnosis",[188,189,190,198,204,222],"ul",{},[191,192,193,197],"li",{},[194,195,196],"strong",{},"Find what personalises the page today."," Search templates for reads of cookies, geo headers, user objects or experiment assignments during render.",[191,199,200,203],{},[194,201,202],{},"Count distinct values per dimension."," Currency (10), country (60), experiment arm (2–3), user (millions). Low-cardinality dimensions can be cache keys; user-level ones cannot.",[191,205,206,209,210,213,214,217,218,221],{},[194,207,208],{},"Check current cache behaviour."," HTML responses with ",[33,211,212],{},"Cache-Control: private",", ",[33,215,216],{},"Vary: Cookie",", or ",[33,219,220],{},"set-cookie"," on every response are not being cached.",[191,223,224,227],{},[194,225,226],{},"Measure TTFB split by personalisation."," Compare anonymous, logged-in and experiment-exposed traffic in RUM.",[183,229,231],{"id":230},"root-cause-analysis","Root Cause Analysis",[15,233,234,237],{},[194,235,236],{},"1. Personalisation during origin render."," Any per-request value used while rendering makes the whole document uncacheable, even if it affects ten bytes.",[15,239,240,47,243,245],{},[194,241,242],{},"2. Varying on high-cardinality inputs.",[33,244,216],{}," or keying on the full cookie header produces a separate cache entry per visitor.",[15,247,248,251],{},[194,249,250],{},"3. Set-Cookie on cacheable responses."," Many CDNs refuse to cache responses that set cookies, so a session-refresh cookie on every page disables caching silently.",[15,253,254,257],{},[194,255,256],{},"4. Client-side rewriting as a workaround."," Swapping content after load with JavaScript causes flicker and layout shift, and can produce a later LCP if the swapped element is large.",[15,259,260],{},[40,261,47,264,47,267,47,270,47,272,47,275,47,277,47,282,47,289,47,293,47,297,47,300,47,304,47,308,47,312,47,314,47,317,47,319,47,322,47,325,47,329,47,331,47,334,47,336,47,338,47,341,47,345,47,347,47,350,47,352,47,355,47,358,47,362,47,364,47,366,47,368,47,371,47,374,47,378,47,380,47,383,47,385,47],{"viewBox":262,"width":43,"role":44,"ariaLabel":263,"style":46},"0 0 760 258","Common personalisation dimensions by cardinality and the recommended way to serve each from a cached page.",[49,265],{"className":266,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,268,269],{},"How to handle each personalisation dimension",[60,271,263],{},[49,273],{"x":82,"y":82,"width":83,"height":274,"rx":85,"fill":86,"stroke":78,"style":87},"256",[89,276,269],{"x":91,"y":92,"fill":78,"style":93},[49,278],{"x":91,"y":96,"width":279,"height":280,"rx":53,"fill":78,"stroke":78,"style":281},"134.7","30.0","fill-opacity:0.06;stroke-opacity:0.4",[89,283,288],{"x":284,"y":285,"fill":78,"style":286,"textAnchor":287},"38.0","75.5","font-size:12.5px;font-weight:700","start","Dimension",[49,290],{"x":291,"y":96,"width":292,"height":280,"rx":53,"fill":78,"stroke":78,"style":281},"162.7","284.6",[89,294,296],{"x":295,"y":285,"fill":78,"style":286,"textAnchor":107},"305.1","Cardinality",[49,298],{"x":299,"y":96,"width":292,"height":280,"rx":53,"fill":78,"stroke":78,"style":281},"447.4",[89,301,303],{"x":302,"y":285,"fill":78,"style":286,"textAnchor":107},"589.7","Approach",[49,305],{"x":91,"y":306,"width":279,"height":280,"rx":53,"fill":86,"stroke":78,"style":307},"86.0","stroke-opacity:0.35",[89,309,311],{"x":284,"y":310,"fill":78,"style":286,"textAnchor":287},"105.5","Currency \u002F country",[49,313],{"x":291,"y":306,"width":292,"height":280,"rx":53,"fill":78,"stroke":78,"style":281},[89,315,316],{"x":295,"y":310,"fill":78,"style":112,"textAnchor":107},"tens",[49,318],{"x":299,"y":306,"width":292,"height":280,"rx":53,"fill":100,"stroke":100,"style":101},[89,320,321],{"x":302,"y":310,"fill":78,"style":112,"textAnchor":107},"cache key at the edge",[49,323],{"x":91,"y":324,"width":279,"height":280,"rx":53,"fill":86,"stroke":78,"style":307},"116.0",[89,326,328],{"x":284,"y":327,"fill":78,"style":286,"textAnchor":287},"135.5","Experiment arm",[49,330],{"x":291,"y":324,"width":292,"height":280,"rx":53,"fill":100,"stroke":100,"style":101},[89,332,333],{"x":295,"y":327,"fill":78,"style":112,"textAnchor":107},"2-3",[49,335],{"x":299,"y":324,"width":292,"height":280,"rx":53,"fill":100,"stroke":100,"style":101},[89,337,321],{"x":302,"y":327,"fill":78,"style":112,"textAnchor":107},[49,339],{"x":91,"y":340,"width":279,"height":280,"rx":53,"fill":86,"stroke":78,"style":307},"146.0",[89,342,344],{"x":284,"y":343,"fill":78,"style":286,"textAnchor":287},"165.5","First-name greeting",[49,346],{"x":291,"y":340,"width":292,"height":280,"rx":53,"fill":121,"stroke":122,"style":123},[89,348,349],{"x":295,"y":343,"fill":78,"style":112,"textAnchor":107},"per user",[49,351],{"x":299,"y":340,"width":292,"height":280,"rx":53,"fill":100,"stroke":100,"style":101},[89,353,354],{"x":302,"y":343,"fill":78,"style":112,"textAnchor":107},"rewrite in stream from cookie or token",[49,356],{"x":91,"y":357,"width":279,"height":280,"rx":53,"fill":86,"stroke":78,"style":307},"176.0",[89,359,361],{"x":284,"y":360,"fill":78,"style":286,"textAnchor":287},"195.5","Cart count",[49,363],{"x":291,"y":357,"width":292,"height":280,"rx":53,"fill":121,"stroke":122,"style":123},[89,365,349],{"x":295,"y":360,"fill":78,"style":112,"textAnchor":107},[49,367],{"x":299,"y":357,"width":292,"height":280,"rx":53,"fill":100,"stroke":100,"style":101},[89,369,370],{"x":302,"y":360,"fill":78,"style":112,"textAnchor":107},"fragment or client fetch",[49,372],{"x":91,"y":373,"width":279,"height":280,"rx":53,"fill":86,"stroke":78,"style":307},"206.0",[89,375,377],{"x":284,"y":376,"fill":78,"style":286,"textAnchor":287},"225.5","Recommendations",[49,379],{"x":291,"y":373,"width":292,"height":280,"rx":53,"fill":121,"stroke":122,"style":123},[89,381,382],{"x":295,"y":376,"fill":78,"style":112,"textAnchor":107},"per user, slow",[49,384],{"x":299,"y":373,"width":292,"height":280,"rx":53,"fill":78,"stroke":78,"style":281},[89,386,387],{"x":302,"y":376,"fill":78,"style":112,"textAnchor":107},"client fetch after paint",[183,389,391],{"id":390},"step-by-step-resolution","Step-by-Step Resolution",[393,394,396],"h3",{"id":395},"_1-render-the-shared-document-with-neutral-placeholders","1. Render the shared document with neutral placeholders",[15,398,399,400,213,403,406],{},"Change templates so the origin renders personal regions as stable placeholders with fixed dimensions: ",[33,401,402],{},"\u003Cspan data-slot=\"greeting\">Welcome\u003C\u002Fspan>",[33,404,405],{},"\u003Cspan data-slot=\"cart-count\">0\u003C\u002Fspan>",". The origin response no longer depends on who asked, so it can be cached publicly.",[393,408,410],{"id":409},"_2-derive-a-variant-key-at-the-edge","2. Derive a variant key at the edge",[412,413,418],"pre",{"className":414,"code":415,"language":416,"meta":417,"style":417},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F Cloudflare Worker: currency and experiment arm as the only variant dimensions.\nfunction variantKey(request) {\n  const country = request.cf?.country ?? 'US';\n  const currency = { DE: 'EUR', FR: 'EUR', GB: 'GBP', US: 'USD' }[country] ?? 'USD';\n  const arm = \u002Fexp_hero=(a|b)\u002F.exec(request.headers.get('cookie') ?? '')?.[1] ?? 'a';\n  return `${currency}:${arm}`;\n}\n\u002F\u002F trade-off: every value added to the key multiplies cache entries per URL.\n\u002F\u002F Keep the key to dimensions that change the first paint; adjust the rest\n\u002F\u002F client-side or in the stream.\n","javascript","",[33,419,420,428,450,476,519,578,601,607,613,619],{"__ignoreMap":417},[421,422,424],"span",{"class":162,"line":423},1,[421,425,427],{"class":426},"sjfSM","\u002F\u002F Cloudflare Worker: currency and experiment arm as the only variant dimensions.\n",[421,429,431,435,439,443,447],{"class":162,"line":430},2,[421,432,434],{"class":433},"sPARh","function",[421,436,438],{"class":437},"smZ65"," variantKey",[421,440,442],{"class":441},"saISM","(",[421,444,446],{"class":445},"sQw3B","request",[421,448,449],{"class":441},") {\n",[421,451,453,456,460,463,466,469,473],{"class":162,"line":452},3,[421,454,455],{"class":433},"  const",[421,457,459],{"class":458},"sPXB4"," country",[421,461,462],{"class":433}," =",[421,464,465],{"class":441}," request.cf?.country ",[421,467,468],{"class":433},"??",[421,470,472],{"class":471},"sZ8jY"," 'US'",[421,474,475],{"class":441},";\n",[421,477,479,481,484,486,489,492,495,497,500,503,506,509,512,514,517],{"class":162,"line":478},4,[421,480,455],{"class":433},[421,482,483],{"class":458}," currency",[421,485,462],{"class":433},[421,487,488],{"class":441}," { DE: ",[421,490,491],{"class":471},"'EUR'",[421,493,494],{"class":441},", FR: ",[421,496,491],{"class":471},[421,498,499],{"class":441},", GB: ",[421,501,502],{"class":471},"'GBP'",[421,504,505],{"class":441},", US: ",[421,507,508],{"class":471},"'USD'",[421,510,511],{"class":441}," }[country] ",[421,513,468],{"class":433},[421,515,516],{"class":471}," 'USD'",[421,518,475],{"class":441},[421,520,522,524,527,529,532,535,538,541,544,547,550,552,555,558,560,563,566,568,571,573,576],{"class":162,"line":521},5,[421,523,455],{"class":433},[421,525,526],{"class":458}," arm",[421,528,462],{"class":433},[421,530,531],{"class":471}," \u002Fexp_hero=(a",[421,533,534],{"class":433},"|",[421,536,537],{"class":471},"b)\u002F",[421,539,540],{"class":441},".",[421,542,543],{"class":437},"exec",[421,545,546],{"class":441},"(request.headers.",[421,548,549],{"class":437},"get",[421,551,442],{"class":441},[421,553,554],{"class":471},"'cookie'",[421,556,557],{"class":441},") ",[421,559,468],{"class":433},[421,561,562],{"class":471}," ''",[421,564,565],{"class":441},")?.[",[421,567,82],{"class":458},[421,569,570],{"class":441},"] ",[421,572,468],{"class":433},[421,574,575],{"class":471}," 'a'",[421,577,475],{"class":441},[421,579,581,584,587,590,593,596,599],{"class":162,"line":580},6,[421,582,583],{"class":433},"  return",[421,585,586],{"class":471}," `${",[421,588,589],{"class":441},"currency",[421,591,592],{"class":471},"}:${",[421,594,595],{"class":441},"arm",[421,597,598],{"class":471},"}`",[421,600,475],{"class":441},[421,602,604],{"class":162,"line":603},7,[421,605,606],{"class":441},"}\n",[421,608,610],{"class":162,"line":609},8,[421,611,612],{"class":426},"\u002F\u002F trade-off: every value added to the key multiplies cache entries per URL.\n",[421,614,616],{"class":162,"line":615},9,[421,617,618],{"class":426},"\u002F\u002F Keep the key to dimensions that change the first paint; adjust the rest\n",[421,620,622],{"class":162,"line":621},10,[421,623,624],{"class":426},"\u002F\u002F client-side or in the stream.\n",[15,626,627],{},"Expected outcome: a handful of cache entries per URL instead of one per visitor.",[393,629,631],{"id":630},"_3-rewrite-personal-slots-in-the-stream","3. Rewrite personal slots in the stream",[412,633,635],{"className":414,"code":634,"language":416,"meta":417,"style":417},"export default {\n  async fetch(request, env, ctx) {\n    const url = new URL(request.url);\n    const cacheKey = new Request(`${url.origin}${url.pathname}?__v=${variantKey(request)}`);\n    let response = await caches.default.match(cacheKey);\n    if (!response) {\n      response = await fetch(request, { headers: { 'x-variant': variantKey(request) } });\n      ctx.waitUntil(caches.default.put(cacheKey, response.clone()));\n    }\n    const name = decodeURIComponent(\u002Ffirst_name=([^;]+)\u002F.exec(request.headers.get('cookie') ?? '')?.[1] ?? '');\n    return new HTMLRewriter()\n      .on('[data-slot=\"greeting\"]', { element(el) { if (name) el.setInnerContent(`Welcome back, ${name}`); } })\n      .transform(response);\n  },\n};\n\u002F\u002F trade-off: setInnerContent escapes text by default, which is correct for\n\u002F\u002F names. Never insert raw HTML from cookies — that is an injection vector.\n",[33,636,637,648,662,681,736,759,773,798,821,826,888,902,952,963,972,980,986],{"__ignoreMap":417},[421,638,639,642,645],{"class":162,"line":423},[421,640,641],{"class":433},"export",[421,643,644],{"class":433}," default",[421,646,647],{"class":445}," {\n",[421,649,650,653,656,659],{"class":162,"line":430},[421,651,652],{"class":433},"  async",[421,654,655],{"class":437}," fetch",[421,657,658],{"class":445},"(request, env, ctx) ",[421,660,661],{"class":441},"{\n",[421,663,664,667,670,672,675,678],{"class":162,"line":452},[421,665,666],{"class":433},"    const",[421,668,669],{"class":458}," url",[421,671,462],{"class":433},[421,673,674],{"class":433}," new",[421,676,677],{"class":437}," URL",[421,679,680],{"class":441},"(request.url);\n",[421,682,683,685,688,690,692,695,697,700,703,705,708,711,713,715,718,721,724,726,728,731,733],{"class":162,"line":478},[421,684,666],{"class":433},[421,686,687],{"class":458}," cacheKey",[421,689,462],{"class":433},[421,691,674],{"class":433},[421,693,694],{"class":437}," Request",[421,696,442],{"class":441},[421,698,699],{"class":471},"`${",[421,701,702],{"class":441},"url",[421,704,540],{"class":471},[421,706,707],{"class":441},"origin",[421,709,710],{"class":471},"}${",[421,712,702],{"class":441},[421,714,540],{"class":471},[421,716,717],{"class":441},"pathname",[421,719,720],{"class":471},"}?__v=${",[421,722,723],{"class":437},"variantKey",[421,725,442],{"class":471},[421,727,446],{"class":441},[421,729,730],{"class":471},")",[421,732,598],{"class":471},[421,734,735],{"class":441},");\n",[421,737,738,741,744,747,750,753,756],{"class":162,"line":521},[421,739,740],{"class":433},"    let",[421,742,743],{"class":441}," response ",[421,745,746],{"class":433},"=",[421,748,749],{"class":433}," await",[421,751,752],{"class":441}," caches.default.",[421,754,755],{"class":437},"match",[421,757,758],{"class":441},"(cacheKey);\n",[421,760,761,764,767,770],{"class":162,"line":580},[421,762,763],{"class":433},"    if",[421,765,766],{"class":441}," (",[421,768,769],{"class":433},"!",[421,771,772],{"class":441},"response) {\n",[421,774,775,778,780,782,784,787,790,793,795],{"class":162,"line":603},[421,776,777],{"class":441},"      response ",[421,779,746],{"class":433},[421,781,749],{"class":433},[421,783,655],{"class":437},[421,785,786],{"class":441},"(request, { headers: { ",[421,788,789],{"class":471},"'x-variant'",[421,791,792],{"class":441},": ",[421,794,723],{"class":437},[421,796,797],{"class":441},"(request) } });\n",[421,799,800,803,806,809,812,815,818],{"class":162,"line":609},[421,801,802],{"class":441},"      ctx.",[421,804,805],{"class":437},"waitUntil",[421,807,808],{"class":441},"(caches.default.",[421,810,811],{"class":437},"put",[421,813,814],{"class":441},"(cacheKey, response.",[421,816,817],{"class":437},"clone",[421,819,820],{"class":441},"()));\n",[421,822,823],{"class":162,"line":615},[421,824,825],{"class":441},"    }\n",[421,827,828,830,833,835,838,840,843,846,849,852,855,858,860,862,864,866,868,870,872,874,876,878,880,882,884,886],{"class":162,"line":621},[421,829,666],{"class":433},[421,831,832],{"class":458}," name",[421,834,462],{"class":433},[421,836,837],{"class":437}," decodeURIComponent",[421,839,442],{"class":441},[421,841,842],{"class":471},"\u002Ffirst_name=(",[421,844,845],{"class":458},"[",[421,847,848],{"class":433},"^",[421,850,851],{"class":458},";]",[421,853,854],{"class":433},"+",[421,856,857],{"class":471},")\u002F",[421,859,540],{"class":441},[421,861,543],{"class":437},[421,863,546],{"class":441},[421,865,549],{"class":437},[421,867,442],{"class":441},[421,869,554],{"class":471},[421,871,557],{"class":441},[421,873,468],{"class":433},[421,875,562],{"class":471},[421,877,565],{"class":441},[421,879,82],{"class":458},[421,881,570],{"class":441},[421,883,468],{"class":433},[421,885,562],{"class":471},[421,887,735],{"class":441},[421,889,891,894,896,899],{"class":162,"line":890},11,[421,892,893],{"class":433},"    return",[421,895,674],{"class":433},[421,897,898],{"class":437}," HTMLRewriter",[421,900,901],{"class":441},"()\n",[421,903,905,908,911,913,916,919,922,924,927,930,933,936,939,941,944,947,949],{"class":162,"line":904},12,[421,906,907],{"class":441},"      .",[421,909,910],{"class":437},"on",[421,912,442],{"class":441},[421,914,915],{"class":471},"'[data-slot=\"greeting\"]'",[421,917,918],{"class":441},", { ",[421,920,921],{"class":437},"element",[421,923,442],{"class":441},[421,925,926],{"class":445},"el",[421,928,929],{"class":441},") { ",[421,931,932],{"class":433},"if",[421,934,935],{"class":441}," (name) el.",[421,937,938],{"class":437},"setInnerContent",[421,940,442],{"class":441},[421,942,943],{"class":471},"`Welcome back, ${",[421,945,946],{"class":441},"name",[421,948,598],{"class":471},[421,950,951],{"class":441},"); } })\n",[421,953,955,957,960],{"class":162,"line":954},13,[421,956,907],{"class":441},[421,958,959],{"class":437},"transform",[421,961,962],{"class":441},"(response);\n",[421,964,966,969],{"class":162,"line":965},14,[421,967,968],{"class":441},"  }",[421,970,971],{"class":445},",\n",[421,973,975,978],{"class":162,"line":974},15,[421,976,977],{"class":445},"}",[421,979,475],{"class":441},[421,981,983],{"class":162,"line":982},16,[421,984,985],{"class":426},"\u002F\u002F trade-off: setInnerContent escapes text by default, which is correct for\n",[421,987,989],{"class":162,"line":988},17,[421,990,991],{"class":426},"\u002F\u002F names. Never insert raw HTML from cookies — that is an injection vector.\n",[15,993,994],{},"Expected outcome: a cached response, personalised in transit, with TTFB close to a plain cache hit.",[393,996,998],{"id":997},"_4-strip-cookies-from-cacheable-responses","4. Strip cookies from cacheable responses",[15,1000,1001,1002,1005],{},"Make sure the origin does not send ",[33,1003,1004],{},"Set-Cookie"," on cacheable HTML. Refresh session cookies on API calls or a dedicated endpoint instead.",[15,1007,1008],{},[40,1009,47,1012,47,1015,47,1018,47,1020,47,1023,47,1025,47,1033,47,1036,47,1042,47,1048,47,1052,47,1056,47,1060,47,1062,47,1065,47,1068,47,1073,47,1079,47,1083,47,1085,47,1089,47,1093,47,1097,47,1101,47,1105,47,1109,47],{"viewBox":1010,"width":43,"role":44,"ariaLabel":1011,"style":46},"0 0 760 200","Two timelines comparing time to first byte for a logged-in visitor when the page is rendered at the origin and when it is served from cache and rewritten at the edge.",[49,1013],{"className":1014,"x":53,"y":53,"width":43,"height":43,"fill":54},[52],[56,1016,1017],{},"TTFB for a logged-in visitor before and after edge personalisation",[60,1019,1011],{},[49,1021],{"x":82,"y":82,"width":83,"height":1022,"rx":85,"fill":86,"stroke":78,"style":87},"198",[89,1024,1017],{"x":91,"y":92,"fill":78,"style":93},[162,1026],{"x1":1027,"y1":1028,"x2":1027,"y2":1029,"stroke":122,"strokeWidth":167,"strokeDashArray":1030,"style":1032},"269.3","72.0","156.0",[1031,1031],"4","stroke-opacity:0.9",[89,1034,1035],{"x":91,"y":111,"fill":78,"style":286},"Origin render",[49,1037],{"x":1038,"y":105,"width":1039,"height":1040,"rx":1041,"fill":78,"stroke":78,"style":281},"117.9","44.2","26.0","3",[89,1043,1047],{"x":1044,"y":1045,"fill":78,"style":1046,"textAnchor":107},"139.9","95.5","font-size:11.5px","edge",[49,1049],{"x":1050,"y":105,"width":1051,"height":1040,"rx":1041,"fill":121,"stroke":122,"style":123},"163.5","485.6",[89,1053,1055],{"x":1054,"y":1045,"fill":78,"style":1046,"textAnchor":107},"406.3","origin render",[89,1057,1059],{"x":91,"y":1058,"fill":78,"style":286},"134.0","Edge rewrite",[49,1061],{"x":1038,"y":324,"width":1039,"height":1040,"rx":1041,"fill":78,"stroke":78,"style":281},[89,1063,1047],{"x":1044,"y":1064,"fill":78,"style":1046,"textAnchor":107},"133.5",[49,1066],{"x":1050,"y":324,"width":1067,"height":1040,"rx":1041,"fill":100,"stroke":100,"style":101},"9.9",[162,1069],{"x1":1070,"y1":1029,"x2":1071,"y2":1029,"stroke":78,"style":1072},"117.1","726.0","stroke-opacity:0.4",[89,1074,1078],{"x":1075,"y":1076,"fill":1077,"style":1046,"textAnchor":107},"127.8","173.0","#51617a","0ms",[89,1080,1082],{"x":1081,"y":1076,"fill":1077,"style":1046,"textAnchor":107},"193.2","100ms",[89,1084,35],{"x":1027,"y":1076,"fill":1077,"style":1046,"textAnchor":107},[89,1086,1088],{"x":1087,"y":1076,"fill":1077,"style":1046,"textAnchor":107},"345.4","300ms",[89,1090,1092],{"x":1091,"y":1076,"fill":1077,"style":1046,"textAnchor":107},"421.6","400ms",[89,1094,1096],{"x":1095,"y":1076,"fill":1077,"style":1046,"textAnchor":107},"497.7","500ms",[89,1098,1100],{"x":1099,"y":1076,"fill":1077,"style":1046,"textAnchor":107},"573.8","600ms",[89,1102,1104],{"x":1103,"y":1076,"fill":1077,"style":1046,"textAnchor":107},"649.9","700ms",[89,1106,1108],{"x":1107,"y":1076,"fill":1077,"style":1046,"textAnchor":107},"715.3","800ms",[89,1110,35],{"x":1027,"y":1111,"fill":1112,"style":1113,"textAnchor":107},"68.0","#8a6608","font-size:12px;font-weight:700",[183,1115,1117],{"id":1116},"verification","Verification",[15,1119,1120],{},"Request the page as several test users and from several countries (via a VPN or the platform's preview tools) and confirm each response is correctly personalised and that cache status headers show hits after the first request per variant. Run a cross-user isolation test. In RUM, TTFB p75 for logged-in and experiment traffic should converge on anonymous TTFB.",[183,1122,1124],{"id":1123},"worked-example-currency-and-greeting-on-a-fashion-store","Worked Example: Currency and Greeting on a Fashion Store",[15,1126,1127,1128,1131,1132,1135],{},"A fashion retailer rendered prices in the visitor's currency and a \"Hi, Sam\" greeting at the origin, so no product page was ever cached. They changed templates to render prices for a currency passed in a request header and the greeting as a placeholder, keyed the edge cache on currency only (seven values), and filled the greeting from a ",[33,1129,1130],{},"first_name"," cookie with ",[33,1133,1134],{},"HTMLRewriter",". The HTML hit ratio for product pages rose from 0% to 91%, TTFB p75 fell from 690ms to 90ms, and LCP p75 improved by about 550ms. Because the greeting placeholder had the same dimensions as the greeting, there was no layout shift.",[183,1137,1139],{"id":1138},"common-mistakes","Common Mistakes",[188,1141,1142,1148,1154,1160],{},[191,1143,1144,1147],{},[194,1145,1146],{},"Keying on the raw cookie header."," It contains analytics IDs that differ per visitor; derive a clean key instead.",[191,1149,1150,1153],{},[194,1151,1152],{},"Personalising the LCP element client-side."," Swapping a hero image after load creates a later LCP; personalise it at the edge or not at all.",[191,1155,1156,1159],{},[194,1157,1158],{},"Inserting unescaped user data."," Cookie values are attacker-controlled; always escape.",[191,1161,1162,1165],{},[194,1163,1164],{},"Forgetting to vary purges."," When a product changes, purge every variant — tag them all with the product's tag.",[183,1167,1169],{"id":1168},"edge-cases","Edge Cases",[15,1171,1172,1175],{},[194,1173,1174],{},"Bots and crawlers."," Search engines should see the default variant; ensure the variant key falls back to a sensible default without cookies or geo data.",[15,1177,1178,1181],{},[194,1179,1180],{},"Users switching currency."," A currency switcher must set a cookie the edge reads; avoid query-string currencies that fragment the cache further.",[15,1183,1184,1187],{},[194,1185,1186],{},"Streaming and late slots."," If a personal slot appears late in a large document, the rewriter still streams earlier chunks immediately; TTFB is unaffected.",[15,1189,1190,1193],{},[194,1191,1192],{},"Platform differences."," Fastly (VCL or Compute), Akamai and Vercel offer equivalent mechanisms with different APIs; the pattern — small key, cached document, streaming rewrite — is the same.",[183,1195,1197],{"id":1196},"faq","FAQ",[1199,1200,1203,1207],"details",{"className":1201},[1202],"faq-item",[1204,1205,1206],"summary",{},"Is it safe to read a first name from a cookie?",[15,1208,1209],{},"It is safe if you escape it and treat it as display-only. Do not derive authorisation or anything sensitive from client-readable cookies; for that, use server-validated tokens on API calls.",[1199,1211,1213,1216],{"className":1212},[1202],[1204,1214,1215],{},"Does HTML rewriting add noticeable latency?",[15,1217,1218],{},"Streaming rewriters operate on chunks as they pass and add a few milliseconds at most. Buffering the entire response to run string replacements is what adds latency — avoid it.",[1199,1220,1222,1225],{"className":1221},[1202],[1204,1223,1224],{},"What if personalisation needs data from a database?",[15,1226,1227],{},"Then it belongs in a fragment fetched in parallel (or on the client), not in the shell. Edge functions should not block the cached shell on slow, distant data stores.",[1199,1229,1231,1234],{"className":1230},[1202],[1204,1232,1233],{},"How does this interact with Vary headers?",[15,1235,1236,1237,1240,1241,1243],{},"You stop using ",[33,1238,1239],{},"Vary"," for personalisation: the edge computes the key explicitly. Remove ",[33,1242,216],{}," from origin responses, or the CDN may still fragment the cache.",[1199,1245,1247,1250],{"className":1246},[1202],[1204,1248,1249],{},"Can I personalise above-the-fold images this way?",[15,1251,1252,1253,1256,1257,1260],{},"Yes, by rewriting the ",[33,1254,1255],{},"src","\u002F",[33,1258,1259],{},"srcset"," attributes of the hero in the stream based on a variant key. Because it happens before the HTML reaches the browser, the preload scanner sees the correct image and LCP is unaffected.",[1199,1262,1264,1267],{"className":1263},[1202],[1204,1265,1266],{},"How many variants per URL is too many?",[15,1268,1269],{},"As a rule of thumb, keep it under a few dozen. Each variant must be requested at least once per location before it is cached, and rarely requested variants expire before they are reused. If your key multiplies several dimensions, check per-variant hit ratios; variants with low hit ratios should be collapsed or handled client-side.",[183,1271,1273],{"id":1272},"related","Related",[188,1275,1276,1283,1290],{},[191,1277,1278,1282],{},[19,1279,1281],{"href":1280},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fcaching-html-for-logged-in-users-safely\u002F","Caching HTML for logged-in users safely"," — the correctness rules behind this pattern.",[191,1284,1285,1289],{},[19,1286,1288],{"href":1287},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fcdn-edge-caching-configuration\u002Fnormalizing-cache-keys-to-raise-hit-rate\u002F","Normalizing cache keys to raise hit rate"," — designing the variant key.",[191,1291,1292,1296],{},[19,1293,1295],{"href":1294},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fusing-the-cache-api-in-cloudflare-workers\u002F","Using the Cache API in Cloudflare Workers"," — the cache calls used above.",[1298,1299,1301],"script",{"type":1300},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Personalize Cached Pages at the Edge\",\n  \"description\": \"Use streaming HTML rewriting and low-cardinality variant keys to personalise cached pages at the edge without fragmenting the cache.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Render the shared document with neutral placeholders\",\n      \"text\": \"Change templates so the origin renders personal regions as stable placeholders with fixed dimensions: Welcome, 0.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Derive a variant key at the edge\",\n      \"text\": \"Expected outcome: a handful of cache entries per URL instead of one per visitor.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Rewrite personal slots in the stream\",\n      \"text\": \"Expected outcome: a cached response, personalised in transit, with TTFB close to a plain cache hit.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Strip cookies from cacheable responses\",\n      \"text\": \"Make sure the origin does not send Set-Cookie on cacheable HTML.\"\n    }\n  ]\n}\n",[1298,1303,1304],{"type":1300},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Personalize Cached Pages at the Edge\",\n  \"description\": \"Use streaming HTML rewriting and low-cardinality variant keys to personalise cached pages at the edge without fragmenting the cache.\",\n  \"datePublished\": \"2026-10-06\",\n  \"dateModified\": \"2026-10-06\",\n  \"author\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"publisher\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"mainEntityOfPage\": {\n    \"@type\": \"WebPage\",\n    \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fpersonalizing-cached-pages-at-the-edge\u002F\"\n  }\n}\n",[1298,1306,1307],{"type":1300},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 1,\n      \"name\": \"Home\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 2,\n      \"name\": \"Advanced Caching Strategies & CDN Architecture\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"Edge Compute & Dynamic Caching\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Personalizing Cached Pages at the Edge\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fpersonalizing-cached-pages-at-the-edge\u002F\"\n    }\n  ]\n}\n",[1309,1310,1311],"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 .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}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);}",{"title":417,"searchDepth":430,"depth":430,"links":1313},[1314,1315,1316,1322,1323,1324,1325,1326,1327],{"id":185,"depth":430,"text":186},{"id":230,"depth":430,"text":231},{"id":390,"depth":430,"text":391,"children":1317},[1318,1319,1320,1321],{"id":395,"depth":452,"text":396},{"id":409,"depth":452,"text":410},{"id":630,"depth":452,"text":631},{"id":997,"depth":452,"text":998},{"id":1116,"depth":430,"text":1117},{"id":1123,"depth":430,"text":1124},{"id":1138,"depth":430,"text":1139},{"id":1168,"depth":430,"text":1169},{"id":1196,"depth":430,"text":1197},{"id":1272,"depth":430,"text":1273},"Use streaming HTML rewriting and low-cardinality variant keys to personalise cached pages at the edge without fragmenting the cache.","md",{"slug":1331,"type":1332,"breadcrumb":1333,"datePublished":1340,"dateModified":1340},"personalizing-cached-pages-at-the-edge","article",[1334,1336,1337,1338],{"name":1335,"url":1256},"Home",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1339},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fpersonalizing-cached-pages-at-the-edge\u002F","2026-10-06",true,"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fpersonalizing-cached-pages-at-the-edge",{"title":1344,"description":1345},"Personalizing Cached Pages at the Edge Without Cache Misses","Serve one cached HTML document to everyone and personalise it in the edge function — currency, locale, greetings, experiment arms — while keeping TTFB under 200ms.","advanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fpersonalizing-cached-pages-at-the-edge\u002Findex","v9DZ_L5hbfLQm_6z7EKSLK55RJQhb3SVNinfC3MaAss",[1349,1353],{"title":1350,"path":1351,"stem":1352},"Edge Side Includes vs Client-Side Fragments","\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fedge-side-includes-vs-client-side-fragments","advanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fedge-side-includes-vs-client-side-fragments\u002Findex",{"title":1295,"path":1354,"stem":1355},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fusing-the-cache-api-in-cloudflare-workers","advanced-caching-strategies-cdn-architecture\u002Fedge-compute-and-dynamic-caching\u002Fusing-the-cache-api-in-cloudflare-workers\u002Findex",1791308076693]