[{"data":1,"prerenderedAt":1623},["ShallowReactive",2],{"content:\u002Fnetwork-protocol-optimization":3,"surroundings:\u002Fnetwork-protocol-optimization":1615},{"id":4,"title":5,"body":6,"description":1596,"extension":1597,"meta":1598,"navigation":1608,"path":1609,"seo":1610,"stem":1613,"__hash__":1614},"content\u002Fnetwork-protocol-optimization\u002Findex.md","Network & Server Response Optimization",{"type":7,"value":8,"toc":1575},"minimark",[9,14,18,21,50,233,238,241,275,278,281,361,365,376,383,401,405,412,415,421,500,504,507,510,520,524,527,537,661,665,668,671,676,680,683,687,690,700,714,718,723,1051,1055,1065,1072,1076,1161,1165,1313,1317,1374,1378,1428,1432,1444,1453,1462,1471,1480,1489,1498,1502,1534,1538,1561,1565,1568,1571],[10,11,13],"h1",{"id":12},"network-server-response-optimization-from-request-to-first-byte-and-beyond","Network & Server Response Optimization: From Request to First Byte and Beyond",[15,16,17],"p",{},"Every page load starts with a network round trip and a server response, and nothing on the page can happen until the first bytes of HTML arrive. Time to First Byte (TTFB) is the foundation of every other loading metric: First Contentful Paint and Largest Contentful Paint can never be faster than TTFB, and on many sites TTFB alone consumes 40% or more of the LCP budget on mobile. After the first byte, how the HTML and its subresources travel — compressed or not, over which protocol, in one piece or streamed, requested on demand or speculatively ahead of time — determines how quickly the rest of the page follows.",[15,19,20],{},"The network layer is also where some of the largest, cheapest improvements hide. Turning on Brotli for text responses can shave 15–25% off HTML, CSS and JavaScript compared with gzip. Removing a redirect from a campaign link removes a full round trip — often 100–300ms on mobile. Streaming HTML lets the browser start fetching CSS and the LCP image while the server is still rendering the body. Speculation rules can make the next navigation effectively instant by prerendering it before the user clicks.",[15,22,23,24,29,30,34,35,39,40,44,45,49],{},"This section is organised around the path of a request. ",[25,26,28],"a",{"href":27},"\u002Fnetwork-protocol-optimization\u002Ftime-to-first-byte-optimization\u002F","Time to First Byte optimization"," covers everything before the first byte: redirects, connection setup, server and backend time, cold starts. ",[25,31,33],{"href":32},"\u002Fnetwork-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002F","Compression with Brotli and Zstandard"," covers how many bytes travel. ",[25,36,38],{"href":37},"\u002Fnetwork-protocol-optimization\u002Fhttp2-http3-and-connection-management\u002F","HTTP\u002F2, HTTP\u002F3 and connection management"," covers how connections are set up and shared. ",[25,41,43],{"href":42},"\u002Fnetwork-protocol-optimization\u002Fstreaming-ssr-and-early-flush\u002F","Streaming SSR and early flush"," covers sending HTML progressively. ",[25,46,48],{"href":47},"\u002Fnetwork-protocol-optimization\u002Fspeculative-loading-and-prefetching\u002F","Speculative loading and prefetching"," covers starting the next page before it is requested.",[15,51,52],{},[53,54,60,61,60,68,60,72,60,75,60,84,60,90,60,101,60,105,60,110,60,118,60,125,60,130,60,134,60,138,60,142,60,146,60,151,60,156,60,160,60,167,60,172,60,178,60,182,60,187,60,193,60,197,60,201,60,205,60,209,60,213,60,217,60,220,60,224,60,230,60],"svg",{"viewBox":55,"width":56,"role":57,"ariaLabel":58,"style":59},"0 0 760 238","100%","img","Timeline of a typical mobile navigation from redirect and connection setup through server time to the first byte and first paint.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[62,63],"rect",{"className":64,"x":66,"y":66,"width":56,"height":56,"fill":67},[65],"svg-canvas","0","#ffffff",[69,70,71],"title",{},"Anatomy of a navigation on a mobile connection",[73,74,58],"desc",{},[62,76],{"x":77,"y":77,"width":78,"height":79,"rx":80,"fill":81,"stroke":82,"style":83},"1","758","236","10","none","currentColor","stroke-opacity:0.18",[85,86,71],"text",{"x":87,"y":88,"fill":82,"style":89},"28.0","34.0","font-size:16px;font-weight:700",[91,92],"line",{"x1":93,"y1":94,"x2":93,"y2":95,"stroke":96,"strokeWidth":97,"strokeDashArray":98,"style":100},"646.7","72.0","194.0","#b8860b","1.5",[99,99],"4","stroke-opacity:0.9",[91,102],{"x1":103,"y1":94,"x2":103,"y2":95,"stroke":96,"strokeWidth":97,"strokeDashArray":104,"style":100},"477.6",[99,99],[85,106,109],{"x":87,"y":107,"fill":82,"style":108},"96.0","font-size:12.5px;font-weight:700","Network",[62,111],{"x":112,"y":113,"width":114,"height":115,"rx":116,"fill":81,"stroke":82,"style":117},"92.5","78.0","72.5","26.0","3","stroke-opacity:0.35",[85,119,124],{"x":120,"y":121,"fill":82,"style":122,"textAnchor":123},"128.8","95.5","font-size:11.5px","middle","redirect",[62,126],{"x":127,"y":113,"width":128,"height":115,"rx":116,"fill":82,"stroke":82,"style":129},"166.5","35.5","fill-opacity:0.06;stroke-opacity:0.4",[85,131,133],{"x":132,"y":121,"fill":82,"style":122,"textAnchor":123},"184.3","DNS",[62,135],{"x":136,"y":113,"width":137,"height":115,"rx":116,"fill":82,"stroke":82,"style":129},"203.5","88.3",[85,139,141],{"x":140,"y":121,"fill":82,"style":122,"textAnchor":123},"247.7","TCP + TLS",[85,143,145],{"x":87,"y":144,"fill":82,"style":108},"134.0","Server",[62,147],{"x":148,"y":149,"width":150,"height":115,"rx":116,"fill":81,"stroke":82,"style":117},"293.4","116.0","183.5",[85,152,155],{"x":153,"y":154,"fill":82,"style":122,"textAnchor":123},"385.1","133.5","server + backend time",[85,157,159],{"x":87,"y":158,"fill":82,"style":108},"172.0","Browser",[62,161],{"x":162,"y":163,"width":164,"height":115,"rx":116,"fill":165,"stroke":165,"style":166},"478.3","154.0","114.8","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[85,168,171],{"x":169,"y":170,"fill":82,"style":122,"textAnchor":123},"535.7","171.5","HTML + CSS",[62,173],{"x":174,"y":163,"width":175,"height":115,"rx":116,"fill":176,"stroke":96,"style":177},"594.6","51.4","#ffc300","fill-opacity:0.24;stroke-opacity:0.9",[85,179,181],{"x":180,"y":170,"fill":82,"style":122,"textAnchor":123},"620.3","paint",[91,183],{"x1":184,"y1":95,"x2":185,"y2":95,"stroke":82,"style":186},"91.8","726.0","stroke-opacity:0.4",[85,188,192],{"x":189,"y":190,"fill":191,"style":122,"textAnchor":123},"102.5","211.0","#51617a","0ms",[85,194,196],{"x":195,"y":190,"fill":191,"style":122,"textAnchor":123},"171.1","300ms",[85,198,200],{"x":199,"y":190,"fill":191,"style":122,"textAnchor":123},"250.3","600ms",[85,202,204],{"x":203,"y":190,"fill":191,"style":122,"textAnchor":123},"329.6","900ms",[85,206,208],{"x":207,"y":190,"fill":191,"style":122,"textAnchor":123},"408.9","1200ms",[85,210,212],{"x":211,"y":190,"fill":191,"style":122,"textAnchor":123},"488.2","1500ms",[85,214,216],{"x":215,"y":190,"fill":191,"style":122,"textAnchor":123},"567.4","1800ms",[85,218,219],{"x":93,"y":190,"fill":191,"style":122,"textAnchor":123},"2100ms",[85,221,223],{"x":222,"y":190,"fill":191,"style":122,"textAnchor":123},"712.3","2400ms",[85,225,229],{"x":103,"y":226,"fill":227,"style":228,"textAnchor":123},"68.0","#8a6608","font-size:12px;font-weight:700","TTFB",[85,231,232],{"x":93,"y":226,"fill":227,"style":228,"textAnchor":123},"FCP",[234,235,237],"h2",{"id":236},"diagnostic-overview-where-network-time-goes","Diagnostic Overview: Where Network Time Goes",[15,239,240],{},"Start with field data. TTFB at the 75th percentile, segmented by country, device class and page type, tells you how much of the loading budget the network and server consume before the browser can do anything. Google's guidance considers a TTFB of 800ms or less \"good\" for most sites, but for a fast LCP on mobile, aiming for well under 600ms at p75 leaves room for everything else. Then break TTFB into its parts using Navigation Timing in RUM or a WebPageTest run:",[242,243,244,257,263,269],"ul",{},[245,246,247,251,252,256],"li",{},[248,249,250],"strong",{},"Redirect time."," Any ",[253,254,255],"code",{},"3xx"," hops before the final URL. Each one is a full round trip, often more on mobile, and can include a new connection to a different host.",[245,258,259,262],{},[248,260,261],{},"DNS, TCP and TLS."," Connection setup to the origin or CDN. Usually a few round trips; much lower with TLS 1.3, HTTP\u002F3 (QUIC) and connection reuse.",[245,264,265,268],{},[248,266,267],{},"Request and server time."," From the request leaving the browser to the first byte arriving: network latency plus everything the server does — routing, authentication, database queries, API calls, rendering.",[245,270,271,274],{},[248,272,273],{},"Content transfer."," After the first byte: how long the HTML takes to arrive, which depends on size, compression and whether the server streams.",[15,276,277],{},"Each part has a distinct fix. Redirects are removed at the source (updated links, canonical URLs). Connection time is reduced by CDNs close to users, modern TLS and HTTP\u002F3, and fewer distinct origins. Server time is reduced by caching (at the CDN or in the application), faster backends, and streaming so that the first byte does not wait for the slowest query. Transfer time is reduced by compression and smaller HTML.",[15,279,280],{},"Field and lab disagree more for network metrics than for any other. Lab tests run from a few locations on simulated connections; real users connect from everywhere, on networks of every quality, often hitting cold caches and cold connections. A TTFB of 200ms in your CI runs and 1.2s at p75 in the field is common for sites without a CDN in front of HTML, or with low CDN cache hit ratios for documents. Always validate network improvements with field data segmented by geography.",[15,282,283],{},[53,284,60,287,60,290,60,293,60,295,60,298,60,300,60,304,60,311,60,317,60,322,60,326,60,331,60,335,60,338,60,342,60,346,60,350,60,354,60,358,60],{"viewBox":285,"width":56,"role":57,"ariaLabel":286,"style":59},"0 0 760 216","Bar chart of mobile TTFB at the 75th percentile for one application served from different delivery setups.",[62,288],{"className":289,"x":66,"y":66,"width":56,"height":56,"fill":67},[65],[69,291,292],{},"TTFB p75 on mobile by delivery setup (same application)",[73,294,286],{},[62,296],{"x":77,"y":77,"width":78,"height":297,"rx":80,"fill":81,"stroke":82,"style":83},"214",[85,299,292],{"x":87,"y":88,"fill":82,"style":89},[91,301],{"x1":302,"y1":94,"x2":302,"y2":95,"stroke":96,"strokeWidth":97,"strokeDashArray":303,"style":100},"496.8",[99,99],[85,305,310],{"x":306,"y":307,"fill":82,"style":308,"textAnchor":309},"248.6","92.0","font-size:13px","end","Single-region origin, no CDN for HTML",[62,312],{"x":313,"y":113,"width":314,"height":315,"rx":116,"fill":81,"stroke":82,"style":316},"260.6","407.4","19","fill-opacity:0.7;stroke-opacity:0.35",[85,318,321],{"x":319,"y":307,"fill":82,"style":320},"674.0","font-size:12px;font-weight:600","1380ms",[85,323,325],{"x":306,"y":324,"fill":82,"style":308,"textAnchor":309},"123.0","CDN proxy, HTML not cached",[62,327],{"x":313,"y":328,"width":329,"height":315,"rx":116,"fill":176,"stroke":96,"style":330},"109.0","301.1","fill-opacity:0.7;stroke-opacity:0.9",[85,332,334],{"x":333,"y":324,"fill":82,"style":320},"567.7","1020ms",[85,336,337],{"x":306,"y":163,"fill":82,"style":308,"textAnchor":309},"CDN-cached HTML + SWR",[62,339],{"x":313,"y":340,"width":341,"height":315,"rx":116,"fill":176,"stroke":96,"style":330},"140.0","91.5",[85,343,345],{"x":344,"y":163,"fill":82,"style":320},"358.1","310ms",[85,347,349],{"x":306,"y":348,"fill":82,"style":308,"textAnchor":309},"185.0","+ HTTP\u002F3 and TLS 1.3",[62,351],{"x":313,"y":352,"width":353,"height":315,"rx":116,"fill":176,"stroke":96,"style":330},"171.0","79.7",[85,355,357],{"x":356,"y":348,"fill":82,"style":320},"346.3","270ms",[85,359,360],{"x":302,"y":226,"fill":227,"style":228,"textAnchor":123},"good threshold",[234,362,364],{"id":363},"architecture-1-getting-the-first-byte-out-fast","Architecture 1: Getting the First Byte Out Fast",[15,366,367,368,371,372,375],{},"TTFB is the sum of everything before the browser receives the first byte of the document. The largest component is usually server time — the application building the response — followed by connection setup and redirects. The most effective improvement is not to build the response at all for most requests: serve HTML from a CDN cache, using ",[253,369,370],{},"s-maxage"," with ",[253,373,374],{},"stale-while-revalidate"," so the cache answers instantly while refreshing in the background. For personalised pages, cache the shared shell and fill personal parts at the edge or on the client.",[15,377,378,379,382],{},"When the response must be built per request, measure where the time goes with the ",[253,380,381],{},"Server-Timing"," response header: the server reports durations for database queries, API calls, template rendering and cache lookups, and the browser exposes them in DevTools and to RUM scripts. That turns \"TTFB is slow\" into \"the recommendations API call takes 420ms at p75\". Fix the slowest dependency, run independent calls in parallel, add application-level caching, or move slow parts out of the critical path by streaming them later.",[15,384,385,386,389,390,393,394,397,398,400],{},"Serverless and edge platforms add a special case: cold starts. A function that has not run recently must be initialised — loading code, establishing database connections — before handling the request, adding hundreds of milliseconds or more. Bundle size, initialisation work, provisioned concurrency and connection pooling all affect it. Redirect chains are the other classic TTFB inflator: ",[253,387,388],{},"http"," to ",[253,391,392],{},"https",", apex to ",[253,395,396],{},"www",", adding a trailing slash, locale detection and campaign tracking redirects can stack into three or four hops. The guides in ",[25,399,28],{"href":27}," cover each.",[234,402,404],{"id":403},"architecture-2-sending-fewer-bytes","Architecture 2: Sending Fewer Bytes",[15,406,407,408,411],{},"Text resources — HTML, CSS, JavaScript, JSON, SVG — compress extremely well. Gzip has been universal for decades; Brotli, supported by all modern browsers, typically produces files 15–25% smaller than gzip for the same content, and more at its highest levels for static assets. Zstandard (zstd), now supported as a ",[253,409,410],{},"Content-Encoding"," in Chromium-based browsers and Firefox, offers compression ratios between gzip and Brotli with much faster compression speeds, making it attractive for dynamic responses.",[15,413,414],{},"Static assets should be compressed once at build time at maximum settings (Brotli level 11), stored alongside the originals, and served directly — the server never compresses them on the fly. Dynamic responses such as HTML from SSR should be compressed on the fly at moderate levels (Brotli 4–6 or zstd 3–6), where the trade-off between CPU time and size is best. Compression dictionary transport goes further: with a shared dictionary (for example, the previous version of a JavaScript bundle), the browser and server can transfer only the differences, cutting repeat-visit downloads of updated bundles by 80–95%.",[15,416,417,418,420],{},"The guides in ",[25,419,33],{"href":32}," cover enabling Brotli across servers and CDNs, zstd support, precompression at build time and dictionary transport.",[15,422,423],{},[53,424,60,427,60,430,60,433,60,435,60,438,60,440,60,445,60,450,60,453,60,457,60,462,60,465,60,469,60,473,60,477,60,481,60,485,60,489,60,492,60,496,60],{"viewBox":425,"width":56,"role":57,"ariaLabel":426,"style":59},"0 0 760 225","Bar chart comparing the transferred size of one JavaScript bundle with different compression methods.",[62,428],{"className":429,"x":66,"y":66,"width":56,"height":56,"fill":67},[65],[69,431,432],{},"Compressed size of a 480KB JavaScript bundle",[73,434,426],{},[62,436],{"x":77,"y":77,"width":78,"height":437,"rx":80,"fill":81,"stroke":82,"style":83},"223",[85,439,432],{"x":87,"y":88,"fill":82,"style":89},[85,441,444],{"x":442,"y":443,"fill":82,"style":308,"textAnchor":309},"245.7","70.0","Uncompressed",[62,446],{"x":447,"y":448,"width":449,"height":315,"rx":116,"fill":81,"stroke":82,"style":316},"257.7","56.0","410.3",[85,451,452],{"x":319,"y":443,"fill":82,"style":320},"480KB",[85,454,456],{"x":442,"y":455,"fill":82,"style":308,"textAnchor":309},"101.0","gzip -6",[62,458],{"x":447,"y":459,"width":460,"height":315,"rx":116,"fill":165,"stroke":165,"style":461},"87.0","121.4","fill-opacity:0.55;stroke-opacity:0.9",[85,463,464],{"x":153,"y":455,"fill":82,"style":320},"142KB",[85,466,468],{"x":442,"y":467,"fill":82,"style":308,"textAnchor":309},"132.0","zstd -19",[62,470],{"x":447,"y":471,"width":472,"height":315,"rx":116,"fill":165,"stroke":165,"style":461},"118.0","106.0",[85,474,476],{"x":475,"y":467,"fill":82,"style":320},"369.7","124KB",[85,478,480],{"x":442,"y":479,"fill":82,"style":308,"textAnchor":309},"163.0","Brotli -11",[62,482],{"x":447,"y":483,"width":484,"height":315,"rx":116,"fill":176,"stroke":96,"style":330},"149.0","100.9",[85,486,488],{"x":487,"y":479,"fill":82,"style":320},"364.6","118KB",[85,490,491],{"x":442,"y":95,"fill":82,"style":308,"textAnchor":309},"Brotli with previous-version dictionary",[62,493],{"x":447,"y":494,"width":495,"height":315,"rx":116,"fill":176,"stroke":96,"style":330},"180.0","12.0",[85,497,499],{"x":498,"y":95,"fill":82,"style":320},"275.7","14KB",[234,501,503],{"id":502},"architecture-3-connections-and-protocols","Architecture 3: Connections and Protocols",[15,505,506],{},"Before any bytes can be requested, the browser needs a connection: DNS lookup, a TCP handshake, and a TLS handshake — together two to three round trips on HTTP\u002F1.1 and HTTP\u002F2 over TLS 1.2, fewer with TLS 1.3, and as few as one with HTTP\u002F3 over QUIC (or zero for resumed connections). On mobile networks with 100–200ms round-trip times, connection setup alone can take half a second.",[15,508,509],{},"HTTP\u002F2 multiplexes many requests over one connection, which made old HTTP\u002F1.1 workarounds — domain sharding, spriting, concatenating everything — unnecessary and often harmful: each extra domain needs its own connection setup and splits prioritisation. HTTP\u002F3 replaces TCP with QUIC over UDP, which removes TCP-level head-of-line blocking (a lost packet no longer stalls all streams), handles network changes (Wi-Fi to cellular) without reconnecting, and reduces handshake round trips. Its benefits are largest on poor networks: high latency and packet loss.",[15,511,512,513,516,517,519],{},"Connection management matters as much as protocol choice. Each third-party origin on the critical path costs a new connection; ",[253,514,515],{},"preconnect"," can start it early, but removing or self-hosting critical third-party resources is better. Browsers can reuse (coalesce) an HTTP\u002F2 or HTTP\u002F3 connection across hostnames that share an IP address and a certificate covering both names, which turns separate asset domains into the same connection. The guides in ",[25,518,38],{"href":37}," cover measuring HTTP\u002F3 gains, domain sharding, coalescing and head-of-line blocking.",[234,521,523],{"id":522},"architecture-4-streaming-html","Architecture 4: Streaming HTML",[15,525,526],{},"Traditional server rendering waits for all data, renders the whole page, then sends it. TTFB equals the slowest query plus render time, and the browser sits idle the whole time. Streaming changes the order: send the document head (with links to CSS, preloads for the LCP image and fonts) immediately, then stream the body as it is rendered, with slow sections arriving later. The browser starts downloading critical resources while the server is still working, and it can paint the first parts of the page before the last ones arrive.",[15,528,529,530,533,534,536],{},"Modern frameworks support streaming natively: React's streaming SSR with Suspense boundaries, Next.js App Router loading states, Nuxt and SvelteKit streaming, Astro's streaming responses. Even without a framework, flushing the head early — writing ",[253,531,532],{},"\u003Chead>"," with critical hints before running slow queries — gives much of the benefit. The main obstacles are intermediaries that buffer the response: some CDNs, proxies, compression middleware and serverless platforms collect the whole response before forwarding it, silently disabling streaming. The guides in ",[25,535,43],{"href":42}," cover React Suspense streaming, early head flushing and keeping streams intact through CDNs.",[15,538,539],{},[53,540,60,543,60,546,60,549,60,551,60,554,60,556,60,561,60,564,60,568,60,572,60,576,60,580,60,584,60,587,60,590,60,593,60,597,60,601,60,604,60,608,60,613,60,616,60,619,60,622,60,626,60,630,60,634,60,637,60,641,60,645,60,648,60,652,60,656,60,658,60],{"viewBox":541,"width":56,"role":57,"ariaLabel":542,"style":59},"0 0 760 232","Timeline comparing a buffered server-rendered response with a streamed one that flushes the head early.",[62,544],{"className":545,"x":66,"y":66,"width":56,"height":56,"fill":67},[65],[69,547,548],{},"Buffered vs streamed server rendering",[73,550,542],{},[62,552],{"x":77,"y":77,"width":78,"height":553,"rx":80,"fill":81,"stroke":82,"style":83},"230",[85,555,548],{"x":87,"y":88,"fill":82,"style":89},[91,557],{"x1":558,"y1":94,"x2":558,"y2":559,"stroke":96,"strokeWidth":97,"strokeDashArray":560,"style":100},"477.3","188.0",[99,99],[85,562,563],{"x":87,"y":107,"fill":82,"style":108},"Buffered SSR",[62,565],{"x":566,"y":113,"width":567,"height":115,"rx":116,"fill":81,"stroke":82,"style":117},"129.8","297.0",[85,569,571],{"x":570,"y":121,"fill":82,"style":122,"textAnchor":123},"278.3","queries + render (browser idle)",[62,573],{"x":574,"y":113,"width":575,"height":115,"rx":116,"fill":82,"stroke":82,"style":129},"428.3","114.6",[85,577,579],{"x":578,"y":121,"fill":82,"style":122,"textAnchor":123},"485.6","CSS + LCP image",[62,581],{"x":582,"y":113,"width":583,"height":115,"rx":116,"fill":165,"stroke":165,"style":166},"544.3","64.8",[85,585,181],{"x":586,"y":121,"fill":82,"style":122,"textAnchor":123},"576.8",[85,588,589],{"x":87,"y":144,"fill":82,"style":108},"Streamed SSR",[62,591],{"x":566,"y":149,"width":592,"height":115,"rx":116,"fill":176,"stroke":96,"style":177},"38.3",[85,594,596],{"x":595,"y":154,"fill":82,"style":122,"textAnchor":123},"148.9","head",[62,598],{"x":599,"y":149,"width":600,"height":115,"rx":116,"fill":165,"stroke":165,"style":166},"169.6","124.5",[85,602,579],{"x":603,"y":154,"fill":82,"style":122,"textAnchor":123},"231.9",[62,605],{"x":599,"y":606,"width":607,"height":115,"rx":116,"fill":82,"stroke":82,"style":129},"148.0","257.2",[85,609,612],{"x":610,"y":611,"fill":82,"style":122,"textAnchor":123},"298.2","165.5","body streams",[62,614],{"x":574,"y":149,"width":615,"height":115,"rx":116,"fill":176,"stroke":96,"style":177},"48.2",[85,617,181],{"x":618,"y":154,"fill":82,"style":122,"textAnchor":123},"452.4",[91,620],{"x1":621,"y1":559,"x2":185,"y2":559,"stroke":82,"style":186},"129.1",[85,623,192],{"x":624,"y":625,"fill":191,"style":122,"textAnchor":123},"139.7","205.0",[85,627,629],{"x":628,"y":625,"fill":191,"style":122,"textAnchor":123},"195.4","200ms",[85,631,633],{"x":632,"y":625,"fill":191,"style":122,"textAnchor":123},"261.7","400ms",[85,635,200],{"x":636,"y":625,"fill":191,"style":122,"textAnchor":123},"328.0",[85,638,640],{"x":639,"y":625,"fill":191,"style":122,"textAnchor":123},"394.4","800ms",[85,642,644],{"x":643,"y":625,"fill":191,"style":122,"textAnchor":123},"460.7","1000ms",[85,646,208],{"x":647,"y":625,"fill":191,"style":122,"textAnchor":123},"527.0",[85,649,651],{"x":650,"y":625,"fill":191,"style":122,"textAnchor":123},"593.3","1400ms",[85,653,655],{"x":654,"y":625,"fill":191,"style":122,"textAnchor":123},"659.7","1600ms",[85,657,216],{"x":222,"y":625,"fill":191,"style":122,"textAnchor":123},[85,659,660],{"x":558,"y":226,"fill":227,"style":228,"textAnchor":123},"LCP streamed",[234,662,664],{"id":663},"architecture-5-loading-the-next-page-before-it-is-requested","Architecture 5: Loading the Next Page Before It Is Requested",[15,666,667],{},"The fastest navigation is one that has already happened. Speculative loading uses idle time and user intent to fetch, or fully render, likely next pages. The Speculation Rules API lets a page declare which URLs to prefetch or prerender, either as a list or with document rules that match links on the page, and at what eagerness: immediately, when the user hovers or presses a link, or conservatively on pointer down. A prerendered page is loaded and rendered in a hidden tab; activating it takes only a few milliseconds, and LCP for that navigation is close to zero.",[15,669,670],{},"Speculation has costs. Every prefetch uses bandwidth and server capacity, and every prerender also uses CPU and memory on the user's device and runs the page's JavaScript — including analytics, which must not count a visit that never happened. Good speculation strategies target high-probability navigations (the next article in a series, the top search result, the product a user hovers over), respect data saver modes, and use eagerness settings that balance hit rate against waste. Libraries like Quicklink prefetch links as they enter the viewport during idle time, an approach that works across browsers without speculation rules support.",[15,672,417,673,675],{},[25,674,48],{"href":47}," cover speculation rules, the prerender versus prefetch trade-off, viewport prefetching and analytics correctness.",[234,677,679],{"id":678},"prioritising-network-work","Prioritising Network Work",[15,681,682],{},"With so many levers, order matters. A practical sequence for most sites is: first remove redirects on key entry URLs (cheap, large win per affected visit); second, cache HTML at the CDN wherever content allows (the biggest TTFB win); third, make sure Brotli is on for every text response and static assets are precompressed (a configuration change); fourth, enable HTTP\u002F3 and TLS 1.3 at the CDN (usually a toggle); fifth, stream or early-flush uncached responses; and finally, add speculation rules for the most common navigation paths. Each step is measurable in the field on its own, so roll them out one at a time and confirm the effect on TTFB, FCP and LCP before moving on. Teams that change everything at once rarely learn which change mattered, and cannot tell which one caused a regression. Keep a short changelog of infrastructure changes next to your RUM dashboards, so every step in the TTFB trend line has an explanation.",[234,684,686],{"id":685},"monitoring-and-ci-holding-the-network-layer-to-budget","Monitoring and CI: Holding the Network Layer to Budget",[15,688,689],{},"Network performance regresses for reasons outside the codebase: a CDN configuration change disables HTML caching, a new marketing redirect is added, a backend dependency slows down, a proxy starts buffering responses. Monitoring must cover infrastructure, not just builds.",[15,691,692,693,695,696,699],{},"In the field, collect TTFB and its sub-parts (redirect, DNS, connect, request-to-response) from Navigation Timing in your RUM, along with ",[253,694,381],{}," entries and the ",[253,697,698],{},"nextHopProtocol"," (h2 or h3). Segment by country and connection type. Alert on p75 TTFB per key template. Track CDN cache hit ratio for HTML separately from assets — it is often the single best predictor of TTFB.",[15,701,702,703,705,706,709,710,713],{},"In CI and synthetic monitoring, assert on response headers: ",[253,704,410],{}," for text resources, ",[253,707,708],{},"Cache-Control"," for HTML and assets, ",[253,711,712],{},"alt-svc"," for HTTP\u002F3 advertisement, absence of redirects for key entry URLs, and streaming behaviour (time from first byte to last byte for the HTML should be longer than zero when streaming is expected). These checks are cheap and catch the most common regressions.",[234,715,717],{"id":716},"reference-implementations","Reference Implementations",[719,720,722],"h3",{"id":721},"server-timing-for-backend-breakdown","Server-Timing for backend breakdown",[724,725,730],"pre",{"className":726,"code":727,"language":728,"meta":729,"style":729},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F Express middleware: report timings for each phase of the request.\napp.use((req, res, next) => {\n  const marks = [];\n  res.locals.time = async (name, fn) => {\n    const t0 = performance.now();\n    try { return await fn(); } finally { marks.push(`${name};dur=${(performance.now() - t0).toFixed(1)}`); }\n  };\n  const writeHead = res.writeHead;\n  res.writeHead = function (...args) { if (marks.length) res.setHeader('Server-Timing', marks.join(', ')); return writeHead.apply(this, args); };\n  next();\n});\n\u002F\u002F In a route: const user = await res.locals.time('db-user', () => db.user(id));\n\u002F\u002F trade-off: Server-Timing is visible to anyone who loads the page; avoid exposing\n\u002F\u002F sensitive internals, or only emit it for sampled or authenticated requests.\n","javascript","",[253,731,732,740,779,795,826,846,925,931,944,1019,1027,1033,1039,1045],{"__ignoreMap":729},[733,734,736],"span",{"class":91,"line":735},1,[733,737,739],{"class":738},"sjfSM","\u002F\u002F Express middleware: report timings for each phase of the request.\n",[733,741,743,747,751,754,758,761,764,766,769,772,776],{"class":91,"line":742},2,[733,744,746],{"class":745},"saISM","app.",[733,748,750],{"class":749},"smZ65","use",[733,752,753],{"class":745},"((",[733,755,757],{"class":756},"sQw3B","req",[733,759,760],{"class":745},", ",[733,762,763],{"class":756},"res",[733,765,760],{"class":745},[733,767,768],{"class":756},"next",[733,770,771],{"class":745},") ",[733,773,775],{"class":774},"sPARh","=>",[733,777,778],{"class":745}," {\n",[733,780,782,785,789,792],{"class":91,"line":781},3,[733,783,784],{"class":774},"  const",[733,786,788],{"class":787},"sPXB4"," marks",[733,790,791],{"class":774}," =",[733,793,794],{"class":745}," [];\n",[733,796,798,801,804,806,809,812,815,817,820,822,824],{"class":91,"line":797},4,[733,799,800],{"class":745},"  res.locals.",[733,802,803],{"class":749},"time",[733,805,791],{"class":774},[733,807,808],{"class":774}," async",[733,810,811],{"class":745}," (",[733,813,814],{"class":756},"name",[733,816,760],{"class":745},[733,818,819],{"class":756},"fn",[733,821,771],{"class":745},[733,823,775],{"class":774},[733,825,778],{"class":745},[733,827,829,832,835,837,840,843],{"class":91,"line":828},5,[733,830,831],{"class":774},"    const",[733,833,834],{"class":787}," t0",[733,836,791],{"class":774},[733,838,839],{"class":745}," performance.",[733,841,842],{"class":749},"now",[733,844,845],{"class":745},"();\n",[733,847,849,852,855,858,861,864,867,870,873,876,879,883,885,888,890,893,896,898,901,904,906,909,912,914,916,919,922],{"class":91,"line":848},6,[733,850,851],{"class":774},"    try",[733,853,854],{"class":745}," { ",[733,856,857],{"class":774},"return",[733,859,860],{"class":774}," await",[733,862,863],{"class":749}," fn",[733,865,866],{"class":745},"(); } ",[733,868,869],{"class":774},"finally",[733,871,872],{"class":745}," { marks.",[733,874,875],{"class":749},"push",[733,877,878],{"class":745},"(",[733,880,882],{"class":881},"sZ8jY","`${",[733,884,814],{"class":745},[733,886,887],{"class":881},"};dur=${",[733,889,878],{"class":881},[733,891,892],{"class":745},"performance",[733,894,895],{"class":881},".",[733,897,842],{"class":749},[733,899,900],{"class":881},"() ",[733,902,903],{"class":774},"-",[733,905,834],{"class":745},[733,907,908],{"class":881},").",[733,910,911],{"class":749},"toFixed",[733,913,878],{"class":881},[733,915,77],{"class":787},[733,917,918],{"class":881},")",[733,920,921],{"class":881},"}`",[733,923,924],{"class":745},"); }\n",[733,926,928],{"class":91,"line":927},7,[733,929,930],{"class":745},"  };\n",[733,932,934,936,939,941],{"class":91,"line":933},8,[733,935,784],{"class":774},[733,937,938],{"class":787}," writeHead",[733,940,791],{"class":774},[733,942,943],{"class":745}," res.writeHead;\n",[733,945,947,950,953,955,958,960,963,966,969,972,975,978,981,984,986,989,992,995,997,1000,1003,1005,1008,1011,1013,1016],{"class":91,"line":946},9,[733,948,949],{"class":745},"  res.",[733,951,952],{"class":749},"writeHead",[733,954,791],{"class":774},[733,956,957],{"class":774}," function",[733,959,811],{"class":745},[733,961,962],{"class":774},"...",[733,964,965],{"class":756},"args",[733,967,968],{"class":745},") { ",[733,970,971],{"class":774},"if",[733,973,974],{"class":745}," (marks.",[733,976,977],{"class":787},"length",[733,979,980],{"class":745},") res.",[733,982,983],{"class":749},"setHeader",[733,985,878],{"class":745},[733,987,988],{"class":881},"'Server-Timing'",[733,990,991],{"class":745},", marks.",[733,993,994],{"class":749},"join",[733,996,878],{"class":745},[733,998,999],{"class":881},"', '",[733,1001,1002],{"class":745},")); ",[733,1004,857],{"class":774},[733,1006,1007],{"class":745}," writeHead.",[733,1009,1010],{"class":749},"apply",[733,1012,878],{"class":745},[733,1014,1015],{"class":787},"this",[733,1017,1018],{"class":745},", args); };\n",[733,1020,1022,1025],{"class":91,"line":1021},10,[733,1023,1024],{"class":749},"  next",[733,1026,845],{"class":745},[733,1028,1030],{"class":91,"line":1029},11,[733,1031,1032],{"class":745},"});\n",[733,1034,1036],{"class":91,"line":1035},12,[733,1037,1038],{"class":738},"\u002F\u002F In a route: const user = await res.locals.time('db-user', () => db.user(id));\n",[733,1040,1042],{"class":91,"line":1041},13,[733,1043,1044],{"class":738},"\u002F\u002F trade-off: Server-Timing is visible to anyone who loads the page; avoid exposing\n",[733,1046,1048],{"class":91,"line":1047},14,[733,1049,1050],{"class":738},"\u002F\u002F sensitive internals, or only emit it for sampled or authenticated requests.\n",[719,1052,1054],{"id":1053},"caching-html-at-the-cdn-with-background-revalidation","Caching HTML at the CDN with background revalidation",[724,1056,1059],{"className":1057,"code":1058,"language":388,"meta":729,"style":729},"language-http shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400\n",[253,1060,1061],{"__ignoreMap":729},[733,1062,1063],{"class":91,"line":735},[733,1064,1058],{},[15,1066,1067,1068,1071],{},"Browsers always revalidate (",[253,1069,1070],{},"max-age=0","), while the CDN serves cached HTML for five minutes and keeps serving the stale copy for up to a day while fetching a fresh one in the background.",[719,1073,1075],{"id":1074},"brotli-for-static-and-dynamic-responses-nginx","Brotli for static and dynamic responses (nginx)",[724,1077,1081],{"className":1078,"code":1079,"language":1080,"meta":729,"style":729},"language-nginx shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","brotli on;\nbrotli_comp_level 5;                     # dynamic responses: balance CPU and size\nbrotli_types text\u002Fhtml text\u002Fcss application\u002Fjavascript application\u002Fjson image\u002Fsvg+xml;\nbrotli_static on;                        # serve prebuilt .br files when present\ngzip on; gzip_static on;                 # fallback for clients without br\n# trade-off: level 11 is too slow for on-the-fly compression; precompress static\n# assets at 11 during the build and keep dynamic compression at 4-6.\n","nginx",[253,1082,1083,1094,1108,1116,1129,1151,1156],{"__ignoreMap":729},[733,1084,1085,1088,1091],{"class":91,"line":735},[733,1086,1087],{"class":774},"brotli",[733,1089,1090],{"class":787}," on",[733,1092,1093],{"class":745},";\n",[733,1095,1096,1099,1102,1105],{"class":91,"line":742},[733,1097,1098],{"class":774},"brotli_comp_level",[733,1100,1101],{"class":787}," 5",[733,1103,1104],{"class":745},";                     ",[733,1106,1107],{"class":738},"# dynamic responses: balance CPU and size\n",[733,1109,1110,1113],{"class":91,"line":781},[733,1111,1112],{"class":774},"brotli_types",[733,1114,1115],{"class":745}," text\u002Fhtml text\u002Fcss application\u002Fjavascript application\u002Fjson image\u002Fsvg+xml;\n",[733,1117,1118,1121,1123,1126],{"class":91,"line":797},[733,1119,1120],{"class":774},"brotli_static",[733,1122,1090],{"class":787},[733,1124,1125],{"class":745},";                        ",[733,1127,1128],{"class":738},"# serve prebuilt .br files when present\n",[733,1130,1131,1134,1137,1140,1143,1145,1148],{"class":91,"line":828},[733,1132,1133],{"class":774},"gzip ",[733,1135,1136],{"class":787},"on",[733,1138,1139],{"class":745},";",[733,1141,1142],{"class":774}," gzip_static ",[733,1144,1136],{"class":787},[733,1146,1147],{"class":745},";                 ",[733,1149,1150],{"class":738},"# fallback for clients without br\n",[733,1152,1153],{"class":91,"line":848},[733,1154,1155],{"class":738},"# trade-off: level 11 is too slow for on-the-fly compression; precompress static\n",[733,1157,1158],{"class":91,"line":927},[733,1159,1160],{"class":738},"# assets at 11 during the build and keep dynamic compression at 4-6.\n",[719,1162,1164],{"id":1163},"flushing-the-head-early-nodejs","Flushing the head early (Node.js)",[724,1166,1168],{"className":726,"code":1167,"language":728,"meta":729,"style":729},"app.get('\u002Fproduct\u002F:id', async (req, res) => {\n  res.setHeader('Content-Type', 'text\u002Fhtml; charset=utf-8');\n  res.write(`\u003C!doctype html>\u003Chtml>\u003Chead>\u003Clink rel=\"stylesheet\" href=\"\u002Fcss\u002Fapp.css\">\n    \u003Clink rel=\"preload\" as=\"image\" href=\"\u002Fimg\u002F${req.params.id}-hero.avif\" fetchpriority=\"high\">\u003C\u002Fhead>\u003Cbody>`);\n  const product = await getProduct(req.params.id);       \u002F\u002F slow work after the flush\n  res.end(`${renderProduct(product)}\u003C\u002Fbody>\u003C\u002Fhtml>`);\n});\n\u002F\u002F trade-off: once the head is sent, the status code and headers are fixed; errors\n\u002F\u002F discovered later must be rendered as in-page error states, not HTTP errors.\n",[253,1169,1170,1201,1220,1232,1254,1274,1299,1303,1308],{"__ignoreMap":729},[733,1171,1172,1174,1177,1179,1182,1184,1187,1189,1191,1193,1195,1197,1199],{"class":91,"line":735},[733,1173,746],{"class":745},[733,1175,1176],{"class":749},"get",[733,1178,878],{"class":745},[733,1180,1181],{"class":881},"'\u002Fproduct\u002F:id'",[733,1183,760],{"class":745},[733,1185,1186],{"class":774},"async",[733,1188,811],{"class":745},[733,1190,757],{"class":756},[733,1192,760],{"class":745},[733,1194,763],{"class":756},[733,1196,771],{"class":745},[733,1198,775],{"class":774},[733,1200,778],{"class":745},[733,1202,1203,1205,1207,1209,1212,1214,1217],{"class":91,"line":742},[733,1204,949],{"class":745},[733,1206,983],{"class":749},[733,1208,878],{"class":745},[733,1210,1211],{"class":881},"'Content-Type'",[733,1213,760],{"class":745},[733,1215,1216],{"class":881},"'text\u002Fhtml; charset=utf-8'",[733,1218,1219],{"class":745},");\n",[733,1221,1222,1224,1227,1229],{"class":91,"line":781},[733,1223,949],{"class":745},[733,1225,1226],{"class":749},"write",[733,1228,878],{"class":745},[733,1230,1231],{"class":881},"`\u003C!doctype html>\u003Chtml>\u003Chead>\u003Clink rel=\"stylesheet\" href=\"\u002Fcss\u002Fapp.css\">\n",[733,1233,1234,1237,1239,1241,1244,1246,1249,1252],{"class":91,"line":797},[733,1235,1236],{"class":881},"    \u003Clink rel=\"preload\" as=\"image\" href=\"\u002Fimg\u002F${",[733,1238,757],{"class":745},[733,1240,895],{"class":881},[733,1242,1243],{"class":745},"params",[733,1245,895],{"class":881},[733,1247,1248],{"class":745},"id",[733,1250,1251],{"class":881},"}-hero.avif\" fetchpriority=\"high\">\u003C\u002Fhead>\u003Cbody>`",[733,1253,1219],{"class":745},[733,1255,1256,1258,1261,1263,1265,1268,1271],{"class":91,"line":828},[733,1257,784],{"class":774},[733,1259,1260],{"class":787}," product",[733,1262,791],{"class":774},[733,1264,860],{"class":774},[733,1266,1267],{"class":749}," getProduct",[733,1269,1270],{"class":745},"(req.params.id);       ",[733,1272,1273],{"class":738},"\u002F\u002F slow work after the flush\n",[733,1275,1276,1278,1280,1282,1284,1287,1289,1292,1294,1297],{"class":91,"line":848},[733,1277,949],{"class":745},[733,1279,309],{"class":749},[733,1281,878],{"class":745},[733,1283,882],{"class":881},[733,1285,1286],{"class":749},"renderProduct",[733,1288,878],{"class":881},[733,1290,1291],{"class":745},"product",[733,1293,918],{"class":881},[733,1295,1296],{"class":881},"}\u003C\u002Fbody>\u003C\u002Fhtml>`",[733,1298,1219],{"class":745},[733,1300,1301],{"class":91,"line":927},[733,1302,1032],{"class":745},[733,1304,1305],{"class":91,"line":933},[733,1306,1307],{"class":738},"\u002F\u002F trade-off: once the head is sent, the status code and headers are fixed; errors\n",[733,1309,1310],{"class":91,"line":946},[733,1311,1312],{"class":738},"\u002F\u002F discovered later must be rendered as in-page error states, not HTTP errors.\n",[719,1314,1316],{"id":1315},"speculation-rules-for-likely-navigations","Speculation rules for likely navigations",[724,1318,1322],{"className":1319,"code":1320,"language":1321,"meta":729,"style":729},"language-html shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u003Cscript type=\"speculationrules\">\n{ \"prerender\": [{ \"where\": { \"href_matches\": \"\u002Farticles\u002F*\" }, \"eagerness\": \"moderate\" }],\n  \"prefetch\":  [{ \"where\": { \"href_matches\": \"\u002F*\" }, \"eagerness\": \"conservative\" }] }\n\u003C\u002Fscript>\n\u003C!-- trade-off: moderate eagerness prerenders on hover (~200ms), which wastes some\n     work on links users hover but do not click; measure hit rates per pattern. -->\n","html",[253,1323,1324,1345,1350,1355,1364,1369],{"__ignoreMap":729},[733,1325,1326,1329,1333,1336,1339,1342],{"class":91,"line":735},[733,1327,1328],{"class":745},"\u003C",[733,1330,1332],{"class":1331},"sZBmE","script",[733,1334,1335],{"class":787}," type",[733,1337,1338],{"class":745},"=",[733,1340,1341],{"class":881},"\"speculationrules\"",[733,1343,1344],{"class":745},">\n",[733,1346,1347],{"class":91,"line":742},[733,1348,1349],{"class":745},"{ \"prerender\": [{ \"where\": { \"href_matches\": \"\u002Farticles\u002F*\" }, \"eagerness\": \"moderate\" }],\n",[733,1351,1352],{"class":91,"line":781},[733,1353,1354],{"class":745},"  \"prefetch\":  [{ \"where\": { \"href_matches\": \"\u002F*\" }, \"eagerness\": \"conservative\" }] }\n",[733,1356,1357,1360,1362],{"class":91,"line":797},[733,1358,1359],{"class":745},"\u003C\u002F",[733,1361,1332],{"class":1331},[733,1363,1344],{"class":745},[733,1365,1366],{"class":91,"line":828},[733,1367,1368],{"class":738},"\u003C!-- trade-off: moderate eagerness prerenders on hover (~200ms), which wastes some\n",[733,1370,1371],{"class":91,"line":848},[733,1372,1373],{"class":738},"     work on links users hover but do not click; measure hit rates per pattern. -->\n",[234,1375,1377],{"id":1376},"common-pitfalls","Common Pitfalls",[242,1379,1380,1386,1392,1398,1404,1410,1416,1422],{},[245,1381,1382,1385],{},[248,1383,1384],{},"Treating TTFB as a server-only metric."," Redirects, DNS, TLS and CDN configuration are often larger contributors than application code.",[245,1387,1388,1391],{},[248,1389,1390],{},"Never caching HTML."," The single biggest TTFB win for most content sites is CDN-cached HTML with background revalidation.",[245,1393,1394,1397],{},[248,1395,1396],{},"Compressing static assets on the fly at low levels."," Precompress at build time at maximum levels instead.",[245,1399,1400,1403],{},[248,1401,1402],{},"Compressing already-compressed formats."," JPEG, AVIF, WOFF2 and video gain nothing and waste CPU.",[245,1405,1406,1409],{},[248,1407,1408],{},"Domain sharding on HTTP\u002F2."," Extra connections and broken prioritisation.",[245,1411,1412,1415],{},[248,1413,1414],{},"Streaming that silently buffers."," A proxy or middleware collects the whole response; verify with timing, not assumptions.",[245,1417,1418,1421],{},[248,1419,1420],{},"Prerendering without analytics guards."," Inflated page views and skewed metrics.",[245,1423,1424,1427],{},[248,1425,1426],{},"Speculating on every link at high eagerness."," Wasted bandwidth and server load for little gain.",[234,1429,1431],{"id":1430},"faq","FAQ",[1433,1434,1437,1441],"details",{"className":1435},[1436],"faq-item",[1438,1439,1440],"summary",{},"What is a good TTFB?",[15,1442,1443],{},"Google considers 800ms or less at the 75th percentile good. For a fast LCP on mobile, aim lower — under about 600ms — because TTFB consumes part of the 2.5-second LCP budget.",[1433,1445,1447,1450],{"className":1446},[1436],[1438,1448,1449],{},"Is TTFB a Core Web Vital?",[15,1451,1452],{},"No, but it is a foundational diagnostic metric. LCP and FCP cannot be faster than TTFB, so slow TTFB caps every loading metric.",[1433,1454,1456,1459],{"className":1455},[1436],[1438,1457,1458],{},"Should I use Brotli or Zstandard?",[15,1460,1461],{},"Use Brotli for precompressed static assets, where its higher ratio matters most. For dynamic responses, Brotli at moderate levels and zstd are both good choices; zstd compresses faster at similar ratios. Keep gzip as a fallback.",[1433,1463,1465,1468],{"className":1464},[1436],[1438,1466,1467],{},"Does HTTP\u002F3 make my site faster?",[15,1469,1470],{},"Mostly for users on high-latency or lossy networks — mobile users, distant regions. On fast, stable connections the difference is small. Measure by protocol in RUM.",[1433,1472,1474,1477],{"className":1473},[1436],[1438,1475,1476],{},"Does streaming SSR help if my CDN caches HTML?",[15,1478,1479],{},"Cached HTML is already fast, so streaming matters most for uncached or personalised responses where the server must do real work for each request.",[1433,1481,1483,1486],{"className":1482},[1436],[1438,1484,1485],{},"Is prerendering safe for all pages?",[15,1487,1488],{},"No. Avoid prerendering pages with side effects on load (adding items to carts, logging out, one-time tokens) and pages that are expensive to render. Guard analytics so prerendered pages only count when activated.",[1433,1490,1492,1495],{"className":1491},[1436],[1438,1493,1494],{},"How much do redirects cost?",[15,1496,1497],{},"Each redirect costs at least one round trip, and more if it switches hosts and needs a new connection. On mobile networks, that is typically 100–300ms per hop.",[234,1499,1501],{"id":1500},"topics-in-this-section","Topics in This Section",[242,1503,1504,1510,1516,1522,1528],{},[245,1505,1506,1509],{},[25,1507,1508],{"href":27},"Time to First Byte (TTFB) optimization"," — redirects, connection setup, server time and cold starts.",[245,1511,1512,1515],{},[25,1513,1514],{"href":32},"Compression: Brotli & Zstandard"," — fewer bytes for every text response.",[245,1517,1518,1521],{},[25,1519,1520],{"href":37},"HTTP\u002F2, HTTP\u002F3 & connection management"," — faster connections and better reuse.",[245,1523,1524,1527],{},[25,1525,1526],{"href":42},"Streaming SSR & early flush"," — send HTML progressively.",[245,1529,1530,1533],{},[25,1531,1532],{"href":47},"Speculative loading & prefetching"," — make the next navigation instant.",[234,1535,1537],{"id":1536},"related","Related",[242,1539,1540,1547,1554],{},[245,1541,1542,1546],{},[25,1543,1545],{"href":1544},"\u002Fadvanced-caching-strategies-cdn-architecture\u002F","Advanced caching strategies & CDN architecture"," — caching that removes server time entirely.",[245,1548,1549,1553],{},[25,1550,1552],{"href":1551},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals measurement"," — how TTFB feeds into LCP.",[245,1555,1556,1560],{},[25,1557,1559],{"href":1558},"\u002Frendering-css-performance\u002F","Rendering & CSS performance"," — what happens after the bytes arrive.",[1332,1562,1564],{"type":1563},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"Network & Server Response Optimization: From Request to First Byte and Beyond\",\n  \"description\": \"A production guide to the network half of page speed — server response time, compression, protocols, streaming HTML and speculative loading — measured against TTFB, FCP and LCP.\",\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\u002Fnetwork-protocol-optimization\u002F\"\n  }\n}\n",[1332,1566,1567],{"type":1563},"\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\": \"Network & Server Response Optimization\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fnetwork-protocol-optimization\u002F\"\n    }\n  ]\n}\n",[1332,1569,1570],{"type":1563},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"FAQPage\",\n  \"mainEntity\": [\n    {\n      \"@type\": \"Question\",\n      \"name\": \"What is a good TTFB?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Google considers 800ms or less at the 75th percentile good. For a fast LCP on mobile, aim lower — under about 600ms — because TTFB consumes part of the 2.5-second LCP budget.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Is TTFB a Core Web Vital?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"No, but it is a foundational diagnostic metric. LCP and FCP cannot be faster than TTFB, so slow TTFB caps every loading metric.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Should I use Brotli or Zstandard?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Use Brotli for precompressed static assets, where its higher ratio matters most. For dynamic responses, Brotli at moderate levels and zstd are both good choices; zstd compresses faster at similar ratios. Keep gzip as a fallback.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Does HTTP\u002F3 make my site faster?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Mostly for users on high-latency or lossy networks — mobile users, distant regions. On fast, stable connections the difference is small. Measure by protocol in RUM.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Does streaming SSR help if my CDN caches HTML?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Cached HTML is already fast, so streaming matters most for uncached or personalised responses where the server must do real work for each request.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Is prerendering safe for all pages?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"No. Avoid prerendering pages with side effects on load (adding items to carts, logging out, one-time tokens) and pages that are expensive to render. Guard analytics so prerendered pages only count when activated.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"How much do redirects cost?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Each redirect costs at least one round trip, and more if it switches hosts and needs a new connection. On mobile networks, that is typically 100–300ms per hop.\"\n      }\n    }\n  ]\n}\n",[1572,1573,1574],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}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 pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}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);}html pre.shiki code .sZBmE, html code.shiki .sZBmE{--shiki-default:#024C1A;--shiki-dark:#72F088;--shiki-light:#024C1A}",{"title":729,"searchDepth":742,"depth":742,"links":1576},[1577,1578,1579,1580,1581,1582,1583,1584,1585,1592,1593,1594,1595],{"id":236,"depth":742,"text":237},{"id":363,"depth":742,"text":364},{"id":403,"depth":742,"text":404},{"id":502,"depth":742,"text":503},{"id":522,"depth":742,"text":523},{"id":663,"depth":742,"text":664},{"id":678,"depth":742,"text":679},{"id":685,"depth":742,"text":686},{"id":716,"depth":742,"text":717,"children":1586},[1587,1588,1589,1590,1591],{"id":721,"depth":781,"text":722},{"id":1053,"depth":781,"text":1054},{"id":1074,"depth":781,"text":1075},{"id":1163,"depth":781,"text":1164},{"id":1315,"depth":781,"text":1316},{"id":1376,"depth":742,"text":1377},{"id":1430,"depth":742,"text":1431},{"id":1500,"depth":742,"text":1501},{"id":1536,"depth":742,"text":1537},"A production guide to the network half of page speed — server response time, compression, protocols, streaming HTML and speculative loading — measured against TTFB, FCP and LCP.","md",{"slug":1599,"type":1600,"breadcrumb":1601,"datePublished":1607,"dateModified":1607},"network-protocol-optimization","guide",[1602,1605],{"name":1603,"url":1604},"Home","\u002F",{"name":5,"url":1606},"\u002Fnetwork-protocol-optimization\u002F","2026-10-06",true,"\u002Fnetwork-protocol-optimization",{"title":1611,"description":1612},"Network & Server Response Optimization for Speed","Cut TTFB and transfer time: server timing, redirects, Brotli and Zstandard, HTTP\u002F2 and HTTP\u002F3, streaming SSR, early flush and speculative loading for instant pages.","network-protocol-optimization\u002Findex","ZyvZqEud0YMxNqoH2Pr_bmNpqjplRm3xi7yUGIDXR_c",[1616,1620],{"title":1617,"path":1618,"stem":1619,"children":-1},"Tuning splitChunks cacheGroups for Long-Term Caching","\u002Fjavascript-bundle-optimization-code-splitting\u002Fwebpack-bundle-analysis-techniques\u002Ftuning-splitchunks-cachegroups-for-long-term-caching","javascript-bundle-optimization-code-splitting\u002Fwebpack-bundle-analysis-techniques\u002Ftuning-splitchunks-cachegroups-for-long-term-caching\u002Findex",{"title":1514,"path":1621,"stem":1622,"children":-1},"\u002Fnetwork-protocol-optimization\u002Fcompression-brotli-and-zstandard","network-protocol-optimization\u002Fcompression-brotli-and-zstandard\u002Findex",1791308069636]