<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Paweł Pokrywka's Lab: Performance]]></title><description><![CDATA[Optimizing how systems think, move, and respond.]]></description><link>https://www.pawelpokrywka.com/s/performance</link><image><url>https://substackcdn.com/image/fetch/$s_!gJuv!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe44a7fc6-d5ec-4a99-984a-7a7e0bc80a17_800x800.png</url><title>Paweł Pokrywka&apos;s Lab: Performance</title><link>https://www.pawelpokrywka.com/s/performance</link></image><generator>Substack</generator><lastBuildDate>Fri, 22 May 2026 16:58:21 GMT</lastBuildDate><atom:link href="https://www.pawelpokrywka.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Paweł Pokrywka]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[rss1@pawelpokrywka.com]]></webMaster><itunes:owner><itunes:email><![CDATA[rss1@pawelpokrywka.com]]></itunes:email><itunes:name><![CDATA[Paweł Pokrywka]]></itunes:name></itunes:owner><itunes:author><![CDATA[Paweł Pokrywka]]></itunes:author><googleplay:owner><![CDATA[rss1@pawelpokrywka.com]]></googleplay:owner><googleplay:email><![CDATA[rss1@pawelpokrywka.com]]></googleplay:email><googleplay:author><![CDATA[Paweł Pokrywka]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[My conclusions after using Signed Exchanges on my website for 2 years]]></title><description><![CDATA[Instant page loads for Google visitors using Signed Exchanges&#8212;part 10 (final)]]></description><link>https://www.pawelpokrywka.com/p/my-conclusions-after-using-signed</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/my-conclusions-after-using-signed</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Thu, 09 Oct 2025 20:21:30 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!WEmm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70230089-3e8e-4194-98e4-25bd891f2013_3051x2435.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WEmm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70230089-3e8e-4194-98e4-25bd891f2013_3051x2435.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset image2-full-screen"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WEmm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70230089-3e8e-4194-98e4-25bd891f2013_3051x2435.jpeg 424w, https://substackcdn.com/image/fetch/$s_!WEmm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70230089-3e8e-4194-98e4-25bd891f2013_3051x2435.jpeg 848w, https://substackcdn.com/image/fetch/$s_!WEmm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70230089-3e8e-4194-98e4-25bd891f2013_3051x2435.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!WEmm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70230089-3e8e-4194-98e4-25bd891f2013_3051x2435.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WEmm!,w_5760,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70230089-3e8e-4194-98e4-25bd891f2013_3051x2435.jpeg" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/70230089-3e8e-4194-98e4-25bd891f2013_3051x2435.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;full&quot;,&quot;height&quot;:1162,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2984135,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/158666674?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70230089-3e8e-4194-98e4-25bd891f2013_3051x2435.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-fullscreen" alt="" srcset="https://substackcdn.com/image/fetch/$s_!WEmm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70230089-3e8e-4194-98e4-25bd891f2013_3051x2435.jpeg 424w, https://substackcdn.com/image/fetch/$s_!WEmm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70230089-3e8e-4194-98e4-25bd891f2013_3051x2435.jpeg 848w, https://substackcdn.com/image/fetch/$s_!WEmm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70230089-3e8e-4194-98e4-25bd891f2013_3051x2435.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!WEmm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70230089-3e8e-4194-98e4-25bd891f2013_3051x2435.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The web performance dilemma inspired by <em>The Judgment of Paris </em>(~1606) by Peter Paul Rubens, oil on panel, 89 &#215; 114.5 cm</figcaption></figure></div><blockquote><p><strong>Update (context):</strong> After finishing this article, I learned that <a href="https://community.cloudflare.com/t/amp-and-signed-exchanges-deprecation-october-20th/831238">Cloudflare plans to deprecate Signed Exchanges (SXG)</a>.</p><p>Recently, as I dug deeper into the SXG ecosystem, I suspected this might be coming&#8212;just not this quickly. Still, SXG proved technically solid on my site and delivered meaningful performance gains. My conclusions below reflect two years of hands-on use.</p><p>If you still want SXG, the current path is to self-host&#8212;but the tooling looks abandoned and setup is non-trivial. Also, Google may follow Cloudflare by shutting down the SXG cache and removing SXG support in Chrome.</p></blockquote><p>Signed Exchanges (SXG) is a technology mainly used to improve page load speed for Google-referred users.</p><p>I enabled it for my website in October 2023, resulting in substantial improvements to the Largest Contentful Paint (LCP) metric.</p><p>I couldn&#8217;t find comprehensive, developer-focused documentation, so I set out to write my own <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">SXG implementation guide</a>. I initially planned three posts. I didn&#8217;t expect it to turn into 11 deep-dive articles (10 parts plus one <a href="https://www.pawelpokrywka.com/p/stretching-google-prefetching">extra</a>), 20 interactive <a href="https://www.planujemywesele.pl/sxg-tests/">demonstrations</a>, 4 <a href="https://github.com/pepawel/sxg-status">related</a> <a href="https://github.com/pepawel/page-load-type">open</a>-<a href="https://github.com/pepawel/sxg_checker">source</a> <a href="https://github.com/pepawel/stretching-prefetching">projects</a>, plus detailed <a href="https://www.pawelpokrywka.com/p/google-prefetching-methods-performance-study">performance data</a> and extensive <a href="https://www.pawelpokrywka.com/p/debugging-complex-signed-exchanges-subresource-issue">reverse engineering</a>.</p><p>In this post, I summarize my research and experiences with SXG. Where appropriate, I compare it with other performance-improving technologies used by Google: <a href="https://amp.dev/">Accelerated Mobile Pages</a> (AMP) and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API">Speculation Rules</a> <a href="https://developer.chrome.com/blog/search-speculation-rules">prefetching</a>.</p><h2>SXG advantages</h2><p>SXG enables cross-origin prefetching of entire pages with subresources while maintaining users&#8217; privacy.</p><p>Websites are expected to serve pages in a special format that&#8217;s consumed by so-called <em>link aggregators</em> (such as Google), which perform the actual prefetching, or more specifically, the prefetching is done by browsers as instructed by link aggregators when visited.</p><h4>Page load speed</h4><p>With SXG, an entirely prefetched page displays almost immediately after the user navigates to it, resulting in a great user experience, even on a slow connection (assuming the prefetching is completed before navigation).</p><p>Below, you can see an extreme example featuring going offline and navigating to the prefetched website without issues.</p><blockquote><p>This is a vertical video, so if you are using a mobile device, click <a href="https://www.youtube.com/shorts/BW_Hkthiawg">here</a> to open it in the YouTube app for best experience.</p></blockquote><div id="youtube2-BW_Hkthiawg" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;BW_Hkthiawg&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/BW_Hkthiawg?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p>When compared to similar technologies in use today, only AMP offers a better performance at the cost of severe limitations and trust issues. Speculation Rules prefetching falls short mainly due to the inability to prefetch subresources, but also because it doesn&#8217;t work for returning visitors in most cases.</p><p>Theoretically, if used to its full potential, SXG could allow 75% of my users to experience LCP of under 544 ms.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ZqAA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b2dc879-4bc4-4591-ad46-7556e2bfd410_3250x741.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ZqAA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b2dc879-4bc4-4591-ad46-7556e2bfd410_3250x741.png 424w, https://substackcdn.com/image/fetch/$s_!ZqAA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b2dc879-4bc4-4591-ad46-7556e2bfd410_3250x741.png 848w, https://substackcdn.com/image/fetch/$s_!ZqAA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b2dc879-4bc4-4591-ad46-7556e2bfd410_3250x741.png 1272w, https://substackcdn.com/image/fetch/$s_!ZqAA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b2dc879-4bc4-4591-ad46-7556e2bfd410_3250x741.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ZqAA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b2dc879-4bc4-4591-ad46-7556e2bfd410_3250x741.png" width="1456" height="332" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1b2dc879-4bc4-4591-ad46-7556e2bfd410_3250x741.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:332,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:43796,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/158666674?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b2dc879-4bc4-4591-ad46-7556e2bfd410_3250x741.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ZqAA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b2dc879-4bc4-4591-ad46-7556e2bfd410_3250x741.png 424w, https://substackcdn.com/image/fetch/$s_!ZqAA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b2dc879-4bc4-4591-ad46-7556e2bfd410_3250x741.png 848w, https://substackcdn.com/image/fetch/$s_!ZqAA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b2dc879-4bc4-4591-ad46-7556e2bfd410_3250x741.png 1272w, https://substackcdn.com/image/fetch/$s_!ZqAA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b2dc879-4bc4-4591-ad46-7556e2bfd410_3250x741.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">LCP histogram for vendor index pages prefetched via SXG (with subresources), mobile and desktop combined. The green dashed line marks the 75th percentile.</figcaption></figure></div><p>In practice, I achieved an <strong>average overall</strong> LCP for Google-referred users of around 700 ms. While calculating the exact 75th percentile is complex for reasons discussed in previous posts, I guess it's under 1 second&#8212;still a solid result for a high-traffic, image-rich production site!</p><p>If you want to know more about this topic, I wrote a detailed post on <a href="https://www.pawelpokrywka.com/p/google-prefetching-methods-performance-study">SXG performance measurements</a>.</p><h4>User engagement</h4><p>SXG prefetching showed significant user engagement improvements in my testing, which I attribute to better page load speed.</p><p>I won&#8217;t present a full report here; instead, I'll highlight the key user engagement metrics I measured for my website, focusing on mobile, which constitutes the majority of overall traffic. Your results may be different.</p><p>When compared to loading a page on demand, prefetching a page with subresources using SXG increased views per session and average session duration by 36%. At the same time, it reduced the bounce rate by more than a quarter (27%).</p><p>For comparison, Speculation Rules prefetching improved views per session by 15%, average session duration by 11% and decreased bounce rate by almost 19%.</p><p>These metrics represent best-case scenarios. Real-world impact will be lower due to mixed page load types, though SXG's benefits remain substantial.</p><h4>Security &amp; privacy</h4><h5>AMP doesn&#8217;t look good</h5><p>In the case of AMP, Google controls the entire page to be prerendered (including subresources) and has the power to alter it. The user has to trust Google or avoid using AMP entirely.</p><p>Another problem with AMP is that the URL of the page points to <strong>google.com</strong> instead of the visited page. As users interact with AMP pages, they are effectively taught <em>not to verify the URL</em>. This undermines the efforts of the security experts trying to teach the opposite.</p><blockquote><p>I can see one positive side of having <strong>google.com</strong> in the URL. It&#8217;s like Google honestly saying: we control this website and we may alter it if we want.</p></blockquote><p>A solution is to introduce SXG to the mix. Cloudflare even has a <a href="https://www.cloudflare.com/website-optimization/amp-real-url/">switch</a> for that. But I failed to find any example of an AMP website using it.</p><h5>Speculation Rules prefetching is ok</h5><p>When it comes to the Speculation Rules prefetching feature implemented in Google search results page, it&#8217;s based on a so-called <em>private prefetch proxy</em>. The browser prefetches the page <em>speculated</em> to be navigated to. The prefetching requests go through a proxy to protect the user&#8217;s IP address.</p><p>The proxy uses the HTTP CONNECT request method. Assuming the website uses HTTPS and has a valid TLS certificate, Google can only see encrypted data, so no alterations are possible in practice.</p><blockquote><p>While the IP address of the user is protected, the country information still leaks to the website due to the <a href="https://developer.chrome.com/blog/private-prefetch-proxy#geo-dependent_content_or_services">geolocation feature</a> of the private prefetch proxy. This could potentially affect users connecting from countries that the website doesn't typically see traffic from, though the privacy impact is relatively limited.</p></blockquote><h5>SXG approach</h5><p>SXG uses digital signatures to prevent malicious link aggregators (such as Google) from modifying data served to users. It also preserves the original URL, which is an important UX, security, and trust-building feature.</p><p>From the security standpoint, SXG is a huge improvement over AMP and is minimally better than Speculation Rules prefetching.</p><h4>Impact on the server load</h4><h5>Organic visits</h5><p>Google SXG cache performs the function of a CDN, taking some load from your website.</p><p>You may even completely stop seeing Google-referred traffic from browsers supporting SXG in your app logs, especially for popular pages or if you <a href="https://www.pawelpokrywka.com/p/overall-impact-of-signed-exchanges">boosted your SXG deployment</a>.</p><p>For example, the homepage of my website is mostly handled by Google.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Xvjz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c85e544-8d5f-4c93-b5f3-122ec1e593fa_605x341.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Xvjz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c85e544-8d5f-4c93-b5f3-122ec1e593fa_605x341.png 424w, https://substackcdn.com/image/fetch/$s_!Xvjz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c85e544-8d5f-4c93-b5f3-122ec1e593fa_605x341.png 848w, https://substackcdn.com/image/fetch/$s_!Xvjz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c85e544-8d5f-4c93-b5f3-122ec1e593fa_605x341.png 1272w, https://substackcdn.com/image/fetch/$s_!Xvjz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c85e544-8d5f-4c93-b5f3-122ec1e593fa_605x341.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Xvjz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c85e544-8d5f-4c93-b5f3-122ec1e593fa_605x341.png" width="725" height="408.6363636363636" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5c85e544-8d5f-4c93-b5f3-122ec1e593fa_605x341.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:341,&quot;width&quot;:605,&quot;resizeWidth&quot;:725,&quot;bytes&quot;:17365,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/158666674?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c85e544-8d5f-4c93-b5f3-122ec1e593fa_605x341.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Xvjz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c85e544-8d5f-4c93-b5f3-122ec1e593fa_605x341.png 424w, https://substackcdn.com/image/fetch/$s_!Xvjz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c85e544-8d5f-4c93-b5f3-122ec1e593fa_605x341.png 848w, https://substackcdn.com/image/fetch/$s_!Xvjz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c85e544-8d5f-4c93-b5f3-122ec1e593fa_605x341.png 1272w, https://substackcdn.com/image/fetch/$s_!Xvjz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5c85e544-8d5f-4c93-b5f3-122ec1e593fa_605x341.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">PlanujemyWesele homepage: Google-referred traffic distribution by source for SXG-capable browsers.</figcaption></figure></div><h5>Googlebot traffic</h5><p>On the other hand, the Googlebot traffic increases substantially, and even more if you boost your SXG.</p><p>One reason is the maximum validity period for SXG subresources. Normally, the assets are configured to expire in a year or so, while SXG limits them to 7 days. Googlebot, therefore, has to download them much more often.</p><h5>AMP</h5><p>AMP can also reduce the load on your server. However, it only works on mobile devices, so it won&#8217;t help you with desktop traffic.</p><h5>Speculation Rules prefetching</h5><p>Prefetching using Speculation Rules slightly increases the load of your website, because your infrastructure has to process requests from both actual and potential visitors. There are ways to <a href="https://developer.chrome.com/blog/private-prefetch-proxy">limit prefetching</a> if it causes issues.</p><h2>SXG disadvantages</h2><h4>Some page loads will be slower</h4><p>On my website, properly deployed SXG improved overall performance compared to a non-SXG setup. But some users still experience increased LCP. I minimized this issue by boosting my SXG implementation, but I know it&#8217;s not possible for every website.</p><p>AMP likely has similar trade-offs, though I haven't verified this. Speculation Rules prefetching avoids this issue entirely since it doesn't involve cache and/or custom data formats.</p><h4><strong>Incomplete SXG implementation hurts performance</strong></h4><p>Enabling SXG in Cloudflare without proper configuration will likely hurt your page load speeds.</p><p>This means you need to set sufficiently long expiration times and configure subresources for prefetching&#8212;otherwise, SXG's overhead and interference with Speculation Rules prefetching will make performance worse, not better.</p><p>If you implement SXG, commit to doing it right. A half-hearted implementation is worse than none at all.</p><h4>No respect for battery level and connection limitations</h4><p>Speculation Rules prefetching respects user constraints&#8212;it won't trigger when devices are low on battery or in data-saving mode. This strikes a sensible balance between performance and resource conservation.</p><p>My tests show that neither SXG nor AMP honors these constraints. This is particularly problematic since both techniques consume more data and CPU resources than HTML-only prefetching.</p><h4>Implementation challenges</h4><h5>Server-side personalization is not allowed</h5><p>The primary challenge in implementing SXG is shifting away from server-side personalization.</p><p>The page HTML should remain unchanged, regardless of whether the user is a first-time visitor, logged in, or has items in their cart. Updates to the base HTML should be handled client-side, with the frontend requesting data from the backend.</p><p>I think setting these constraints is valuable because it pays off when you decide to cache HTML, which&#8212;when combined with CDN&#8212;can greatly reduce server load.</p><p>However, this approach may introduce complexity and other development costs. Also, some applications are simply not compatible with these constraints.</p><h5>Framework friendliness</h5><p>Web frameworks have different opinions on where the personalization should happen. For example, Ruby on Rails encourages developers to do most of the work on the server, while Next.js embraces client-side.</p><p>As a person preferring Ruby over JavaScript, I understand the Rails approach to maximize the Ruby part of the app (the server) while minimizing JavaScript (the client). On the contrary, Next.js uses the same language on both sides of the network connection and is heavily reliant on React, a frontend-focused framework.</p><p>When reading the <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">first</a> <a href="https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges">two</a> parts of this series, you could notice that forcing Rails to generate SXG-compatible HTML was a lot of work, while Next.js required only cosmetic changes. On the other hand, Rails has SXG-friendly subresources by default, while adjusting them in Next.js was a real pain involving the use of Cloudflare workers.</p><h5>Insufficient documentation</h5><p>As I write this post, there is no Wikipedia article on SXG. If you search for SXG documentation, you won&#8217;t find many practical, developer-focused resources.</p><p>There is a <a href="https://wicg.github.io/webpackage/draft-yasskin-wpack-use-cases.html">standard draft</a>, a few valuable articles from the <a href="https://web.dev/articles/signed-exchanges">web.dev</a>, <a href="https://developer.chrome.com/blog/optimizing-lcp-using-signed-exchanges/">Chrome</a>, <a href="https://developers.google.com/search/docs/appearance/signed-exchange">Google</a>, and <a href="https://developers.cloudflare.com/speed/optimization/other/signed-exchanges/">Cloudflare</a> blogs and knowledge bases, and technical documentation scattered <a href="https://github.com/WICG/webpackage">across</a> <a href="https://github.com/google/webpackager">SXG-related</a> <a href="https://github.com/google/libsxg">Github</a> <a href="https://github.com/search?q=path%3A%2F%5C.md%24%2F+%22signed+exchanges%22&amp;type=code">repositories</a>. A lot of remaining results point to low-quality, generic blog posts written primarily for SEO purposes.</p><p>No one talks about the real-world issues with subresources that have a major impact on the overall effectiveness of SXG.</p><p>I found several instances of outdated<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> or downright <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms#footnote-1-139127541">incorrect</a> information.</p><p>This series was created to address the above issues.</p><h5>Quirks and bugs</h5><p>One of my posts focuses solely on debugging SXG, while three others are simply detailed bug reports with workarounds. In summary, 40% of this SXG guide deals with bugs and quirks.</p><p>The good news is that most of these issues aren't fundamental to SXG technology itself and could theoretically be addressed by Cloudflare, though we remain dependent on Cloudflare's willingness to implement such fixes.</p><blockquote><p>It's also possible to address most of these challenges by creating a specialized Cloudflare worker that would transform all website traffic, eliminating SXG issues. The code snippets for fixing particular problems are already available in my posts&#8212;they just need to be combined into a single, comprehensive, fire-and-forget solution, a super-fix worker.</p></blockquote><h4>Maintenance and monitoring</h4><p>SXG is easy to break; therefore, it needs constant monitoring and maintenance. Here is the additional work you should do:</p><ul><li><p>Create and maintain automated tests to cover basic problems like setting cookies or other prohibited headers, so that SXG won&#8217;t break when you add new features to your app.</p></li><li><p>Monitor if the critical production pages work at all (I created <a href="https://github.com/pepawel/sxg_checker">sxg-checker</a> for that).</p></li><li><p>Monitor the performance degradation event rate on production, such as the failed prefetching of subresources, fallback client-side redirects, and loading SXG on-demand; the <a href="https://github.com/pepawel/page-load-type">page-load-type</a> library could be useful for detecting these cases; you can report them using the Application Performance Monitoring (APM) solution of your choice.</p></li></ul><h4>Not being new</h4><p>Tech often chases the newest trends. Despite SXG's real advantages and ability to solve current problems, it's been around for a while now. Newer alternatives get more attention, which likely contributes to SXG's limited adoption.</p><h2>Risks of implementing SXG</h2><h4>Stagnation</h4><p>Both the technology and the ecosystem around it are stagnant:</p><ul><li><p>The SXG standard draft looks like it is stuck.</p></li><li><p>Lack of development: the open source projects seem abandoned. When it comes to Google's repositories, commits stopped at the end of 2022 as if the project was killed.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zWsv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb965448a-a02f-4840-bf43-f3ba50807274_1176x979.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zWsv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb965448a-a02f-4840-bf43-f3ba50807274_1176x979.png 424w, https://substackcdn.com/image/fetch/$s_!zWsv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb965448a-a02f-4840-bf43-f3ba50807274_1176x979.png 848w, https://substackcdn.com/image/fetch/$s_!zWsv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb965448a-a02f-4840-bf43-f3ba50807274_1176x979.png 1272w, https://substackcdn.com/image/fetch/$s_!zWsv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb965448a-a02f-4840-bf43-f3ba50807274_1176x979.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zWsv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb965448a-a02f-4840-bf43-f3ba50807274_1176x979.png" width="1176" height="979" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b965448a-a02f-4840-bf43-f3ba50807274_1176x979.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:979,&quot;width&quot;:1176,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:77933,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/158666674?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb965448a-a02f-4840-bf43-f3ba50807274_1176x979.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!zWsv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb965448a-a02f-4840-bf43-f3ba50807274_1176x979.png 424w, https://substackcdn.com/image/fetch/$s_!zWsv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb965448a-a02f-4840-bf43-f3ba50807274_1176x979.png 848w, https://substackcdn.com/image/fetch/$s_!zWsv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb965448a-a02f-4840-bf43-f3ba50807274_1176x979.png 1272w, https://substackcdn.com/image/fetch/$s_!zWsv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb965448a-a02f-4840-bf43-f3ba50807274_1176x979.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div></li><li><p>No new content on blogs and tech sites.</p></li><li><p>Forget about getting your SXG questions answered on Clouflare or Google forums.</p></li></ul><h4>Poor adoption</h4><p>Only Google and Cloudflare embraced SXG.</p><ul><li><p><a href="https://docs.google.com/document/d/1ha00dSGKmjoEh2mRiG8FIA5sJ1KihTuZe-AXX1r8P-8/edit?tab=t.0">Mozilla</a> and <a href="https://brave.com/web-standards-at-brave/6-privacy-sandbox-concerns/#signed-exchange">Brave</a> intentionally don&#8217;t support SXG.</p></li><li><p>Websites supporting SXG are rare. From those I could find, none supported subresources properly, so they would be better off by disabling SXG and using Speculation Rules prefetching instead.</p></li><li><p>Google is currently the only search engine that prefetches content using SXG. This differs significantly from AMP's adoption pattern across the industry.<br>When Google <a href="https://blog.amp.dev/2016/02/24/amping-up-in-google-search/">announced</a> AMP support, Microsoft immediately <a href="https://blogs.bing.com/search/September-2016/bing-app-joins-the-amp-open-source-effort">followed suit</a> the same year and eventually <a href="https://web.archive.org/web/20190504164301/https://blogs.bing.com/Webmaster-Blog/September-2018/Introducing-Bing-AMP-viewer-and-Bing-AMP-cache">built</a> its own AMP cache and viewer within two years<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>.<br>However, despite many years passing since Google introduced SXG, Bing has shown no interest in implementing this technology, leaving Google as the sole adopter among major search engines.</p></li></ul><p>This creates a chicken-and-egg problem: Link aggregators don't use SXG because websites don't implement it, and websites don't implement it because link aggregators (except Google) don't use it.</p><p>Stagnation and poor adoption may lead to abandonment. I believe there is a non-zero risk of Google sunsetting SXG support. The fact that a <a href="https://support.google.com/webmasters/thread/308247718/google-sxg-cache-webpkgcache-com-stopped-working-globally">2-week global outage in 2024</a> went unnoticed by anyone except me&#8212;and wasn't addressed in Google's official communications&#8212;may increase the likelihood of future discontinuation.</p><h4>Artificial Intelligence</h4><p>As more people obtain information directly from large language models without visiting websites, prefetching becomes less critical. Meanwhile, Google&#8212;currently the only search engine supporting SXG&#8212;faces mounting <a href="https://www.cnbc.com/2025/06/10/google-buyouts-search-ads-unit.html">pressure</a> from competitors developing solutions that could make the traditional &#8220;10 blue links&#8221; search model obsolete.</p><p>Technically, I can imagine the chatbot interface prefetching links to sources using SXG. However, considering the adoption rate, it&#8217;s unlikely.</p><h4>Reliance on Cloudflare</h4><p>I haven't tried to self-host an SXG-enabled website. Given that the last commit to the <a href="https://github.com/google/nginx-sxg-module">SXG nginx module</a> was at the end of 2021, I foresee issues with using it with the recent nginx versions.</p><p>Even if the module can be compiled and loaded by nginx, I&#8217;m sure there will be bugs affecting:</p><ul><li><p>functionality, like the issues I discovered in the Cloudflare implementation,</p></li><li><p>security.</p></li></ul><p>Since the code appears abandoned, I would avoid running it in production. The other option, <a href="https://github.com/google/webpackager/blob/main/cmd/webpkgserver/README.md">Web Packager Server</a>, is also unmaintained.</p><p>This makes Cloudflare the only option. In other words, depending on SXG makes you dependent on this company, classic vendor lock-in.</p><h4>Having to trust Big Tech</h4><p>Remember when I praised SXG for not allowing Google to alter the content because it's signed by the publisher? That's good, but there's another problem: Cloudflare holds the SXG signing keys, and if it chooses to, it can alter your website.</p><p>This illustrates the broader issue with Cloudflare. It provides amazing features, but requires you to proxy traffic through its servers. With access to plain text requests and responses, Cloudflare can do anything with them.</p><p>You can self-host your AMP pages, but you have to rely on Google to cache them, with the risk that Google might alter them. You don't need to worry about Google altering your SXG content, but you need to trust Cloudflare to generate it properly. In both cases, you must trust a big tech company.</p><h2>Verdict</h2><h4>It works for me</h4><p>If you ask me if it was worth implementing SXG in my specific case, I would answer: <em>yes</em>. Here is why:</p><ul><li><p>Most of my traffic comes from Google; therefore, making a great first impression on my new users pays off.</p></li><li><p>I implemented SXG before Google deployed Speculation Rules prefetching on the desktop; therefore, my website&#8217;s performance improved significantly compared to its previous state.</p></li><li><p>The improvements on the desktop were probably even greater because of the TTFB Cloudflare issue that SXG masks.</p></li><li><p>SXG was the pretext for implementing edge caching, and at the same time, made it easier. Edge caching improved overall website speed, not only for Google-referrer visits. At the same time, it decreased server load.</p></li><li><p>Even if Google shuts down SXG prefetching or Chrome stops supporting it in the future, I already gained a lot in terms of user engagement and conversions due to being the fastest website in its category.</p></li></ul><p>While it took a lot of work (mostly debugging), SXG worked well for my specific use case.</p><p>In addition, I learned a lot during the process, such as HTTP caching details, Cloudflare workers implementation, and various frontend tricks. At the same time, I sharpened my skills regarding analytics, website performance measurement, and black-box testing.</p><h4>Will it work for you?</h4><p>However, should I recommend it to you if most of your traffic comes from Google? It depends, as you have to balance all the pros and cons I described in this article.</p><p>If, in your specific situation, there are more drawbacks, then stick to Speculation Rules prefetching; it just works.</p><p>Otherwise, if you want to see how low your LCP can be, follow my <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">SXG guide</a>. I hope it helps you avoid my mistakes and significantly speed up your SXG implementation.</p><h2>The changes I&#8217;d like to see</h2><p>Some things could improve the current status quo. Here is my wishlist:</p><ul><li><p>I'm not sure if it's technically feasible, but if Google could use Speculation Rules to prefetch SXG with a fallback to standard prefetching, we could have the best of both worlds. All the negative side effects of SXG would be eliminated and replaced by standard prefetching, resulting in significant performance improvements. Users concerned about battery drain and data usage would see improvements in both!</p></li><li><p>I'd love to see broader adoption across platforms: search engines beyond Google, LLM chatbots, social platforms like Reddit, and tech sites like Hacker News. WordPress supporting SXG out of the box would be particularly impactful given its massive market share.</p></li><li><p>If Cloudflare improved its SXG implementation to be more reliable, this guide would be much shorter.</p></li><li><p>Finally, I'd like to see a well-maintained, open-source self-hosting solution to enable smaller hosting companies and individual developers to implement SXG without relying on Cloudflare.</p></li></ul><h2>Congrats and thanks!</h2><p>You made it to the end of this post, and if you read the previous posts too, you've finished the entire SXG series. I hope you like it, and I'd like to thank you for your time!</p><p>If you think this post might be valuable to someone else, please share it.</p><p>I don't plan to write more about SXG, but if you're into my <a href="https://www.pawelpokrywka.com/about">other interests</a>, subscribe below.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Privacy, security, and performance: original thoughts, research, and code.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Official Google documentation <a href="https://github.com/google/webpackager/blob/main/docs/cache_requirements.md">states</a> that the maximum SXG size is 8 MB, but the actual value is around <a href="https://www.pawelpokrywka.com/p/other-errors-with-signed-exchanges">1044 KB</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Even though AMP has been embraced by Microsoft, I was unable to find any AMP-enabled site that was pre-fetched or even loaded on demand when using Bing search on my mobile device.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Overall impact of Signed Exchanges on page load speed—a data-driven study]]></title><description><![CDATA[Boosting page speed for Google traffic with Signed Exchanges (part 9 of 10)]]></description><link>https://www.pawelpokrywka.com/p/overall-impact-of-signed-exchanges</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/overall-impact-of-signed-exchanges</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Thu, 09 Oct 2025 17:57:15 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!mCG0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa88c796d-a636-4276-b8db-dbdc6d70a469_2816x2112.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mCG0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa88c796d-a636-4276-b8db-dbdc6d70a469_2816x2112.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset image2-full-screen"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mCG0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa88c796d-a636-4276-b8db-dbdc6d70a469_2816x2112.jpeg 424w, https://substackcdn.com/image/fetch/$s_!mCG0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa88c796d-a636-4276-b8db-dbdc6d70a469_2816x2112.jpeg 848w, https://substackcdn.com/image/fetch/$s_!mCG0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa88c796d-a636-4276-b8db-dbdc6d70a469_2816x2112.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!mCG0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa88c796d-a636-4276-b8db-dbdc6d70a469_2816x2112.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mCG0!,w_5760,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa88c796d-a636-4276-b8db-dbdc6d70a469_2816x2112.jpeg" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a88c796d-a636-4276-b8db-dbdc6d70a469_2816x2112.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;full&quot;,&quot;height&quot;:1092,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1259426,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/171386233?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa88c796d-a636-4276-b8db-dbdc6d70a469_2816x2112.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-fullscreen" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mCG0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa88c796d-a636-4276-b8db-dbdc6d70a469_2816x2112.jpeg 424w, https://substackcdn.com/image/fetch/$s_!mCG0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa88c796d-a636-4276-b8db-dbdc6d70a469_2816x2112.jpeg 848w, https://substackcdn.com/image/fetch/$s_!mCG0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa88c796d-a636-4276-b8db-dbdc6d70a469_2816x2112.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!mCG0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa88c796d-a636-4276-b8db-dbdc6d70a469_2816x2112.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Overall impact of Signed Exchanges on page load speed inspired by <em>Jacob Wrestling with the Angel</em> (1865) by Alexandre-Louis Leloir, oil on canvas</figcaption></figure></div><blockquote><p>Context (Oct 2025): After deep work with SXG, I suspected changes might be coming&#8212;just not this quickly. Cloudflare <a href="https://community.cloudflare.com/t/amp-and-signed-exchanges-deprecation-october-20th/831238">announced SXG deprecation</a>, and I observed SXG stop working around Sept 19, 2025.</p><p>If you still want SXG, the current path is to self-host&#8212;but the tooling looks abandoned, and setup is non-trivial. Also, Google may follow Cloudflare by shutting down the SXG cache and removing SXG support in Chrome.</p></blockquote><p>In the previous part, we examined <a href="https://www.pawelpokrywka.com/p/google-prefetching-methods-performance-study">how different page load types affect performance</a> for Google-referred users. The results were surprising: while some methods achieved sub-second load times, others actually degraded performance compared to standard loading.</p><p>But here's what really matters: your website doesn't use just one loading method. In practice, visitors experience a mix of these page load types, with the distribution depending on your technical setup, caching configuration, and which optimizations you've implemented.</p><p>In this post, I'll analyze how this mix affects overall Largest Contentful Paint (LCP) performance using real-world data from my site. My primary goal is to answer a critical question: when we account for all the different ways pages actually load, does Signed Exchanges (SXG) improve or harm overall performance?</p><blockquote><p>This post is part of my series on SXG&#8212;a technology that can make your website load dramatically faster (and sometimes slower) for users coming from Google. If you&#8217;re thinking about implementing it, start <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">here</a>.</p></blockquote><h2>TL;DR</h2><p>For Google-referred users:</p><ul><li><p>Proper SXG implementation can bring the average LCP below 1 second</p></li><li><p>Almost half a second is possible by <em>boosting</em> SXG</p></li><li><p>Poor SXG configuration can make things much worse (+500 ms)</p></li></ul><p>HTML edge caching provides consistent benefits regardless of other optimizations.</p><h2>Your mileage may vary</h2><p>The data and conclusions in this post come from my specific website setup, serving primarily Polish users with particular CDN and caching configurations. While the patterns I observed may apply to similar setups, your results will likely vary based on your infrastructure, user geography, and implementation details.</p><h2>Measuring frequency of page load types</h2><p>The measurement data I collected allowed me to compare the frequency of page load types in different configurations. I extended the spreadsheet from the <a href="https://www.pawelpokrywka.com/p/google-prefetching-methods-performance-study">previous post</a> to include frequency data and overall estimations. You can download it below.</p><div class="file-embed-wrapper" data-component-name="FileToDOM"><div class="file-embed-container-reader"><div class="file-embed-container-top"><image class="file-embed-thumbnail-default" src="https://substackcdn.com/image/fetch/$s_!0Cy0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Fimg%2Fattachment_icon.svg"></image><div class="file-embed-details"><div class="file-embed-details-h1">LCP analysis of page load types with overall impact</div><div class="file-embed-details-h2">44KB &#8729; XLSX file</div></div><a class="file-embed-button wide" href="https://www.pawelpokrywka.com/api/v1/file/3a3e6e9f-6350-4d12-b868-8d9df074801c.xlsx"><span class="file-embed-button-text">Download</span></a></div><a class="file-embed-button narrow" href="https://www.pawelpokrywka.com/api/v1/file/3a3e6e9f-6350-4d12-b868-8d9df074801c.xlsx"><span class="file-embed-button-text">Download</span></a></div></div><p>For more information on the data collection methodology, please refer to my <a href="https://www.pawelpokrywka.com/p/google-prefetching-methods-performance-study">previous</a> <a href="https://www.pawelpokrywka.com/p/different-methods-of-prefetching">posts</a>.</p><p>Let me first examine how different configurations performed.</p><h4>Standard setup with edge cache</h4><p>For a short period, I disabled SXG for testing purposes. This way, Google-referred users visited my website using only the following methods:</p><ul><li><p>Speculation Rules Prefetch</p></li><li><p>Edge Cache Load</p></li><li><p>Server Load with Early Hints</p></li><li><p>Server Load</p></li></ul><p>The chart below visualizes the proportions of page load types.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eYx7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F002a8f64-1fb0-466a-8351-d2677db0ef2b_597x544.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eYx7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F002a8f64-1fb0-466a-8351-d2677db0ef2b_597x544.png 424w, https://substackcdn.com/image/fetch/$s_!eYx7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F002a8f64-1fb0-466a-8351-d2677db0ef2b_597x544.png 848w, https://substackcdn.com/image/fetch/$s_!eYx7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F002a8f64-1fb0-466a-8351-d2677db0ef2b_597x544.png 1272w, https://substackcdn.com/image/fetch/$s_!eYx7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F002a8f64-1fb0-466a-8351-d2677db0ef2b_597x544.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eYx7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F002a8f64-1fb0-466a-8351-d2677db0ef2b_597x544.png" width="597" height="544" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/002a8f64-1fb0-466a-8351-d2677db0ef2b_597x544.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:544,&quot;width&quot;:597,&quot;resizeWidth&quot;:597,&quot;bytes&quot;:19661,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/171386233?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F002a8f64-1fb0-466a-8351-d2677db0ef2b_597x544.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!eYx7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F002a8f64-1fb0-466a-8351-d2677db0ef2b_597x544.png 424w, https://substackcdn.com/image/fetch/$s_!eYx7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F002a8f64-1fb0-466a-8351-d2677db0ef2b_597x544.png 848w, https://substackcdn.com/image/fetch/$s_!eYx7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F002a8f64-1fb0-466a-8351-d2677db0ef2b_597x544.png 1272w, https://substackcdn.com/image/fetch/$s_!eYx7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F002a8f64-1fb0-466a-8351-d2677db0ef2b_597x544.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Frequency of page load types for a standard configuration with edge cache.</figcaption></figure></div><p>My observations:</p><ul><li><p>Pages were prefetched using Speculation Rules twice as often on desktop (61%) as on mobile (29%). The explanation is simple: in addition to prefetching the first 2 results, the desktop uses prefetching on hover.</p></li><li><p>Most of the on-demand page loads use Cloudflare edge cache, ensuring fast load speed. This confirms that my website's cache utilization is optimal.</p></li><li><p>Early Hints are almost non-existent in the breakdown. This suggests that when you use HTML caching, Early Hints provides minimal performance benefits, as discussed in the <a href="https://www.pawelpokrywka.com/p/google-prefetching-methods-performance-study">previous part</a>. For this reason, as well as for more readable charts, I omit Early Hints in further discussions.</p></li></ul><h4>Standard SXG setup with edge cache</h4><p>Most of the time, my website utilizes SXG, as well as other performance-improving technologies, including edge cache and Speculation Rules prefetching.</p><p>I reused the data I measured for performance analysis to calculate frequencies of page load types. Here they are, followed by my comments.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!wZvD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1b5deee-1cba-43e7-8cc6-e92fd37ce1df_731x684.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wZvD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1b5deee-1cba-43e7-8cc6-e92fd37ce1df_731x684.png 424w, https://substackcdn.com/image/fetch/$s_!wZvD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1b5deee-1cba-43e7-8cc6-e92fd37ce1df_731x684.png 848w, https://substackcdn.com/image/fetch/$s_!wZvD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1b5deee-1cba-43e7-8cc6-e92fd37ce1df_731x684.png 1272w, https://substackcdn.com/image/fetch/$s_!wZvD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1b5deee-1cba-43e7-8cc6-e92fd37ce1df_731x684.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wZvD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1b5deee-1cba-43e7-8cc6-e92fd37ce1df_731x684.png" width="731" height="684" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b1b5deee-1cba-43e7-8cc6-e92fd37ce1df_731x684.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:684,&quot;width&quot;:731,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:29490,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/171386233?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1b5deee-1cba-43e7-8cc6-e92fd37ce1df_731x684.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!wZvD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1b5deee-1cba-43e7-8cc6-e92fd37ce1df_731x684.png 424w, https://substackcdn.com/image/fetch/$s_!wZvD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1b5deee-1cba-43e7-8cc6-e92fd37ce1df_731x684.png 848w, https://substackcdn.com/image/fetch/$s_!wZvD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1b5deee-1cba-43e7-8cc6-e92fd37ce1df_731x684.png 1272w, https://substackcdn.com/image/fetch/$s_!wZvD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1b5deee-1cba-43e7-8cc6-e92fd37ce1df_731x684.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Frequency of page load types for a standard SXG configuration with edge cache.</figcaption></figure></div><h5>SXG was used for most of the visits</h5><p>All SXG-related page views accounted for 78-87% of total traffic, depending on the device category. In other words, only 13-22% Google visits were normal or prefetched using Speculation Rules.</p><p>It's worth noting that SXG was used more frequently on desktop than on mobile by 9 percentage points, possibly due to different user behavior patterns. However, if you have a better explanation, leave a comment below.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/p/overall-impact-of-signed-exchanges/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.pawelpokrywka.com/p/overall-impact-of-signed-exchanges/comments"><span>Leave a comment</span></a></p><h5>Half of the visits used prefetching</h5><p>Prefetched pages accounted for 46-57%, making about half of the visits better than normal loads.</p><p>Fully prefetched page loads (the best possible experience) covered 40-46% of traffic!</p><h5>Speculation Rules prefetching was almost eradicated</h5><p>For SXG-enabled websites, Google prioritizes this technology. In other words, SXG cannibalizes Speculation Rules prefetching.</p><p>As a result, Speculation Rules prefetching was used for only 2% of mobile and 8% of desktop traffic.</p><h5>Performance degradation caused by SXG affected many users</h5><p>13% of mobile page views used SXG On-Demand Load, which degraded performance compared to normal load. As I explained in the <a href="https://www.pawelpokrywka.com/p/google-prefetching-methods-performance-study">previous part</a>, it didn&#8217;t affect the desktop in my setup.</p><p>SXG Redirect impacted 21% of mobile visits and 24% of desktop visits.</p><p>If we sum the above numbers for mobile, we end up with <strong>1 in every 3 users experiencing worse performance!</strong></p><h4>Boosted SXG setup with edge cache</h4><p>After seeing the results, I began to wonder how to improve the overall performance. Increasing the share of fully prefetched pages and decreasing the share of SXG fallback redirects would probably help.</p><p>One way to achieve that is to increase the expiration times of pages. If a given page is kept longer in the SXG cache, then it&#8217;s more probable someone will prefetch it.</p><p>However, I decided to keep my current 24-hour expiration time. Additionally, I built a system that continuously and actively feeds Google SXG cache with a large set of pages&#8212;effectively <em>boosting</em> its utilization.</p><p>This drastically improved the mix of page load types as visualized on the chart below:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!gwR3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd18099a1-35a2-49ac-a440-5a04b712d29e_733x682.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!gwR3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd18099a1-35a2-49ac-a440-5a04b712d29e_733x682.png 424w, https://substackcdn.com/image/fetch/$s_!gwR3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd18099a1-35a2-49ac-a440-5a04b712d29e_733x682.png 848w, https://substackcdn.com/image/fetch/$s_!gwR3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd18099a1-35a2-49ac-a440-5a04b712d29e_733x682.png 1272w, https://substackcdn.com/image/fetch/$s_!gwR3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd18099a1-35a2-49ac-a440-5a04b712d29e_733x682.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!gwR3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd18099a1-35a2-49ac-a440-5a04b712d29e_733x682.png" width="733" height="682" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d18099a1-35a2-49ac-a440-5a04b712d29e_733x682.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:682,&quot;width&quot;:733,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:29530,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/171386233?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd18099a1-35a2-49ac-a440-5a04b712d29e_733x682.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!gwR3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd18099a1-35a2-49ac-a440-5a04b712d29e_733x682.png 424w, https://substackcdn.com/image/fetch/$s_!gwR3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd18099a1-35a2-49ac-a440-5a04b712d29e_733x682.png 848w, https://substackcdn.com/image/fetch/$s_!gwR3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd18099a1-35a2-49ac-a440-5a04b712d29e_733x682.png 1272w, https://substackcdn.com/image/fetch/$s_!gwR3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd18099a1-35a2-49ac-a440-5a04b712d29e_733x682.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Frequency of page load types for a boosted SXG configuration with edge cache.</figcaption></figure></div><p>It almost eliminated SXG fallback redirects by bringing it down to less than 2%.</p><p>At the same time, the ratio of prefetched pages increased to 70-73%, while the fully prefetched loads took 63-64%.</p><h2>Overall LCP</h2><p>We have the frequencies (as presented above) and average LCP for each page load type (on the chart below, discussed in detail in the previous post).</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0l1R!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F048768a2-1f37-4ff6-bf17-c49ca1f36218_1069x394.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0l1R!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F048768a2-1f37-4ff6-bf17-c49ca1f36218_1069x394.png 424w, https://substackcdn.com/image/fetch/$s_!0l1R!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F048768a2-1f37-4ff6-bf17-c49ca1f36218_1069x394.png 848w, https://substackcdn.com/image/fetch/$s_!0l1R!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F048768a2-1f37-4ff6-bf17-c49ca1f36218_1069x394.png 1272w, https://substackcdn.com/image/fetch/$s_!0l1R!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F048768a2-1f37-4ff6-bf17-c49ca1f36218_1069x394.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0l1R!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F048768a2-1f37-4ff6-bf17-c49ca1f36218_1069x394.png" width="1069" height="394" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/048768a2-1f37-4ff6-bf17-c49ca1f36218_1069x394.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:394,&quot;width&quot;:1069,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:34194,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/171386233?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F048768a2-1f37-4ff6-bf17-c49ca1f36218_1069x394.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0l1R!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F048768a2-1f37-4ff6-bf17-c49ca1f36218_1069x394.png 424w, https://substackcdn.com/image/fetch/$s_!0l1R!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F048768a2-1f37-4ff6-bf17-c49ca1f36218_1069x394.png 848w, https://substackcdn.com/image/fetch/$s_!0l1R!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F048768a2-1f37-4ff6-bf17-c49ca1f36218_1069x394.png 1272w, https://substackcdn.com/image/fetch/$s_!0l1R!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F048768a2-1f37-4ff6-bf17-c49ca1f36218_1069x394.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Average LCP comparison between page load types. The less, the better. Some values were estimated as stated in the labels.</figcaption></figure></div><p>These data allow me to calculate the overall average LCP for Google-referred visits for each configuration. It&#8217;s simply a weighted average. This gives us the real-world performance impact, accounting for how often each loading method actually occurs.</p><p>However, before doing so, let&#8217;s summarize the configurations:</p><ul><li><p><strong>Standard + edge cache</strong>: The only improvement over the default (normal loads and prefetching with Speculation Rules) is edge cache for HTML responses</p></li><li><p><strong>Standard + edge cache + SXG</strong>: As above, but with properly implemented SXG.</p></li><li><p><strong>Standard + edge cache + boosted SXG</strong>: Same, but with a system boosting SXG cache utilization.</p></li></ul><h4>Other configurations</h4><p>In addition, the measured data for the three configurations above allow us to simulate different configurations. We can do this by replacing some of the page load types with others for a given base configuration, creating a new configuration.</p><p>This way, we could simulate how my website would perform in more common setups, such as without edge cache for HTML, by replacing <em>Edge Cache Load</em> with <em>Server Load</em> and calculating the average LCP.</p><p>The other simulation could show us what the LCP would look like if we went back in time to the days before prefetching. We could also simulate poor SXG implementations to see how they impact the average LCP.</p><p>Here are the 4 additional configurations I simulated:</p><ul><li><p><strong>Good ol' days (no prefetching, no edge cache)</strong>: just plain old <em>Server Load</em> for everything</p></li><li><p><strong>Standard</strong>: the way most websites work by allowing Google to prefetch pages using Speculation Rules and not bothering with edge cache for HTML</p></li><li><p><strong>Standard + poor SXG (0% SXG cache utilization)</strong>: as above, but with SXG enabled and at the same time crippled by too short expiration times, causing the Google SXG cache to be empty most of the time</p></li><li><p><strong>Standard + poor SXG (no subresources)</strong>: same as <em>Standard</em>, but with partial SXG implementation, working properly only for HTML documents and not for subresources</p></li></ul><h4>Performance results of different configurations</h4><p>Here are the overall, calculated LCP values for my website.</p><p>Keep in mind these are averages, not 75th percentiles. The 75th percentiles would likely be higher, but I won't attempt to estimate them due to the potential for significant error, at least for some of the configurations.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6wSz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73dafdf4-1348-404d-8bf9-f8f86c3989e0_1103x380.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6wSz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73dafdf4-1348-404d-8bf9-f8f86c3989e0_1103x380.png 424w, https://substackcdn.com/image/fetch/$s_!6wSz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73dafdf4-1348-404d-8bf9-f8f86c3989e0_1103x380.png 848w, https://substackcdn.com/image/fetch/$s_!6wSz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73dafdf4-1348-404d-8bf9-f8f86c3989e0_1103x380.png 1272w, https://substackcdn.com/image/fetch/$s_!6wSz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73dafdf4-1348-404d-8bf9-f8f86c3989e0_1103x380.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6wSz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73dafdf4-1348-404d-8bf9-f8f86c3989e0_1103x380.png" width="1103" height="380" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/73dafdf4-1348-404d-8bf9-f8f86c3989e0_1103x380.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:380,&quot;width&quot;:1103,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:33474,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/171386233?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73dafdf4-1348-404d-8bf9-f8f86c3989e0_1103x380.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6wSz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73dafdf4-1348-404d-8bf9-f8f86c3989e0_1103x380.png 424w, https://substackcdn.com/image/fetch/$s_!6wSz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73dafdf4-1348-404d-8bf9-f8f86c3989e0_1103x380.png 848w, https://substackcdn.com/image/fetch/$s_!6wSz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73dafdf4-1348-404d-8bf9-f8f86c3989e0_1103x380.png 1272w, https://substackcdn.com/image/fetch/$s_!6wSz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F73dafdf4-1348-404d-8bf9-f8f86c3989e0_1103x380.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Average, overall LCP comparison between website configurations. The less, the better. Some SXG-related LCP components were estimated as explained.</figcaption></figure></div><h5>Proper SXG implementation improves the overall performance</h5><p>Despite side effects, the overall impact of SXG on average LCP is positive. Compared to the standard configuration (Speculation Rules, no edge cache):</p><ul><li><p><strong>Mobile</strong>: -196 ms</p></li><li><p><strong>Desktop</strong>: -231 ms</p></li></ul><h5>Boosted SXG has superior performance, but it&#8217;s not for everyone</h5><p>Eliminating SXG side effects and improving cache hit rate paid off. The average LCP dropped significantly when compared to the standard configuration:</p><ul><li><p><strong>Mobile</strong>: -458 ms</p></li><li><p><strong>Desktop</strong>:  -432 ms</p></li></ul><p>The result: pages display in ~700 ms for an average user coming from Google. That&#8217;s worth celebrating!</p><p>It&#8217;s worth noting that this solution makes sense as long as the number of pages you want to push to Google SXG cache is small enough. I don&#8217;t know the limits of this cache, but I imagine that for larger websites, they may eventually be reached.</p><h5>Forgetting about SXG subresources is a mistake</h5><p>The simulated configuration with disabled prefetching of subresources performed suboptimally. In comparison with the standard setup, the average LCP increased:</p><ul><li><p><strong>Mobile</strong>: +100 ms</p></li><li><p><strong>Desktop</strong>:  +64 ms</p></li></ul><p>While not terrible, this raises the question: <em>What's the purpose of implementing SXG if the results are worse, even slightly?</em></p><p>If you decide to use SXG, it&#8217;s critical to implement subresource prefetching. Otherwise, instead of improving your website's overall performance, you will degrade it due to SXG side effects. If you use SXG only to prefetch HTML, Speculation Rules prefetching will do this job much better.</p><p>But there is a mistake that will cost you a lot more.</p><h5>Make sure you set proper expiration times for SXG</h5><p>The simulated scenario with 0% SXG cache utilization, which could be caused in real life by setting too short expiration times for HTML, caused substantial performance degradation:</p><ul><li><p><strong>Mobile</strong>: +479 ms</p></li><li><p><strong>Desktop</strong>:  +602 ms</p></li></ul><p>This is a result of a latency added by SXG fallback redirects.</p><p>If long expiration times are problematic in your use case, I can see only 3 solutions:</p><ol><li><p>Using a long expiration time for a page, combined with fetching fresh critical parts client-side.</p></li><li><p>Using short expiration times and implementing a system to boost the SXG cache utilization aggressively (may not scale well, as mentioned above).</p></li><li><p>Forgetting about SXG, unfortunately.</p></li></ol><p>Otherwise, your overall LCP will suffer.</p><h5>Edge cache is a cherry on top</h5><p>Edge caching improved performance, no matter if used alone or in combination with other performance optimization techniques. Compared to the standard configuration, the user experience was slightly better:</p><ul><li><p><strong>Mobile</strong>: -80 ms</p></li><li><p><strong>Desktop</strong>:  -99 ms</p></li></ul><p>If the website has properly implemented SXG, enabling edge caching is a formality and an almost free ~100 ms LCP improvement.</p><h5>Speculation Rules prefetching is positive</h5><p>In the remaining setup, I simulated a scenario without prefetching. The results were worse than the standard configuration:</p><ul><li><p><strong>Mobile</strong>: +71 ms</p></li><li><p><strong>Desktop</strong>:  +384 ms</p></li></ul><p>While the mobile improvement was slight, the desktop LCP decreased significantly because prefetching compensated for the Cloudflare TTFB issue I mentioned in the <a href="https://www.pawelpokrywka.com/p/google-prefetching-methods-performance-study">previous post</a>.</p><p>That&#8217;s a good result given that it comes free. Thanks, Google!</p><h2>Summary</h2><p>I presented the overall impact of SXG on website performance, using my website as an example. I believe some of the observations I share in this case study should apply to other websites as well, but your mileage may vary. If you're considering implementing SXG, I hope I made it slightly easier for you to decide.</p><p>In the next and final part of the series, I will share my <a href="https://www.pawelpokrywka.com/p/my-conclusions-after-using-signed">conclusions and thoughts on the SXG technology</a>.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;4810f679-4f00-41c2-a892-f242c45132da&quot;,&quot;caption&quot;:&quot;Update (context): After finishing this article, I learned that Cloudflare plans to deprecate Signed Exchanges (SXG).&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;My conclusions after using Signed Exchanges on my website for 2 years&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:41077879,&quot;name&quot;:&quot;Pawe&#322; Pokrywka&quot;,&quot;bio&quot;:&quot;I write about security, privacy, and complex systems&#8212;exploring them from the messy middle ground between engineering and research.&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1c35edc2-abb7-4761-8eb4-f379f25e519a_800x800.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-10-09T20:21:30.934Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!WEmm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F70230089-3e8e-4194-98e4-25bd891f2013_3051x2435.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.pawelpokrywka.com/p/my-conclusions-after-using-signed&quot;,&quot;section_name&quot;:&quot;Performance&quot;,&quot;video_upload_id&quot;:null,&quot;id&quot;:158666674,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:0,&quot;comment_count&quot;:0,&quot;publication_id&quot;:400540,&quot;publication_name&quot;:&quot;Pawe&#322; Pokrywka's Lab&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!gJuv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe44a7fc6-d5ec-4a99-984a-7a7e0bc80a17_800x800.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div>]]></content:encoded></item><item><title><![CDATA[How fast do websites load from Google Search? Comparing various prefetching and on-demand load methods.]]></title><description><![CDATA[Optimizing page load performance for Google-referred users with Signed Exchanges (part 8 of 10)]]></description><link>https://www.pawelpokrywka.com/p/google-prefetching-methods-performance-study</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/google-prefetching-methods-performance-study</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Sat, 13 Sep 2025 11:48:50 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!mSSi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa59e2c0c-83c7-4a90-94eb-3aa1bcb9e7ad_2560x1538.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mSSi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa59e2c0c-83c7-4a90-94eb-3aa1bcb9e7ad_2560x1538.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset image2-full-screen"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mSSi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa59e2c0c-83c7-4a90-94eb-3aa1bcb9e7ad_2560x1538.jpeg 424w, https://substackcdn.com/image/fetch/$s_!mSSi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa59e2c0c-83c7-4a90-94eb-3aa1bcb9e7ad_2560x1538.jpeg 848w, https://substackcdn.com/image/fetch/$s_!mSSi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa59e2c0c-83c7-4a90-94eb-3aa1bcb9e7ad_2560x1538.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!mSSi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa59e2c0c-83c7-4a90-94eb-3aa1bcb9e7ad_2560x1538.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mSSi!,w_5760,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa59e2c0c-83c7-4a90-94eb-3aa1bcb9e7ad_2560x1538.jpeg" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a59e2c0c-83c7-4a90-94eb-3aa1bcb9e7ad_2560x1538.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;full&quot;,&quot;height&quot;:875,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:814303,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa59e2c0c-83c7-4a90-94eb-3aa1bcb9e7ad_2560x1538.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-fullscreen" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mSSi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa59e2c0c-83c7-4a90-94eb-3aa1bcb9e7ad_2560x1538.jpeg 424w, https://substackcdn.com/image/fetch/$s_!mSSi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa59e2c0c-83c7-4a90-94eb-3aa1bcb9e7ad_2560x1538.jpeg 848w, https://substackcdn.com/image/fetch/$s_!mSSi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa59e2c0c-83c7-4a90-94eb-3aa1bcb9e7ad_2560x1538.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!mSSi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa59e2c0c-83c7-4a90-94eb-3aa1bcb9e7ad_2560x1538.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Web page loading process inspired by <em>The Old Stagecoach</em> (1871) by Eastman Johnson, oil on canvas, 92 &#215; 153 cm</figcaption></figure></div><blockquote><p>Context (Oct 2025): After deep work with SXG, I suspected changes might be coming&#8212;just not this quickly. Cloudflare <a href="https://community.cloudflare.com/t/amp-and-signed-exchanges-deprecation-october-20th/831238">announced SXG deprecation</a>, and I observed SXG stop working around Sept 19, 2025.</p><p>If you still want SXG, the current path is to self-host&#8212;but the tooling looks abandoned, and setup is non-trivial. Also, Google may follow Cloudflare by shutting down the SXG cache and removing SXG support in Chrome.</p></blockquote><p>In the previous part, we saw that a <a href="https://www.pawelpokrywka.com/p/different-methods-of-prefetching">website can load in 15 different ways</a> when visited from the Google results page and how to measure the performance impact of each load type.</p><p>In this post, I will share the results of my website's performance measurement, along with my comments.</p><p>I assume you are familiar with the differences between various page load types. If you need a refresher, please read the <a href="https://www.pawelpokrywka.com/p/different-methods-of-prefetching">previous post</a>.</p><blockquote><p>This post is part of a series on Signed Exchanges (SXG). To assess SXG&#8217;s impact, I measured the performance of different page load types. This article is based on my research and summarizes my findings.</p><p>SXG is a technology to make your website load faster for Google-referred users. If you want to implement it on your website, start <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">here</a>.</p></blockquote><h2>TL;DR</h2><p>For the impatient, here are the results showing the Largest Contentful Paint (LCP) 75th percentiles I measured (or estimated as stated in the labels and explained later in the text).</p><p>I show that it&#8217;s possible to go below half a second, but SXG side effects may worsen the experience for some users. HTML-only prefetching improved the performance, but sometimes only slightly.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aFi6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe54efc0c-dd3f-4330-87fd-781325cb7035_1161x387.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aFi6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe54efc0c-dd3f-4330-87fd-781325cb7035_1161x387.png 424w, https://substackcdn.com/image/fetch/$s_!aFi6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe54efc0c-dd3f-4330-87fd-781325cb7035_1161x387.png 848w, https://substackcdn.com/image/fetch/$s_!aFi6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe54efc0c-dd3f-4330-87fd-781325cb7035_1161x387.png 1272w, https://substackcdn.com/image/fetch/$s_!aFi6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe54efc0c-dd3f-4330-87fd-781325cb7035_1161x387.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aFi6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe54efc0c-dd3f-4330-87fd-781325cb7035_1161x387.png" width="1161" height="387" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e54efc0c-dd3f-4330-87fd-781325cb7035_1161x387.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:387,&quot;width&quot;:1161,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:35200,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe54efc0c-dd3f-4330-87fd-781325cb7035_1161x387.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!aFi6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe54efc0c-dd3f-4330-87fd-781325cb7035_1161x387.png 424w, https://substackcdn.com/image/fetch/$s_!aFi6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe54efc0c-dd3f-4330-87fd-781325cb7035_1161x387.png 848w, https://substackcdn.com/image/fetch/$s_!aFi6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe54efc0c-dd3f-4330-87fd-781325cb7035_1161x387.png 1272w, https://substackcdn.com/image/fetch/$s_!aFi6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe54efc0c-dd3f-4330-87fd-781325cb7035_1161x387.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">P75 LCP comparison between page load types. The less, the better. Some values were estimated as stated in the labels.</figcaption></figure></div><h2>Your mileage may vary</h2><p>These results are based on real user data from my specific website, measured from Polish users. Your results may vary significantly based on your website's architecture, user geography, CDN configuration, and other factors. The patterns I observed, particularly the desktop TTFB issues, may be unique to my setup.</p><h2>Methodology</h2><h4>Measured pages</h4><p>I share my findings focusing on a specific section of my website: the vendor index page (similar to a product index page in e-commerce). While I measured other sections as well, including them here would add length and complexity without providing significant additional value.</p><p>The vendor index page receives significant traffic from Google and has strong performance metrics, making it an ideal candidate for comparison testing.</p><h4>Chosen page load types</h4><p>The results include all the page load types I identified in the <a href="https://www.pawelpokrywka.com/p/different-methods-of-prefetching">previous post</a>, except for types related to:</p><ul><li><p><strong>Accelerated Mobile Pages (AMP)</strong>, as my website doesn&#8217;t use it</p></li><li><p><strong>Google Ads</strong> (no ad campaigns for the measured section of the website at the moment of data collection)</p></li><li><p><strong>Early Hints</strong>, because my website uses HTML edge caching (more on this later)</p></li></ul><h4>Data collection conditions</h4><p>I collected data only from visits that met the following conditions. For the explanation of why these specific conditions were necessary, see my <a href="https://www.pawelpokrywka.com/p/different-methods-of-prefetching">previous post</a>.</p><ul><li><p>It&#8217;s a Google-referred visit</p></li><li><p>The user visited the website for the first time (checked with a local storage marker item)</p></li><li><p>The page has been opened in an existing tab</p></li><li><p>The browser supported SXG</p></li></ul><p>All of the above data was sent to DebugBear, a monitoring platform.</p><h4>Filters applied to collected data</h4><p>Then I used its filtering functionality to include only:</p><ul><li><p><strong>Users visiting the website from Poland</strong>, as Polish users are my target population, so I wanted to understand how they experience website performance.<br>Additionally, filtering by location helps exclude bots that typically visit my website from other countries.</p></li><li><p><strong>Visits not using cached assets</strong>, as the local storage check (described above) did not always work properly.</p></li><li><p><strong>Visits with Time To First Byte (TTFB) equaling zero for load types not involving prefetching</strong>. It&#8217;s practically impossible to receive the first byte of the response below 1 millisecond; therefore, I treated those samples as invalid.</p></li><li><p><strong>Page load types that can be reliably measured</strong> by excluding SXG redirects. I'll explain this exclusion later.</p></li></ul><p>After the above filtering, I was left with over 14k data points.</p><h4>Obtained performance metrics</h4><p>I segmented the data by page load type and device category to generate LCP histograms and calculate 75th percentiles and averages for each combination.</p><p>For calculating averages, I removed outliers by excluding visits with LCP over 5 seconds.</p><p>I rely on 75th percentiles by default. When using averages, I always explicitly mention it.</p><h4>Estimations</h4><p>I estimated LCP for page load types that use SXG redirects:</p><ul><li><p>When estimating averages, I added a bias correction&#8212;which I determined was necessary for accuracy&#8212;to the reference average.</p></li><li><p>For estimating 75th percentiles, I observed a correlation between percentiles and averages. I then leveraged this relationship to calculate missing percentiles from the available averages.<br>Note that the estimated percentiles were derived from estimated averages, creating a dependency chain in the calculations.</p></li></ul><p>I&#8217;ll provide more details later in the text.</p><h4>Spreadsheet with data</h4><p>The spreadsheet below contains the LCP data I collected from DebugBear, along with comparison charts. It&#8217;s composed of 2 worksheets for:</p><ul><li><p>75th percentiles</p></li><li><p>averages</p></li></ul><p>Each worksheet contains a configuration section where you can experiment with the estimation parameters to see how the results change.</p><div class="file-embed-wrapper" data-component-name="FileToDOM"><div class="file-embed-container-reader"><div class="file-embed-container-top"><image class="file-embed-thumbnail-default" src="https://substackcdn.com/image/fetch/$s_!0Cy0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack.com%2Fimg%2Fattachment_icon.svg"></image><div class="file-embed-details"><div class="file-embed-details-h1">LCP analysis of page load types</div><div class="file-embed-details-h2">15.7KB &#8729; XLSX file</div></div><a class="file-embed-button wide" href="https://www.pawelpokrywka.com/api/v1/file/211ea60a-1de5-4992-b114-b301c3afe2bc.xlsx"><span class="file-embed-button-text">Download</span></a></div><a class="file-embed-button narrow" href="https://www.pawelpokrywka.com/api/v1/file/211ea60a-1de5-4992-b114-b301c3afe2bc.xlsx"><span class="file-embed-button-text">Download</span></a></div></div><h4>Disclaimer</h4><p>I&#8217;m not a data scientist, but I did my best to ensure the data is reliable and the conclusions sound.</p><h2>LCP results</h2><p>Below, you can see the comparison of the speed (LCP) of page load types. It&#8217;s the same chart as the one at the beginning of this post. I put it here, so you can reference it easily while reading my observations.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!N7AW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46d3aee9-7987-4e2a-9ee1-45d5b5762392_1161x387.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!N7AW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46d3aee9-7987-4e2a-9ee1-45d5b5762392_1161x387.png 424w, https://substackcdn.com/image/fetch/$s_!N7AW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46d3aee9-7987-4e2a-9ee1-45d5b5762392_1161x387.png 848w, https://substackcdn.com/image/fetch/$s_!N7AW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46d3aee9-7987-4e2a-9ee1-45d5b5762392_1161x387.png 1272w, https://substackcdn.com/image/fetch/$s_!N7AW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46d3aee9-7987-4e2a-9ee1-45d5b5762392_1161x387.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!N7AW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46d3aee9-7987-4e2a-9ee1-45d5b5762392_1161x387.png" width="1161" height="387" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/46d3aee9-7987-4e2a-9ee1-45d5b5762392_1161x387.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:387,&quot;width&quot;:1161,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:35200,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46d3aee9-7987-4e2a-9ee1-45d5b5762392_1161x387.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!N7AW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46d3aee9-7987-4e2a-9ee1-45d5b5762392_1161x387.png 424w, https://substackcdn.com/image/fetch/$s_!N7AW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46d3aee9-7987-4e2a-9ee1-45d5b5762392_1161x387.png 848w, https://substackcdn.com/image/fetch/$s_!N7AW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46d3aee9-7987-4e2a-9ee1-45d5b5762392_1161x387.png 1272w, https://substackcdn.com/image/fetch/$s_!N7AW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46d3aee9-7987-4e2a-9ee1-45d5b5762392_1161x387.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">P75 LCP comparison between page load types. The less, the better. Some values were estimated as stated in the labels.</figcaption></figure></div><h4>Desktop vs mobile</h4><p>Looking at the chart, you can see that half of the page load types work better on desktop, while the other half works better on mobile:</p><ul><li><p>When the page was loaded from Google (SXG On-Demand Load) or prefetched, the desktop won.</p></li><li><p>When the browser was talking to Cloudflare (Server Load, Edge Cache Load, and both SXG Redirects), mobile came out on top.</p></li></ul><p>I thought desktop users should have a better experience than mobile users because of better connection quality and more CPU power. Why then it&#8217;s not universally applicable to all page load types?</p><h4>TTFB component of LCP</h4><p>When I dug deeper into the data, I found that for Cloudflare loads, on desktop, TTFB contributed to 36-51% of LCP, while on mobile, it accounted for 22-33%. Compare the TTFB histograms below&#8212;mobile looks much better:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Ehhw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840eb477-ec1b-4a70-bbf1-24094fbc4652_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Ehhw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840eb477-ec1b-4a70-bbf1-24094fbc4652_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!Ehhw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840eb477-ec1b-4a70-bbf1-24094fbc4652_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!Ehhw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840eb477-ec1b-4a70-bbf1-24094fbc4652_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!Ehhw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840eb477-ec1b-4a70-bbf1-24094fbc4652_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Ehhw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840eb477-ec1b-4a70-bbf1-24094fbc4652_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/840eb477-ec1b-4a70-bbf1-24094fbc4652_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:42632,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840eb477-ec1b-4a70-bbf1-24094fbc4652_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Ehhw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840eb477-ec1b-4a70-bbf1-24094fbc4652_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!Ehhw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840eb477-ec1b-4a70-bbf1-24094fbc4652_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!Ehhw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840eb477-ec1b-4a70-bbf1-24094fbc4652_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!Ehhw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F840eb477-ec1b-4a70-bbf1-24094fbc4652_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The TTFB histogram for the <em>Edge Cache Load</em> on mobile. The green, dashed line marks the 75th percentile.</figcaption></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!stdP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4058792f-ccd4-493f-9d81-a54d7f226887_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!stdP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4058792f-ccd4-493f-9d81-a54d7f226887_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!stdP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4058792f-ccd4-493f-9d81-a54d7f226887_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!stdP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4058792f-ccd4-493f-9d81-a54d7f226887_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!stdP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4058792f-ccd4-493f-9d81-a54d7f226887_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!stdP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4058792f-ccd4-493f-9d81-a54d7f226887_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4058792f-ccd4-493f-9d81-a54d7f226887_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:42288,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4058792f-ccd4-493f-9d81-a54d7f226887_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!stdP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4058792f-ccd4-493f-9d81-a54d7f226887_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!stdP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4058792f-ccd4-493f-9d81-a54d7f226887_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!stdP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4058792f-ccd4-493f-9d81-a54d7f226887_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!stdP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4058792f-ccd4-493f-9d81-a54d7f226887_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The TTFB histogram for the Edge Cache Load on desktop. The green, dashed line marks the 75th percentile.</figcaption></figure></div><p>For prefetched pages, TTFB is zero or near zero, so it&#8217;s useless for comparisons. But when the pages were loaded from Google SXG cache on demand, the TTFB was better on desktop (the opposite of the previous case). On desktop, TTFB contributed to 25% of LCP, while on mobile, it accounted for 30%.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_CUZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96d4e347-eaba-427e-b74a-df91c9d06859_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_CUZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96d4e347-eaba-427e-b74a-df91c9d06859_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!_CUZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96d4e347-eaba-427e-b74a-df91c9d06859_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!_CUZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96d4e347-eaba-427e-b74a-df91c9d06859_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!_CUZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96d4e347-eaba-427e-b74a-df91c9d06859_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_CUZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96d4e347-eaba-427e-b74a-df91c9d06859_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/96d4e347-eaba-427e-b74a-df91c9d06859_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:42953,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96d4e347-eaba-427e-b74a-df91c9d06859_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_CUZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96d4e347-eaba-427e-b74a-df91c9d06859_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!_CUZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96d4e347-eaba-427e-b74a-df91c9d06859_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!_CUZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96d4e347-eaba-427e-b74a-df91c9d06859_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!_CUZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96d4e347-eaba-427e-b74a-df91c9d06859_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The TTFB histogram for the <em>SXG On-Demand Load</em> on mobile. The green, dashed line marks the 75th percentile.</figcaption></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!V-yY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6067c6fc-3c42-4afb-85ca-9e9461a8e684_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!V-yY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6067c6fc-3c42-4afb-85ca-9e9461a8e684_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!V-yY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6067c6fc-3c42-4afb-85ca-9e9461a8e684_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!V-yY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6067c6fc-3c42-4afb-85ca-9e9461a8e684_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!V-yY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6067c6fc-3c42-4afb-85ca-9e9461a8e684_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!V-yY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6067c6fc-3c42-4afb-85ca-9e9461a8e684_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6067c6fc-3c42-4afb-85ca-9e9461a8e684_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:42986,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6067c6fc-3c42-4afb-85ca-9e9461a8e684_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!V-yY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6067c6fc-3c42-4afb-85ca-9e9461a8e684_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!V-yY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6067c6fc-3c42-4afb-85ca-9e9461a8e684_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!V-yY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6067c6fc-3c42-4afb-85ca-9e9461a8e684_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!V-yY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6067c6fc-3c42-4afb-85ca-9e9461a8e684_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The TTFB histogram for the <em>SXG On-Demand Load</em> on desktop. The green, dashed line marks the 75th percentile.</figcaption></figure></div><p><strong>Why did waiting for the first byte take longer on a desktop than on a mobile for Cloudflare loads?</strong></p><p>I hypothesize that in Poland, Cloudflare edge servers have better network connectivity with mobile ISPs than with residential ones. This could be because there are only a few major mobile operators, while many residential ISPs exist.</p><p>From Cloudflare's perspective as a global infrastructure provider, Poland might be considered a relatively small market, leading them to potentially prioritize peering agreements with the larger operators.</p><p>Note that this is specific to my measurements in Poland and may not apply elsewhere.</p><p>If you have a better explanation, drop a comment below.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/p/google-prefetching-methods-performance-study/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.pawelpokrywka.com/p/google-prefetching-methods-performance-study/comments"><span>Leave a comment</span></a></p><h4>Reference page load type for comparisons</h4><p>In this post, I compare the P75 LCP of each page load type to that of a baseline on-demand page load from the server:</p><ul><li><p><strong>Mobile:</strong> 1.43 seconds</p></li><li><p><strong>Desktop:</strong> 1.82 seconds</p></li></ul><blockquote><p>The gap between mobile and desktop is 390 ms.</p><p>Keep in mind that the desktop LCP in this study is heavily impacted by the increased TTFB, as discussed above. This impact may not be present in different countries and/or in the future. This makes it difficult to draw general conclusions (i.e., unrelated to my specific website) based on comparisons of page load types on desktop and between device categories.</p><p>On the other hand, the mobile performance characteristics should be fairly universal.</p></blockquote><p>Below, you can see the LCP histograms with the TTFB impact on desktop clearly visible:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!s4m4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f304c4a-38ce-46af-8262-0d20bbca8977_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!s4m4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f304c4a-38ce-46af-8262-0d20bbca8977_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!s4m4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f304c4a-38ce-46af-8262-0d20bbca8977_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!s4m4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f304c4a-38ce-46af-8262-0d20bbca8977_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!s4m4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f304c4a-38ce-46af-8262-0d20bbca8977_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!s4m4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f304c4a-38ce-46af-8262-0d20bbca8977_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5f304c4a-38ce-46af-8262-0d20bbca8977_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:44207,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f304c4a-38ce-46af-8262-0d20bbca8977_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!s4m4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f304c4a-38ce-46af-8262-0d20bbca8977_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!s4m4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f304c4a-38ce-46af-8262-0d20bbca8977_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!s4m4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f304c4a-38ce-46af-8262-0d20bbca8977_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!s4m4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f304c4a-38ce-46af-8262-0d20bbca8977_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The LCP histogram for the <em>Server Load</em> on mobile. The green, dashed line marks the 75th percentile.</figcaption></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!k2Db!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0530285-69d8-4427-ac88-d4e244f1f926_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!k2Db!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0530285-69d8-4427-ac88-d4e244f1f926_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!k2Db!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0530285-69d8-4427-ac88-d4e244f1f926_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!k2Db!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0530285-69d8-4427-ac88-d4e244f1f926_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!k2Db!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0530285-69d8-4427-ac88-d4e244f1f926_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!k2Db!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0530285-69d8-4427-ac88-d4e244f1f926_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f0530285-69d8-4427-ac88-d4e244f1f926_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:41800,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0530285-69d8-4427-ac88-d4e244f1f926_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!k2Db!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0530285-69d8-4427-ac88-d4e244f1f926_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!k2Db!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0530285-69d8-4427-ac88-d4e244f1f926_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!k2Db!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0530285-69d8-4427-ac88-d4e244f1f926_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!k2Db!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff0530285-69d8-4427-ac88-d4e244f1f926_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The LCP histogram for the <em>Server Load</em> on desktop. The green, dashed line marks the 75th percentile.</figcaption></figure></div><h4>SXG prefetching with subresources is unbeatable</h4><p>You probably won&#8217;t be surprised that prefetching the entire website (document and subresources) using SXG is a definitive winner in terms of performance. Compared to the reference, on-demand load from the server:</p><ul><li><p><strong>Mobile:</strong> -846 ms</p></li><li><p><strong>Desktop:</strong> -1356 ms</p></li></ul><blockquote><p>As you observed above, I mark LCP improvements with a minus sign (LCP decrease, which is what we want). I use plus sign for LCP degradations (LCP increases, which we should avoid). I don&#8217;t use signs before absolute values, as you can see below.</p></blockquote><p>As a result, the LCP is as low as:</p><ul><li><p><strong>Mobile:</strong> 584 ms</p></li><li><p><strong>Desktop:</strong> 464 ms</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">A half-second LCP feels almost unreal? If you&#8217;d like more insights like this, subscribe to my newsletter.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Below you will find LCP histograms. Please note that the data points with values higher than 1.5 seconds are almost non-existent!</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zz_R!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9c84d4-f536-4031-b552-3fb7ac0f840b_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zz_R!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9c84d4-f536-4031-b552-3fb7ac0f840b_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!zz_R!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9c84d4-f536-4031-b552-3fb7ac0f840b_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!zz_R!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9c84d4-f536-4031-b552-3fb7ac0f840b_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!zz_R!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9c84d4-f536-4031-b552-3fb7ac0f840b_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zz_R!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9c84d4-f536-4031-b552-3fb7ac0f840b_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1d9c84d4-f536-4031-b552-3fb7ac0f840b_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:47882,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9c84d4-f536-4031-b552-3fb7ac0f840b_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!zz_R!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9c84d4-f536-4031-b552-3fb7ac0f840b_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!zz_R!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9c84d4-f536-4031-b552-3fb7ac0f840b_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!zz_R!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9c84d4-f536-4031-b552-3fb7ac0f840b_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!zz_R!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1d9c84d4-f536-4031-b552-3fb7ac0f840b_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The LCP histogram for the <em>SXG Prefetch with Subresources</em> on mobile. The green, dashed line marks the 75th percentile.</figcaption></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PC49!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbace7fc4-bd37-4ea8-97fb-848df66c3b1a_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PC49!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbace7fc4-bd37-4ea8-97fb-848df66c3b1a_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!PC49!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbace7fc4-bd37-4ea8-97fb-848df66c3b1a_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!PC49!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbace7fc4-bd37-4ea8-97fb-848df66c3b1a_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!PC49!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbace7fc4-bd37-4ea8-97fb-848df66c3b1a_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PC49!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbace7fc4-bd37-4ea8-97fb-848df66c3b1a_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bace7fc4-bd37-4ea8-97fb-848df66c3b1a_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:45586,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbace7fc4-bd37-4ea8-97fb-848df66c3b1a_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!PC49!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbace7fc4-bd37-4ea8-97fb-848df66c3b1a_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!PC49!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbace7fc4-bd37-4ea8-97fb-848df66c3b1a_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!PC49!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbace7fc4-bd37-4ea8-97fb-848df66c3b1a_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!PC49!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbace7fc4-bd37-4ea8-97fb-848df66c3b1a_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The LCP histogram for the SXG Prefetch with Subresources on desktop. The green, dashed line marks the 75th percentile.</figcaption></figure></div><h4>Can this be improved even further?</h4><p>My data shows that for SXG-prefetched pages with subresources, 95% of LCP time is spent on rendering.</p><p>For extremely low LCP: use large images or videos freely (they're prefetched anyway) <em>and</em> keep your page structure lean and simple (to minimize rendering). The first won't hurt; the second fixes the real bottleneck.</p><h4><strong>Second place belongs to HTML-only prefetching</strong></h4><p>While this may seem obvious, it's worth stating clearly: prefetching improves performance regardless of whether you use Speculation Rules or SXG, even when only the HTML document is prefetched. Compared to the reference on-demand load from the server, HTML-only prefetching is faster by:</p><ul><li><p><strong>Mobile:</strong> from -20 to -100 ms</p></li><li><p><strong>Desktop:</strong> from -670 to -740 ms</p></li></ul><p>The mobile results appear subpar, but they align roughly with the&nbsp;<a href="https://developer.chrome.com/blog/search-speculation-rules#impact-first-two">results reported by Google itself</a>. The desktop performance improvement is significant, mostly because prefetching is a workaround for Cloudflare&#8217;s TTFB issue.</p><blockquote><p>I believe inlining critical subresources, such as important CSS fragments, should make HTML-only prefetching more performant. However, I haven&#8217;t implemented it on my website (yet).</p></blockquote><h4>Speculation Rules are slightly better than SXG for prefetching HTML</h4><p>A page prefetched using Speculation Rules loads a bit faster than the one prefetched using SXG <strong>without</strong> subresources. That&#8217;s interesting, since both methods technically do the same thing.</p><p>On the histograms below, you can see that SXG has more extreme samples (below 250 milliseconds and 5+ seconds), while Speculation Rules&#8217; samples are much more concentrated below 1 second.</p><p>I suspect the difference may be explained in part by the cryptography-related CPU overhead of SXG.</p><h5>Mobile histograms</h5><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!MRia!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd3c0790-caef-4018-9f1e-64c7cdf496bb_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MRia!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd3c0790-caef-4018-9f1e-64c7cdf496bb_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!MRia!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd3c0790-caef-4018-9f1e-64c7cdf496bb_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!MRia!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd3c0790-caef-4018-9f1e-64c7cdf496bb_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!MRia!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd3c0790-caef-4018-9f1e-64c7cdf496bb_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MRia!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd3c0790-caef-4018-9f1e-64c7cdf496bb_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fd3c0790-caef-4018-9f1e-64c7cdf496bb_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:41282,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd3c0790-caef-4018-9f1e-64c7cdf496bb_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!MRia!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd3c0790-caef-4018-9f1e-64c7cdf496bb_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!MRia!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd3c0790-caef-4018-9f1e-64c7cdf496bb_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!MRia!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd3c0790-caef-4018-9f1e-64c7cdf496bb_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!MRia!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd3c0790-caef-4018-9f1e-64c7cdf496bb_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The LCP histogram for the <em>Speculation Rules Prefetch</em> on mobile. The green, dashed line marks the 75th percentile.</figcaption></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!K1sQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F421be29d-ee7e-4dd1-9510-2dd44b757a39_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!K1sQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F421be29d-ee7e-4dd1-9510-2dd44b757a39_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!K1sQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F421be29d-ee7e-4dd1-9510-2dd44b757a39_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!K1sQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F421be29d-ee7e-4dd1-9510-2dd44b757a39_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!K1sQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F421be29d-ee7e-4dd1-9510-2dd44b757a39_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!K1sQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F421be29d-ee7e-4dd1-9510-2dd44b757a39_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/421be29d-ee7e-4dd1-9510-2dd44b757a39_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:43415,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F421be29d-ee7e-4dd1-9510-2dd44b757a39_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!K1sQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F421be29d-ee7e-4dd1-9510-2dd44b757a39_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!K1sQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F421be29d-ee7e-4dd1-9510-2dd44b757a39_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!K1sQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F421be29d-ee7e-4dd1-9510-2dd44b757a39_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!K1sQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F421be29d-ee7e-4dd1-9510-2dd44b757a39_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The LCP histogram for the <em>SXG Prefetch without Subresources</em> on mobile. The green, dashed line marks the 75th percentile.</figcaption></figure></div><h5>Desktop histograms</h5><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zuMw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee97e79b-244b-48ee-af44-ec31f2b2fb33_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zuMw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee97e79b-244b-48ee-af44-ec31f2b2fb33_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!zuMw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee97e79b-244b-48ee-af44-ec31f2b2fb33_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!zuMw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee97e79b-244b-48ee-af44-ec31f2b2fb33_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!zuMw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee97e79b-244b-48ee-af44-ec31f2b2fb33_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zuMw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee97e79b-244b-48ee-af44-ec31f2b2fb33_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ee97e79b-244b-48ee-af44-ec31f2b2fb33_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:45108,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee97e79b-244b-48ee-af44-ec31f2b2fb33_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!zuMw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee97e79b-244b-48ee-af44-ec31f2b2fb33_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!zuMw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee97e79b-244b-48ee-af44-ec31f2b2fb33_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!zuMw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee97e79b-244b-48ee-af44-ec31f2b2fb33_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!zuMw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fee97e79b-244b-48ee-af44-ec31f2b2fb33_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The LCP histogram for the <em>Speculation Rules Prefetch</em> on desktop. The green, dashed line marks the 75th percentile.</figcaption></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!x_d6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e45a495-2f79-4b7f-b53d-c924d9510a76_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!x_d6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e45a495-2f79-4b7f-b53d-c924d9510a76_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!x_d6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e45a495-2f79-4b7f-b53d-c924d9510a76_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!x_d6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e45a495-2f79-4b7f-b53d-c924d9510a76_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!x_d6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e45a495-2f79-4b7f-b53d-c924d9510a76_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!x_d6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e45a495-2f79-4b7f-b53d-c924d9510a76_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2e45a495-2f79-4b7f-b53d-c924d9510a76_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:44310,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e45a495-2f79-4b7f-b53d-c924d9510a76_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!x_d6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e45a495-2f79-4b7f-b53d-c924d9510a76_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!x_d6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e45a495-2f79-4b7f-b53d-c924d9510a76_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!x_d6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e45a495-2f79-4b7f-b53d-c924d9510a76_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!x_d6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e45a495-2f79-4b7f-b53d-c924d9510a76_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The LCP histogram for the <em>SXG Prefetch without Subresources</em> on desktop. The green, dashed line marks the 75th percentile.</figcaption></figure></div><h4>Edge caching was worth it</h4><p>Introducing HTML edge caching lets me skip processing frequently accessed pages on my server. Also, as Cloudflare works as a reverse proxy, these pages don&#8217;t need to be transferred between my server and Cloudflare on each request. They are kept on an optimized infrastructure and retrieved in milliseconds.</p><p>This is visible in LCP measurements, when comparing on-demand page loads from the server and the edge cache:</p><ul><li><p><strong>Mobile:</strong> -120 ms</p></li><li><p><strong>Desktop:</strong> -350 ms</p></li></ul><p>Given that edge caching improves all the traffic, not only Google-referred, I think the improvement is satisfactory, especially on desktop.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!BJ-l!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479dca12-d344-4baa-a946-a0a14211fc04_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BJ-l!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479dca12-d344-4baa-a946-a0a14211fc04_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!BJ-l!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479dca12-d344-4baa-a946-a0a14211fc04_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!BJ-l!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479dca12-d344-4baa-a946-a0a14211fc04_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!BJ-l!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479dca12-d344-4baa-a946-a0a14211fc04_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!BJ-l!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479dca12-d344-4baa-a946-a0a14211fc04_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/479dca12-d344-4baa-a946-a0a14211fc04_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:45703,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479dca12-d344-4baa-a946-a0a14211fc04_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!BJ-l!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479dca12-d344-4baa-a946-a0a14211fc04_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!BJ-l!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479dca12-d344-4baa-a946-a0a14211fc04_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!BJ-l!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479dca12-d344-4baa-a946-a0a14211fc04_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!BJ-l!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F479dca12-d344-4baa-a946-a0a14211fc04_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The LCP histogram for the <em>Edge Cache Load</em> on mobile. The green, dashed line marks the 75th percentile.</figcaption></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LxXC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d99b99f-0086-4d67-a87a-10e38a60c62b_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LxXC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d99b99f-0086-4d67-a87a-10e38a60c62b_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!LxXC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d99b99f-0086-4d67-a87a-10e38a60c62b_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!LxXC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d99b99f-0086-4d67-a87a-10e38a60c62b_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!LxXC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d99b99f-0086-4d67-a87a-10e38a60c62b_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LxXC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d99b99f-0086-4d67-a87a-10e38a60c62b_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0d99b99f-0086-4d67-a87a-10e38a60c62b_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:43552,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d99b99f-0086-4d67-a87a-10e38a60c62b_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!LxXC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d99b99f-0086-4d67-a87a-10e38a60c62b_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!LxXC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d99b99f-0086-4d67-a87a-10e38a60c62b_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!LxXC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d99b99f-0086-4d67-a87a-10e38a60c62b_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!LxXC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d99b99f-0086-4d67-a87a-10e38a60c62b_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The LCP histogram for the <em>Edge Cache Load</em> on desktop. The green, dashed line marks the 75th percentile.</figcaption></figure></div><p>On the histogram above, you can see the absolute LCP values:</p><ul><li><p><strong>Mobile:</strong> 1.31 seconds</p></li><li><p><strong>Desktop:</strong> 1.47 seconds</p></li></ul><blockquote><p>This time, the gap between mobile and desktop is 160 ms&#8212;a 2.5x decrease compared to the reference <em>Server Load</em>. I found it&#8217;s caused by TTFB again, but why there is less difference now?</p><p>I don&#8217;t know. If you have an explanation, drop a comment below.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/p/google-prefetching-methods-performance-study/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.pawelpokrywka.com/p/google-prefetching-methods-performance-study/comments"><span>Leave a comment</span></a></p></blockquote><p></p><h4>Proper HTML edge caching makes Early Hints' impact negligible</h4><p>As I mentioned at the beginning, I didn't include measurements for the Early Hints page load type.</p><p>Cloudflare caches the <strong>Link</strong> HTTP header from the <strong>server&#8217;s first response</strong> and then reuses it to send Early Hints in <strong>later responses</strong> for the same URL.</p><p>However, when the edge cache is in use, then not only the Link header, but the entire HTTP response is cached. In effect, those later responses almost always include the full page directly from the cache. In that case, the origin server is never contacted, so there&#8217;s no gap between sending Early Hints and delivering the complete response. As a result, Early Hints provide no performance benefit.</p><p>Because of this, I wasn&#8217;t able to collect enough samples of requests that actually went to the origin server, where Early Hints could make a difference.</p><h4>SXG On-Demand Load impact varies between device categories</h4><p>When the SXG version of a page isn't prefetched, it must be fetched on demand. The reasons for this were explained in the <a href="https://www.pawelpokrywka.com/p/different-methods-of-prefetching">previous part</a>.</p><p>In this scenario, compared to the reference, the LCP difference was as follows:</p><ul><li><p>Mobile: +240 ms</p></li><li><p>Desktop: -210 ms</p></li></ul><h5>Mobile</h5><p>The slowdown on mobile can be attributed to the network overhead of downloading SXG subresources on demand. Each subresource may require its own certificate file to be fetched, which in the worst case can double the number of files that need to be downloaded. The higher latency of mobile connections makes this effect more visible.</p><p>Also, SXG processing requires performing cryptographic operations. Mobile CPUs often have less processing power, which can make the overhead more visible and further contribute to LCP degradation.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!F6dA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b897272-6d27-4c4e-bde7-c846a8197c7a_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!F6dA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b897272-6d27-4c4e-bde7-c846a8197c7a_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!F6dA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b897272-6d27-4c4e-bde7-c846a8197c7a_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!F6dA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b897272-6d27-4c4e-bde7-c846a8197c7a_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!F6dA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b897272-6d27-4c4e-bde7-c846a8197c7a_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!F6dA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b897272-6d27-4c4e-bde7-c846a8197c7a_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3b897272-6d27-4c4e-bde7-c846a8197c7a_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:44708,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b897272-6d27-4c4e-bde7-c846a8197c7a_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!F6dA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b897272-6d27-4c4e-bde7-c846a8197c7a_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!F6dA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b897272-6d27-4c4e-bde7-c846a8197c7a_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!F6dA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b897272-6d27-4c4e-bde7-c846a8197c7a_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!F6dA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b897272-6d27-4c4e-bde7-c846a8197c7a_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The LCP histogram for the <em>SXG On-Demand Load</em> on mobile. The green, dashed line marks the 75th percentile.</figcaption></figure></div><h5>Desktop</h5><p>I attribute the desktop LCP improvement to the TTFB issue, as explained earlier. Normally, I would expect a slight performance degradation.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pOkq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcddd8894-1661-47f9-a118-681ceec51b00_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pOkq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcddd8894-1661-47f9-a118-681ceec51b00_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!pOkq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcddd8894-1661-47f9-a118-681ceec51b00_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!pOkq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcddd8894-1661-47f9-a118-681ceec51b00_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!pOkq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcddd8894-1661-47f9-a118-681ceec51b00_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pOkq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcddd8894-1661-47f9-a118-681ceec51b00_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cddd8894-1661-47f9-a118-681ceec51b00_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:41726,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcddd8894-1661-47f9-a118-681ceec51b00_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!pOkq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcddd8894-1661-47f9-a118-681ceec51b00_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!pOkq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcddd8894-1661-47f9-a118-681ceec51b00_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!pOkq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcddd8894-1661-47f9-a118-681ceec51b00_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!pOkq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcddd8894-1661-47f9-a118-681ceec51b00_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The LCP histogram for the SXG On-Demand Load on desktop. The green, dashed line marks the 75th percentile.</figcaption></figure></div><h2>The dark side of SXG</h2><p>As you have seen above, loading the page on demand from the Google SXG cache is not optimal, at least on mobile devices. That&#8217;s not good for the technology that promised to improve page load speed. But the real problem is much worse.</p><h4>SXG fallback client-side redirect</h4><p>It occurs when the page is missing from Google&#8217;s SXG cache. When the user decides to navigate to the target page, the SXG cache serves a fallback page to the browser. This page contains a client-side JavaScript code that redirects the browser to the target page.</p><p>It would be interesting to know how much this degrades performance!</p><h4>RUM tools blind you to the SXG performance problems users experience</h4><p>I collected many samples with detailed performance measurements for page loads redirected from the Google SXG cache. Unfortunately, I decided to throw them out entirely.</p><p>I noticed they don&#8217;t make sense at all: they seemed to have no impact on LCP. No improvement (that&#8217;s obvious), but also no expected degradation.</p><p>Why? The answer is straightforward; however, it took me a while to understand. The bottom line is that it's <strong>impossible</strong> to measure this in production. No matter which Real User Monitoring (RUM) solution you use, you won&#8217;t be able to say if you have a performance issue!</p><h4>Start of navigation bias</h4><p>When a user is on page A and clicks a standard link to page B, LCP measurement begins at the moment of the click. This way, the LCP value reflects the entire wait time for the largest element on the target page, including network delays, HTTP redirects, and other overhead.</p><p>The situation changes when an SXG-fallback intermediary page uses JavaScript to redirect the user to the target page. In this case, the browser cannot distinguish whether the redirect was triggered by the user or automatically. It assumes a user action and starts measuring time only from the moment the client-side redirect occurs&#8212;not from the original click on the Google search results page.</p><p>This means the interval between the click and the client-side redirect&#8212;I'll call this <em>click-to-redirect</em>&#8212;is invisible to the browser. And that gap can be significant. In my tests, it ranged anywhere from 50 to 2000 ms, depending on the device, browser, connection type, and likely other factors.</p><blockquote><p>I built a <a href="https://www.planujemywesele.pl/sxg-tests/fallback">SXG fallback redirect demo</a> so you can try this yourself and see the difference. The demo also lets you simulate SXG and Speculation Rules prefetching, but in my testing, those didn&#8217;t affect the results.</p></blockquote><h4>Estimating the average SXG fallback LCP</h4><p>The LCP of a page loaded using SXG fallback redirect should be a sum of the LCP of the underlying page load type (<em>Server Load</em> or <em>Edge Cache Load</em>) and the <em>click-to-redirect</em> time.</p><p>The HTML of the SXG fallback redirect page is under 350 bytes, so the time it takes for the page to load and start the redirect is almost the same as its TTFB.</p><p>If the TTFB of the fallback page is similar to the TTFB of <em>SXG On-Demand Load </em>(which it should be, since both responses are generated by the same system), I could use the already collected data.</p><blockquote><p>I prepared a demo that measures TTFB of a SXG On-Demand Load. In my tests, I could confirm that the TTFB roughly equals <em>click-to-redirect</em> time. You can check it by yourself <a href="https://www.planujemywesele.pl/sxg-tests/ttfb">here</a> and compare the result with the measurement from the <a href="https://www.planujemywesele.pl/sxg-tests/fallback">previous demo</a>.</p></blockquote><p>For the initial estimation, I will use averages because they are easier to work with than percentiles.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!di2u!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb012676-c82e-45a1-94c5-b690d539ebcf_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!di2u!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb012676-c82e-45a1-94c5-b690d539ebcf_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!di2u!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb012676-c82e-45a1-94c5-b690d539ebcf_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!di2u!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb012676-c82e-45a1-94c5-b690d539ebcf_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!di2u!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb012676-c82e-45a1-94c5-b690d539ebcf_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!di2u!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb012676-c82e-45a1-94c5-b690d539ebcf_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/db012676-c82e-45a1-94c5-b690d539ebcf_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:42782,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb012676-c82e-45a1-94c5-b690d539ebcf_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!di2u!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb012676-c82e-45a1-94c5-b690d539ebcf_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!di2u!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb012676-c82e-45a1-94c5-b690d539ebcf_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!di2u!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb012676-c82e-45a1-94c5-b690d539ebcf_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!di2u!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb012676-c82e-45a1-94c5-b690d539ebcf_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The TTFB histogram for the <em>SXG On-Demand Load</em> on mobile. The green, dashed line marks the average. TTFB measurements corresponding to LCP measurements exceeding 5 seconds are not included, as explained in the methodology section.</figcaption></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!XIwA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd282ee22-84dc-411f-972e-8e4818e6c71a_3248x745.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!XIwA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd282ee22-84dc-411f-972e-8e4818e6c71a_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!XIwA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd282ee22-84dc-411f-972e-8e4818e6c71a_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!XIwA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd282ee22-84dc-411f-972e-8e4818e6c71a_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!XIwA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd282ee22-84dc-411f-972e-8e4818e6c71a_3248x745.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!XIwA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd282ee22-84dc-411f-972e-8e4818e6c71a_3248x745.png" width="1456" height="334" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d282ee22-84dc-411f-972e-8e4818e6c71a_3248x745.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:334,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:40558,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd282ee22-84dc-411f-972e-8e4818e6c71a_3248x745.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!XIwA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd282ee22-84dc-411f-972e-8e4818e6c71a_3248x745.png 424w, https://substackcdn.com/image/fetch/$s_!XIwA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd282ee22-84dc-411f-972e-8e4818e6c71a_3248x745.png 848w, https://substackcdn.com/image/fetch/$s_!XIwA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd282ee22-84dc-411f-972e-8e4818e6c71a_3248x745.png 1272w, https://substackcdn.com/image/fetch/$s_!XIwA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd282ee22-84dc-411f-972e-8e4818e6c71a_3248x745.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">The TTFB histogram for the SXG On-Demand Load on desktop. The green, dashed line marks the average. TTFB measurements corresponding to LCP measurements exceeding 5 seconds are not included, as explained in the methodology section.</figcaption></figure></div><p>The average <em>click-to-redirect</em> delay that should be added to the underlying, average page load type LCP is:</p><ul><li><p><strong>Mobile</strong>: +403 ms</p></li><li><p><strong>Desktop</strong>: +311 ms</p></li></ul><p>You can find the estimated absolute average LCP values &#8203;&#8203;in the spreadsheet mentioned earlier. Below is a chart comparing the average LCP for each page load type. The conclusions are roughly similar to those for the 75th percentile.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5NkH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F856d21a8-1fba-4014-b67c-94739262b991_1069x394.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5NkH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F856d21a8-1fba-4014-b67c-94739262b991_1069x394.png 424w, https://substackcdn.com/image/fetch/$s_!5NkH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F856d21a8-1fba-4014-b67c-94739262b991_1069x394.png 848w, https://substackcdn.com/image/fetch/$s_!5NkH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F856d21a8-1fba-4014-b67c-94739262b991_1069x394.png 1272w, https://substackcdn.com/image/fetch/$s_!5NkH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F856d21a8-1fba-4014-b67c-94739262b991_1069x394.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5NkH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F856d21a8-1fba-4014-b67c-94739262b991_1069x394.png" width="1069" height="394" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/856d21a8-1fba-4014-b67c-94739262b991_1069x394.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:394,&quot;width&quot;:1069,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:34194,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F856d21a8-1fba-4014-b67c-94739262b991_1069x394.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!5NkH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F856d21a8-1fba-4014-b67c-94739262b991_1069x394.png 424w, https://substackcdn.com/image/fetch/$s_!5NkH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F856d21a8-1fba-4014-b67c-94739262b991_1069x394.png 848w, https://substackcdn.com/image/fetch/$s_!5NkH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F856d21a8-1fba-4014-b67c-94739262b991_1069x394.png 1272w, https://substackcdn.com/image/fetch/$s_!5NkH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F856d21a8-1fba-4014-b67c-94739262b991_1069x394.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Average LCP comparison between page load types. The less, the better. Some values were estimated as stated in the labels.</figcaption></figure></div><h4>Estimating the 75th percentile LCP for SXG fallback</h4><p>I see the 75th percentile as the standard way of assessing LCP performance. It's used by the Chrome User Experience Report (<a href="https://developer.chrome.com/docs/crux">CrUX</a>). Therefore, we should estimate it for SXG fallback page load types.</p><p>In the case of my data, I found that for each page load type, its 75th percentile can be calculated by increasing the average by 20-40%. Therefore, I assumed that the missing 75th percentile for SXG Redirect loads can be estimated using this method. For the exact calculations, see the spreadsheet included earlier.</p><h4>SXG fallback significantly hurts performance</h4><p>Below, you can see how the P75 LCP degrades when the page loads using SXG fallback compared to the reference:</p><ul><li><p>For Server Load</p><ul><li><p><strong>Mobile:</strong> +615 ms</p></li><li><p><strong>Desktop:</strong> +388 ms</p></li></ul></li><li><p>For Edge Cache Load</p><ul><li><p><strong>Mobile:</strong> +369 ms</p></li><li><p><strong>Desktop:</strong> +66 ms</p></li></ul></li></ul><p>The LCP increase for the Edge Cache Load on desktop doesn&#8217;t seem much, but keep in mind that it has erased all the edge caching gains.</p><p>In my opinion, the performance degradation for pages loaded using SXG fallback is substantial. <strong>And neither Google nor Cloudflare documentation will tell you this.</strong></p><p>While these specific performance numbers are from my website, the measurement blind spot I've identified is a fundamental issue that affects all SXG implementations.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption"><strong>Enjoy uncovering hidden insights?</strong> I share experiments, lessons learned, and surprising discoveries from the problems I dig into. If you like finding out what others overlook, you&#8217;ll enjoy my newsletter.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h4>SEO is not impacted</h4><p>There&#8217;s an interesting side effect of the fact that LCP for SXG fallbacks cannot be measured accurately in production.</p><p>Chrome browsers continuously report LCP (along with the other Core Web Vitals) to Google, which then publishes the aggregated data as CrUX.</p><p>In my experiment, Chrome reported <strong>incomplete LCP data</strong> to Google when a page was loaded via an SXG fallback redirect. I throttled the network to 3G and simulated the fallback. The LCP shown in Chrome DevTools&#8217; Performance panel (screenshot below, left) matched what Chrome reported to Google (screenshot below, right). Neither measurement included the <em>click-to-redirect</em> delay.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AGZc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabf1b3dd-e989-4b48-9600-5abad21d3d33_3732x1622.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AGZc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabf1b3dd-e989-4b48-9600-5abad21d3d33_3732x1622.png 424w, https://substackcdn.com/image/fetch/$s_!AGZc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabf1b3dd-e989-4b48-9600-5abad21d3d33_3732x1622.png 848w, https://substackcdn.com/image/fetch/$s_!AGZc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabf1b3dd-e989-4b48-9600-5abad21d3d33_3732x1622.png 1272w, https://substackcdn.com/image/fetch/$s_!AGZc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabf1b3dd-e989-4b48-9600-5abad21d3d33_3732x1622.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AGZc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabf1b3dd-e989-4b48-9600-5abad21d3d33_3732x1622.png" width="1456" height="633" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/abf1b3dd-e989-4b48-9600-5abad21d3d33_3732x1622.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:633,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1053077,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/170279668?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabf1b3dd-e989-4b48-9600-5abad21d3d33_3732x1622.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!AGZc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabf1b3dd-e989-4b48-9600-5abad21d3d33_3732x1622.png 424w, https://substackcdn.com/image/fetch/$s_!AGZc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabf1b3dd-e989-4b48-9600-5abad21d3d33_3732x1622.png 848w, https://substackcdn.com/image/fetch/$s_!AGZc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabf1b3dd-e989-4b48-9600-5abad21d3d33_3732x1622.png 1272w, https://substackcdn.com/image/fetch/$s_!AGZc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabf1b3dd-e989-4b48-9600-5abad21d3d33_3732x1622.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><strong>Left:</strong> LCP measured in Chrome DevTools&#8217; Performance panel. <strong>Right:</strong> Chrome&#8217;s URL-Keyed Metrics (UKM) page showing the values that will be uploaded to Google.</figcaption></figure></div><p>As you know, LCP is a ranking factor: a good score (&#8804; 2.5 seconds) should boost your SERP positions. And how does Google get that data? From CrUX.</p><p>Now imagine this scenario: your LCP sits right at 2.5 seconds. You enable SXG, but don&#8217;t configure it properly. As a result, most of your pages load through a fallback redirect. Your <em>real</em> LCP rises above 2.5 seconds, degrading UX. But Google still sees the optimistic value from CrUX and continues to treat your site as if it had a good LCP&#8212;effectively ranking you higher than it should.</p><h2>The SXG Tradeoff</h2><p>Website loading speed depends heavily on <em>how</em> pages are delivered. Techniques like SXG and Speculation Rules, combined with edge caching, can dramatically improve LCP. At the same time, the very SXG mechanism that enables sub-second page loads can also introduce scenarios where performance suffers.</p><p>This raises an important question: Is it acceptable to sacrifice the experience of some visitors so that others enjoy a much faster site? The answer likely depends on the ratio between those who benefit and those who are negatively affected.</p><p>The key questions are:</p><ul><li><p>How many visitors experience degraded performance when SXG is enabled?</p></li><li><p>What strategies can reduce or eliminate those negative effects?</p></li><li><p>When we balance the wins against the drawbacks, is SXG&#8217;s overall impact on LCP positive or negative?</p></li></ul><p>I&#8217;ll explore these questions in the <a href="https://www.pawelpokrywka.com/p/overall-impact-of-signed-exchanges">next part</a> of this series.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;6af33757-e68b-4314-93bf-b01dde5babac&quot;,&quot;caption&quot;:&quot;Context (Oct 2025): After deep work with SXG, I suspected changes might be coming&#8212;just not this quickly. Cloudflare announced SXG deprecation, and I observed SXG stop working around Sept 19, 2025.&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Overall impact of Signed Exchanges on page load speed&#8212;a data-driven study&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:41077879,&quot;name&quot;:&quot;Pawe&#322; Pokrywka&quot;,&quot;bio&quot;:&quot;I write about security, privacy, and complex systems&#8212;exploring them from the messy middle ground between engineering and research.&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1c35edc2-abb7-4761-8eb4-f379f25e519a_800x800.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-10-09T17:57:15.490Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!mCG0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa88c796d-a636-4276-b8db-dbdc6d70a469_2816x2112.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.pawelpokrywka.com/p/overall-impact-of-signed-exchanges&quot;,&quot;section_name&quot;:&quot;Performance&quot;,&quot;video_upload_id&quot;:null,&quot;id&quot;:171386233,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:0,&quot;comment_count&quot;:0,&quot;publication_id&quot;:400540,&quot;publication_name&quot;:&quot;Pawe&#322; Pokrywka's Lab&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!gJuv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe44a7fc6-d5ec-4a99-984a-7a7e0bc80a17_800x800.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><h2>Thanks</h2><p>A special thanks to <strong>Micha&#322; Pokrywka</strong> and <strong>Maciej Wo&#378;ny</strong> for their valuable comments.</p><p>I'd also like to thank <strong>Matt Zeunert</strong> and the <strong>DebugBear</strong> team for providing me with access to their web performance monitoring service.</p><p>And thank <strong>you</strong> for reading. I hope you enjoyed it!</p>]]></content:encoded></item><item><title><![CDATA[15 ways your website loads from Google Search and how to measure each one]]></title><description><![CDATA[How to improve page load time for Google visitors using Signed Exchanges (part 7 of 10)]]></description><link>https://www.pawelpokrywka.com/p/different-methods-of-prefetching</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/different-methods-of-prefetching</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Wed, 03 Sep 2025 12:43:17 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!dweN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa50cba8b-c83d-40a4-98e7-fc1c0f6845d6_4000x2305.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dweN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa50cba8b-c83d-40a4-98e7-fc1c0f6845d6_4000x2305.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset image2-full-screen"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dweN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa50cba8b-c83d-40a4-98e7-fc1c0f6845d6_4000x2305.jpeg 424w, https://substackcdn.com/image/fetch/$s_!dweN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa50cba8b-c83d-40a4-98e7-fc1c0f6845d6_4000x2305.jpeg 848w, https://substackcdn.com/image/fetch/$s_!dweN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa50cba8b-c83d-40a4-98e7-fc1c0f6845d6_4000x2305.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!dweN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa50cba8b-c83d-40a4-98e7-fc1c0f6845d6_4000x2305.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dweN!,w_5760,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa50cba8b-c83d-40a4-98e7-fc1c0f6845d6_4000x2305.jpeg" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a50cba8b-c83d-40a4-98e7-fc1c0f6845d6_4000x2305.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;full&quot;,&quot;height&quot;:839,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:11102304,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/166600480?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa50cba8b-c83d-40a4-98e7-fc1c0f6845d6_4000x2305.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-fullscreen" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dweN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa50cba8b-c83d-40a4-98e7-fc1c0f6845d6_4000x2305.jpeg 424w, https://substackcdn.com/image/fetch/$s_!dweN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa50cba8b-c83d-40a4-98e7-fc1c0f6845d6_4000x2305.jpeg 848w, https://substackcdn.com/image/fetch/$s_!dweN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa50cba8b-c83d-40a4-98e7-fc1c0f6845d6_4000x2305.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!dweN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa50cba8b-c83d-40a4-98e7-fc1c0f6845d6_4000x2305.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Google's website loading methods inspired by <em>Becalmed off Halfway Rock</em> (1860) by Fitz Henry Lane, oil on canvas, 70.4 &#215; 120.5 cm</figcaption></figure></div><blockquote><p>Context (Oct 2025): After deep work with SXG, I suspected changes might be coming&#8212;just not this quickly. Cloudflare <a href="https://community.cloudflare.com/t/amp-and-signed-exchanges-deprecation-october-20th/831238">announced SXG deprecation</a>, and I observed SXG stop working around Sept 19, 2025.</p><p>If you still want SXG, the current path is to self-host&#8212;but the tooling looks abandoned, and setup is non-trivial. Also, Google may follow Cloudflare by shutting down the SXG cache and removing SXG support in Chrome.</p></blockquote><p>When you find a page on Google, you probably don't think much about what happens before you click it. Perhaps you've heard about prefetching, but did you know that Google employs 5 or more methods (depending on how you classify them) for loading pages? Each technique has distinct performance characteristics.</p><blockquote><p>This post is a part of a series about Signed Exchanges (SXG). In an attempt to measure how SXG impacts page loading speed I needed to distinguish between different page load types. This article is based on my research and summarizes my findings.</p><p>SXG is a technology to make your website load faster for Google-referred users. If you want to implement it on your website, start <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">here</a>.</p></blockquote><h2>Main page load type categories</h2><h4>(Plain old) on-demand document loading</h4><p>Let&#8217;s start with the obvious way of loading the page. It was there from the beginning of the web.</p><p>When the user clicks a link, the browser fetches the HTML. Then the browser fetches all the assets required for the document to display.</p><p>It&#8217;s simple and it works. However, if the server hosting the page is slow or overloaded, the user will experience a delay, which could lead to a poor experience.</p><p>To improve the situation, Google rewards websites that load quickly with better search results positions. However, Google knows that not every website in the world can become fast.</p><p>That is probably one of the reasons Google implemented prefetching.</p><h4>Prefetching the document (Speculation Rules)</h4><p>The idea is to download the page before the user decides to click on it. When they do, the page HTML is ready.</p><p>For the vast majority of Google results, prefetching is implemented using <a href="https://developer.chrome.com/blog/search-speculation-rules">Speculation Rules</a> and a <a href="https://developer.chrome.com/blog/private-prefetch-proxy">private prefetch proxy</a>.</p><p>This feature is supported on Chromium-based browsers, but support from other browsers may follow.</p><p>HTML prefetching greatly improves user experience and works with most websites out of the box, but it comes with a limitation. It doesn&#8217;t <a href="https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API#:~:text=handle%20subresource%20prefetches">prefetch</a> <a href="https://developer.chrome.com/docs/devtools/application/debugging-speculation-rules#:~:text=only%20prefetch%20the%20document">assets</a> such as CSS styles, images, and fonts.</p><h4>Prefetching the complete page (Signed Exchanges)</h4><p>It is possible to have the whole page (including assets) prefetched on Google results. When the user clicks the result, the browser starts rendering the page immediately without the need to download assets first.</p><p>To achieve this, the website owner has to <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">implement Signed Exchanges</a> (SXG). This technology was integrated into Google search even before the HTML prefetching I described above.</p><p>It&#8217;s worth noting that only Chromium-based browsers implement SXG.</p><h4>Prerendering (Accelerated Mobile Pages)</h4><p>Probably the fastest way to display a page is to use Accelerated Mobile Pages (AMP). That&#8217;s because Google prerenders websites built using this technology, so when the user clicks, the page is not only prefetched but also fully rendered. It&#8217;s hard to imagine a better user experience.</p><p>On the downside, AMP has severe restrictions. Developers are forced to use a <a href="https://amp.dev/documentation/guides-and-tutorials/learn/spec/amphtml">subset of HTML</a>, JavaScript is constrained, and many other <a href="https://www.youtube.com/watch?v=Gv8A4CktajQ">limitations</a> apply.</p><p>It works only on mobile devices using Chromium-based browsers.</p><p>The other problem with AMP is a centralized cache fully controlled by Google. It means Google effectively became a hosting company for every possible AMP page on the Internet. Quite dystopian, if you ask me.</p><p>As AMP pages are hosted on Google, your website becomes a subpage of <strong>google.com</strong>. Your URL will look like below:</p><pre><code><code>https://www.google.com/amp/s/www-yourdomain-com/yourpage</code></code></pre><p>There is a way to deal with the last two problems by introducing SXG to the mix. Cloudflare even has a <a href="https://www.cloudflare.com/website-optimization/amp-real-url/">switch</a> for that. But when I was writing this post, I tried hard but failed to find any example of an AMP website using it.</p><h4>On-demand loading with server-side redirection (ads)</h4><p>The last category describes how a page is loaded when the user clicks a Google ad. The document is loaded on demand, but with an additional HTTP/302 redirect for registering the click, which adds latency.</p><p><strong>No prefetching.</strong> Ironically, Google&#8217;s paying customers get the worst possible page load method. If you use Google Ads, make sure your page is optimized to load fast to neutralize the latency added by Google. Another potential solution is to <a href="https://support.google.com/google-ads/answer/7495018?hl=en">use AMP on your ad landing page</a> for mobile users; however, I was unable to find an example in the wild.</p><h2>Conditions, edge cases, and quirks</h2><p>The above categories are just scratching the surface of a full list of possible ways the page can load when referred from Google. That&#8217;s because various factors can improve or degrade page load efficiency, and some page load types impact others.</p><h4>Signed Exchanges</h4><p>When mentioning prefetching the entire page using SXG above, I described the best possible scenario: the HTML and all the required assets (or subresources) are being prefetched. That&#8217;s the goal, but in the real world, things don&#8217;t always go smoothly.</p><p>Various factors can influence how the page is loaded, and they greatly impact performance. Here are the possible SXG page load types I identified:</p><h5>Page prefetched with subresources</h5><p>If most of your SXG-enabled page views are prefetched with subresources, you did a great job optimizing your website!</p><p>However, despite your efforts, you will notice other, less efficient page loads.</p><h5>Page prefetched without subresources</h5><p>The browser will use subresources only if all of them were successfully prefetched. The <a href="https://www.pawelpokrywka.com/p/understanding-cors-sxg-errors">all-or-nothing principle</a> states that even if one subresource fails to prefetch, when the user visits the page, all of the subresources will need to be downloaded again.</p><p>Here are the causes of missing subresources I identified:</p><ul><li><p>SXG implementation errors (should not happen if you followed my <a href="https://www.pawelpokrywka.com/p/fixing-sxg-prefetching-errors-caused-by-mutable-subresources">previous</a> <a href="https://www.pawelpokrywka.com/p/debugging-complex-signed-exchanges-subresource-issue">SXG</a> <a href="https://www.pawelpokrywka.com/p/other-errors-with-signed-exchanges">posts</a> carefully)</p></li><li><p>Global Google SXG cache issues (very rare)</p></li><li><p>Google SXG cache housekeeping and temporary errors (happens regularly, but impacts only a small portion of page loads)</p></li><li><p>The user not spending enough time on the search results page for the prefetching to complete</p></li><li><p>User opening the page in a new tab (more on this later)</p></li></ul><p>As you can see, apart from the first, the remaining factors are beyond your control.</p><p>But still, at least the HTML was prefetched, and it&#8217;s a win for performance when compared to vanilla, on-demand page load.</p><h5>Page loaded on-demand using client-side redirect fallback</h5><p>Sometimes the target page is unavailable in Google SXG cache. The browser will still try to prefetch it (it signals Google to populate the SXG cache with this page when possible), but will fail.</p><p>When the user finally decides to click the result, the browser will once again try to load the cached page from the SXG cache. The cache response will include a simple HTML document with a client-side JavaScript redirect. The browser will follow this redirect, loading the target page.</p><p>It&#8217;s worth noting, the redirect document introduces additional latency caused by another HTTP request, HTML parsing, and JavaScript execution.</p><h5>Page loaded on-demand from Google SXG cache</h5><p>Sometimes the browser doesn&#8217;t prefetch the page, but loads its SXG version on demand when the user navigates to it. I found 2 reasons for that:</p><ol><li><p>Google prefetches only 1 SXG result on a page. I don&#8217;t know how Google determines which SXG result to prefetch, but it&#8217;s not always the first result for sure. If the user chooses to click the one that was not prefetched, it is loaded on demand, but still via SXG.</p></li><li><p>Google tried to prefetch the SXG page and failed. However, the user stayed long enough for the Google SXG cache to become populated. Now, when the user clicks the result, it&#8217;s loaded&#8212;on demand&#8212;from the SXG cache instead of the fallback document mentioned above.</p></li></ol><p>Loading the SXG version of the page on demand introduces cryptography overhead. I suspect it comes mostly from additional requests required to download certificates for signature verification. I don&#8217;t think the CPU overhead plays a big role, because cryptography operations are cheap nowadays.</p><h4>Speculation Rules</h4><p>Currently, Google prefetches the page with Speculation Rules if all of the following conditions apply:</p><ul><li><p>The target page is in the top 2 results, or the user hovered over the result (on desktop only).</p></li><li><p>The browser doesn&#8217;t hold any cookies for the target website. In most cases, this means the user hasn't visited the site before.</p></li><li><p>The device has enough capacity in terms of memory, network, and battery. For example, using battery-saving mode on a mobile will deactivate prefetching.</p></li><li><p>Prefetching is not disabled by browser extensions; for example, <a href="https://chromewebstore.google.com/detail/privacy-badger/pkehgijcmpdhfbdbbnkijodmdjhbjlgp">Privacy Badger</a> by default disables prefetching.</p></li><li><p>The target website doesn&#8217;t disallow prefetching (by default, prefetching is allowed).</p></li></ul><p>It&#8217;s worth noting that none of the above conditions apply to SXG prefetching.</p><h4>AMP prerendering</h4><p>Similarly to SXG, only one of the AMP results is prerendered. Others need to be loaded on demand from the AMP cache.</p><p>The AMP viewer is shared between the prerendered page and the others; therefore, it loads instantly every time. However, the user needs to wait for the non-prerendered pages to load in the viewer.</p><blockquote><p>There are likely other edge cases when loading AMP pages, such as missing AMP cache entries during prerendering or on-demand loading.</p><p>Replicating them manually would require creating test pages, waiting for Google to index them, searching for them in Google, and hoping that edge cases manifest themselves.</p><p>I couldn&#8217;t find any tools that would make it less difficult and time-consuming. Therefore, I chose not to research these cases. If you have additional information on this topic, please leave a comment below.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/p/different-methods-of-prefetching/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.pawelpokrywka.com/p/different-methods-of-prefetching/comments"><span>Leave a comment</span></a></p></blockquote><h4>Opening the page in a new tab</h4><p>Most of the time, when the user opens the page in a new tab, it is loaded in the background. The user will likely switch to the tab after some time, giving it a chance to render fully, while they are still interacting with the referrer website. Therefore, I believe the page load speed is far less critical in these cases. Your performance optimizations are aimed mostly at people opening the page in an existing tab.</p><p>However, it&#8217;s good to know that opening the page in a new tab degrades performance:</p><ol><li><p>Already prefetched SXG subresources are not used as mentioned previously.</p></li><li><p>The prefetched SXG HTML is also discarded, unless the CTRL+click method is used (only on desktop). I explained why in the <a href="https://www.pawelpokrywka.com/p/other-errors-with-signed-exchanges">previous part</a>.</p></li><li><p>AMP pages have to be loaded on demand. No prerendering or prefetching.</p></li></ol><p>On the other hand, Speculation Rules prefetching handles new tabs extremely reliably. It just works, regardless of how the tab is opened, on both desktop and mobile.</p><h4>Duplicate prefetching</h4><p>If the given page implements SXG, Google prefetches its SXG version, but at the same time uses Speculation Rules prefetching for the normal version. In my tests, I observed this phenomenon on the desktop only.</p><p>Seems like a waste of the user&#8217;s bandwidth (it is!), but it has a bright side too. If the user decides to open the page in a new tab (using a right-click menu, not CTRL+click), then the prefetched SXG version is discarded, but the normal version prefetched with Speculation Rules is used instead. Subresources have to be loaded normally, but at least the document is ready quickly.</p><h2>Generic techniques for improving page load speed</h2><p>If you want to compare various methods used to load a page when referred from Google, you should also consider techniques that improve performance and are not specific to Google.</p><p>You should segment your results by the technique applied for a given page load. That&#8217;s because each page load may use a different set of techniques. Mixing high- and low-performance loads in measurements increases the variance of the results, thus making it harder to analyze.</p><h4>Early Hints</h4><p>Early Hints allow the browser to begin fetching subresources even before the main document starts to load. This can improve the performance, especially for the pages that take time to render on the server.</p><h4>Caching at the edge</h4><p>If you cache HTML at the edge, such as when using Cloudflare cache, then it should be measured as a different category of page loads for the same reason as above.</p><p>If the page is delivered from the edge cache, it results in:</p><ul><li><p>Subresources start loading immediately, especially if they were listed in the <strong>Link</strong> header of the response. The benefits are similar to Early Hints.</p></li><li><p>HTML becomes available much earlier than if it had to be delivered from the origin.</p></li></ul><p>The cached page can use Early Hints, but I don&#8217;t see any performance benefits.</p><h4>No impact on prefetching</h4><p>The above page load types affect only the on-demand category of page loads. If the page is prefetched, it doesn&#8217;t matter if it was served with Early Hints or using edge cache.</p><h4>Browser caching</h4><p>If the user has visited the given website in the past, when they visit it again, the browser cache may contain some subresources, such as the website logo, ready to be used. If the website uses client-side HTML-caching, even the document could be saved in the browser cache, making subsequent visits instant.</p><p>When measuring the performance of various page loads, cached visits should be separated into a different category. Later, it may be included or excluded from the analysis.</p><p>Personally, I exclude it because returning users behave differently and may be less sensitive to page load speed because they know the site already.</p><h2>Probably an incomplete list of page load types</h2><p>Combining all the scenarios described above results in the following list of page load types. The list excludes scenarios involving a returning user and opening the page in a new tab, as those are not very useful in performance analysis.</p><ol><li><p>Server Load</p></li><li><p>Server Load with Early Hints</p></li><li><p>Edge Cache Load</p></li><li><p>Speculation Rules Prefetch</p></li><li><p>SXG Prefetch with Subresources</p></li><li><p>SXG Prefetch without Subresources</p></li><li><p>SXG On-Demand Load</p></li><li><p>Server Load via SXG Redirect</p></li><li><p>Server Load with Early Hints via SXG Redirect</p></li><li><p>Edge Cache Load via SXG Redirect</p></li><li><p>AMP Prerender</p></li><li><p>AMP On-Demand Load</p></li><li><p>Server Load via Ad Redirect</p></li><li><p>Server Load with Early Hints via Ad Redirect</p></li><li><p>Edge Cache Load via Ad Redirect</p></li></ol><p>That&#8217;s a lot of possibilities! I started preparing a flowchart describing the conditions needed for each page load type. However, it quickly became too complex, so I ditched this idea.</p><h2>Comparison of various page load types</h2><p>At first, I thought to sort all the load types by the speed, measured by Largest Contentful Paint (LCP), that they should (hypothetically) offer. It&#8217;s easy for top performers:</p><ol><li><p>AMP Prerender (fastest)</p></li><li><p>SXG Prefetch with Subresources</p></li><li><p>Speculation Rules Prefetch</p></li><li><p>SXG Prefetch without Subresources (slowest)</p></li></ol><p>The <strong>AMP On-Demand Load</strong> is very hard to grade because it&#8217;s totally different and potentially leaner than a full page. Therefore, it may, in some conditions, load even faster than the full page with a prefetched HTML. On the other hand, if the prefetched page is optimized, <strong>AMP On-Demand Load</strong> will load more slowly.</p><p>In the on-demand category, there are various ways to fetch data. I sorted them by speed:</p><ol><li><p>Edge Cache Load (fastest)</p></li><li><p>Server Load with Early Hints</p></li><li><p>Server Load (slowest)</p></li></ol><p>But there is one more - <strong>SXG On-Demand Load</strong>. It&#8217;s challenging to rank due to the SXG overhead and other individual factors, such as your server speed and connectivity.</p><p>The other dimension is the redirection method used. It may be ordered like this:</p><ol><li><p>No redirection (fastest).</p></li><li><p>Server-side redirection (Google Ads).</p></li><li><p>Client-side redirection (SXG fallback, slowest).</p></li></ol><h2>How to measure different page load types in real life</h2><p>Now, when you understand the difference between various page load types, it&#8217;s time to measure how they compare in terms of performance under real-world conditions. I will show you how I did it on my website.</p><p>The first thing that&#8217;s needed is a method to differentiate page load types. I created a JavaScript library <a href="https://github.com/pepawel/page-load-type">page-load-type</a> just for that.</p><p>It uses a variety of techniques to determine how the page was loaded and supports most of the load types described in this post, except AMP and Google Ads (both were not used on my website during the measurement).</p><h4>Requirements</h4><p>Measuring every page load doesn&#8217;t make sense. The visit should be collected only when all of the following requirements are met:</p><ul><li><p>The user comes from Google. It&#8217;s easy to overlook a special case involving the SXG cache. Read on.</p></li><li><p>The browser cache doesn&#8217;t contain entries for the measured website. Otherwise, some assets may be fetched from the cache, which improves load time, but pollutes the measurement data, making it overly optimistic.</p></li><li><p>The page isn&#8217;t opened in a new tab. As discussed earlier in this post, it breaks some prefetching/prerendering methods, and at the same time, decreases the performance sensitivity of the user.</p></li></ul><p>If the website, such as mine, implements SXG, additional requirements follow:</p><ul><li><p>The current page supports SXG. It is perfectly fine if some of your pages, particularly non-performance-critical ones, don&#8217;t support SXG. Examples include Terms and Conditions and Privacy Policy. Make sure to insert the JavaScript measurement code only on pages with SXG implemented.</p></li><li><p>The browser supports SXG. It doesn&#8217;t make sense to include Firefox visits, for example, because <a href="https://github.com/mozilla/standards-positions/issues/29">Mozilla doesn&#8217;t like SXG</a>.</p></li></ul><h5>Detecting Google-referred visits</h5><p>To determine if a user is Google-referred, the easiest method is to check the referrer for Google domains. However, as I wrote <a href="https://www.pawelpokrywka.com/p/understanding-cors-sxg-errors">earlier</a>, when the SXG fallback is used, the referrer is set to the Google SXG cache (<strong>your-domain-com.webpkgcache.com</strong>). Taking everything into account, the final JavaScript function could look like this:</p><pre><code><code>function isFromGoogle() {
  if (!document.referrer) return false;

  const regex = /(^|\.)((google\.[a-z]{2,3}(?:\.[a-z]{2})?)|(webpkgcache\.com))$/i;

  try {
    const referrer = new URL(document.referrer);
    return regex.test(referrer.hostname);
  } catch (e) {
    return false;
  }
}</code></code></pre><h5>Checking the browser cache</h5><p>The simplest way to ensure the cache is empty is to check whether the visitor is accessing the page for the first time.</p><p>In most cases, this can be done by checking for the presence of a cookie or local storage entry and then immediately setting it for future visits<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>:</p><pre><code><code>function isFirstVisit() {
  const returning = localStorage.getItem('returning');
  localStorage.setItem('returning', 'true');
  return !returning;
}</code></code></pre><h5>Detecting a new tab</h5><p>When navigating between pages, the browser maintains the history of visited pages. However, when the page is opened in a new tab (or new window), the history is not preserved. This browser behavior can be used to detect how the page was opened.</p><pre><code><code>function isOpenedInNewTab() {
  return(!window.history || window.history.length === 1);
}</code></code></pre><h5>SXG support in the browser</h5><p>I couldn&#8217;t find a direct way to query the browser for SXG support from JavaScript. As the SXG is implemented only in Chromium, the naive approach is to parse the <strong>User Agent</strong>.</p><p>However, on iOS devices, Chromium uses WebKit, which doesn&#8217;t support SXG. Also, some Chromium-based browsers, such as Brave, <a href="https://github.com/brave/brave-browser/issues/24227">intentionally</a> disable SXG. In some cases, the browser may spoof the <strong>User Agent</strong> to look like Google Chrome. All of this makes it impossible to reliably detect SXG support by parsing the <strong>User Agent</strong>.</p><h5>Accept header to the rescue</h5><p>When loading a page, the browser includes the <strong>Accept</strong> header in the request. It contains the <strong>application/signed-exchange</strong> substring if the browser supports SXG. </p><p>However, the Accept header is accessible only on the server. Therefore, the detection of SXG support should be implemented in the app or some form of middleware. Probably one of the simplest methods is to include a server-side generated <strong>&lt;script&gt;</strong> tag near the top of the HTML, looking like this:</p><pre><code><code>&lt;script id="sxgSupportScript"&gt;window.sxgSupport = false&lt;/script&gt;</code></code></pre><p>The global <strong>sxgSupport</strong> constant can be accessed later in the frontend code.</p><h5>HTML caching introduces a challenge</h5><p>Things become more complex when HTML caching on the edge becomes involved. Let&#8217;s say your server generated an HTML with the <strong>sxgSupport</strong> constant set to <strong>true</strong>, which was correct for the <strong>Chrome</strong> browser requesting the page at this moment. But the page has been cached, the <strong>sxgSupport</strong> is frozen to <strong>true</strong>, and when <strong>Firefox</strong> requests the page, it gets the incorrect value of <strong>sxgSupport</strong>.</p><p>The solution is to set the sxgSupport constant <em>after</em> it&#8217;s retrieved from the cache. It&#8217;s a perfect task for a Cloudflare worker, which could look like this:</p><pre><code><code>export default {
  async fetch(request) {
    // Get the original response
    const response = await fetch(request);
    
    // Check the SXG support
    const acceptHeader = request.headers.get('accept') || '';
    const supportsSXG = acceptHeader.includes('application/signed-exchange');
    
    // Only process HTML responses
    const contentType = response.headers.get('content-type') || '';
    if (!contentType.includes('text/html')) return response;
    
    // Create HTMLRewriter to modify the script element
    const rewriter = new HTMLRewriter()
      .on('script#sxgSupportScript', {
        text(text) {
          // Replace the entire content of the script tag
          text.replace(`window.sxgSupport = ${supportsSXG}`);
        }
      });
    
    // Apply the rewriter and return the modified response
    return rewriter.transform(response);

    // For more information, see the following blog post:
    // https://www.pawelpokrywka.com/p/different-methods-of-prefetching
  }
};</code></code></pre><p>For the instructions on how to set up and deploy Cloudflare workers, see my <a href="https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges">earlier blog post</a>.</p><h4>Including the page load type in the measurements</h4><p>If you implemented all of the above and properly installed the <a href="https://github.com/pepawel/page-load-type">page-load-type</a> library in your project, the measurement code should look like this:</p><pre><code><code>// If you use SXG, this code should be run only on SXG-enabled pages.
// If you don't use SXG you can skip checking for window.sxgSupport.

import getPageLoadType from "page-load-type";

if (isFromGoogle() &amp;&amp;
    isFirstVisit() &amp;&amp;
    !isOpenedInNewTab() &amp;&amp;
    window.sxgSupport) {

    const loadType = await getPageLoadType();

    // Run measurement code when loadType becomes available
}</code></code></pre><p>For LCP measurement, I used DebugBear, a frontend performance monitoring tool. I added the DebugBear snippet to the <strong>&lt;head&gt;</strong> section and implemented page load type tracking. Here is the resulting code, with boring fragments replaced by <strong>[...]</strong> marks.</p><pre><code><code>[...]

if (isFromGoogle() &amp;&amp; [...]) {
  const loadType = await getPageLoadType();

  // Record page load type
  window.dbbRum.push(["tag1", loadType])
}</code></code></pre><p>Shortly after deployment, I began receiving performance reports for each qualifying Google-referred visit, along with the page load type.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eUCG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9de284f-e408-4b58-bb45-29108db60ace_3260x1728.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eUCG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9de284f-e408-4b58-bb45-29108db60ace_3260x1728.png 424w, https://substackcdn.com/image/fetch/$s_!eUCG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9de284f-e408-4b58-bb45-29108db60ace_3260x1728.png 848w, https://substackcdn.com/image/fetch/$s_!eUCG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9de284f-e408-4b58-bb45-29108db60ace_3260x1728.png 1272w, https://substackcdn.com/image/fetch/$s_!eUCG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9de284f-e408-4b58-bb45-29108db60ace_3260x1728.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eUCG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9de284f-e408-4b58-bb45-29108db60ace_3260x1728.png" width="1456" height="772" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e9de284f-e408-4b58-bb45-29108db60ace_3260x1728.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:772,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:325068,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/163352093?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9de284f-e408-4b58-bb45-29108db60ace_3260x1728.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!eUCG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9de284f-e408-4b58-bb45-29108db60ace_3260x1728.png 424w, https://substackcdn.com/image/fetch/$s_!eUCG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9de284f-e408-4b58-bb45-29108db60ace_3260x1728.png 848w, https://substackcdn.com/image/fetch/$s_!eUCG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9de284f-e408-4b58-bb45-29108db60ace_3260x1728.png 1272w, https://substackcdn.com/image/fetch/$s_!eUCG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9de284f-e408-4b58-bb45-29108db60ace_3260x1728.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Page view details as presented in DebugBear UI. As you can see, I track more dimensions than page load type, but that&#8217;s irrelevant to this article.</figcaption></figure></div><p>I measured user engagement metrics in Google Analytics by firing a custom event within the same <strong>if</strong> condition:</p><pre><code>gtag('event', 'google_visit', {page_load_type: loadType});</code></pre><p>After creating a <a href="https://support.google.com/analytics/answer/14239696">custom, event-based dimension</a> and waiting 24 hours, I was able to use the page load type in my reports.</p><h2>Results</h2><p>You learned about the various methods Google uses to load your page, how to differentiate between them, and how to set up performance measurement.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Like what you&#8217;re reading? Subscribe for more.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Performance measurements will vary across websites, but seeing a real-life case study can help you decide whether and how SXG could improve your site's performance.</p><p>Therefore, I present the <a href="https://www.pawelpokrywka.com/p/google-prefetching-methods-performance-study">results for my website</a> in the next part of the series.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;6537f29b-51d5-43a7-9fd3-27e579d72b26&quot;,&quot;caption&quot;:&quot;In the previous part, we saw that a website can load in 15 different ways when visited from the Google results page and how to measure the performance impact of each load type.&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;How fast do websites load from Google Search? Comparing various prefetching and on-demand load methods.&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:41077879,&quot;name&quot;:&quot;Pawe&#322; Pokrywka&quot;,&quot;bio&quot;:&quot;I write about security, privacy, and complex systems&#8212;exploring them from the messy middle ground between engineering and research.&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1c35edc2-abb7-4761-8eb4-f379f25e519a_800x800.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-09-13T11:48:50.980Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!mSSi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa59e2c0c-83c7-4a90-94eb-3aa1bcb9e7ad_2560x1538.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.pawelpokrywka.com/p/google-prefetching-methods-performance-study&quot;,&quot;section_name&quot;:&quot;Performance&quot;,&quot;video_upload_id&quot;:null,&quot;id&quot;:170279668,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:0,&quot;comment_count&quot;:0,&quot;publication_id&quot;:null,&quot;publication_name&quot;:&quot;Pawe&#322; Pokrywka's Lab&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!gJuv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe44a7fc6-d5ec-4a99-984a-7a7e0bc80a17_800x800.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>If the user clears browser storage and returns to the page, this method will falsely indicate that it's their first visit. The same will happen if the browser <a href="https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/">clears its storage by itself</a>.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Stretching Google's prefetching]]></title><description><![CDATA[Making Chrome play a 19 MB video while "offline"]]></description><link>https://www.pawelpokrywka.com/p/stretching-google-prefetching</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/stretching-google-prefetching</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Wed, 09 Apr 2025 15:40:15 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!bty-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a15f98b-f60c-4045-afe3-7118e97c1333_3000x1949.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!bty-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a15f98b-f60c-4045-afe3-7118e97c1333_3000x1949.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset image2-full-screen"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!bty-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a15f98b-f60c-4045-afe3-7118e97c1333_3000x1949.jpeg 424w, https://substackcdn.com/image/fetch/$s_!bty-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a15f98b-f60c-4045-afe3-7118e97c1333_3000x1949.jpeg 848w, https://substackcdn.com/image/fetch/$s_!bty-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a15f98b-f60c-4045-afe3-7118e97c1333_3000x1949.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!bty-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a15f98b-f60c-4045-afe3-7118e97c1333_3000x1949.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!bty-!,w_5760,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a15f98b-f60c-4045-afe3-7118e97c1333_3000x1949.jpeg" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5a15f98b-f60c-4045-afe3-7118e97c1333_3000x1949.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;full&quot;,&quot;height&quot;:946,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2111680,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.pawelpokrywka.com/i/159056741?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a15f98b-f60c-4045-afe3-7118e97c1333_3000x1949.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-fullscreen" alt="" srcset="https://substackcdn.com/image/fetch/$s_!bty-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a15f98b-f60c-4045-afe3-7118e97c1333_3000x1949.jpeg 424w, https://substackcdn.com/image/fetch/$s_!bty-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a15f98b-f60c-4045-afe3-7118e97c1333_3000x1949.jpeg 848w, https://substackcdn.com/image/fetch/$s_!bty-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a15f98b-f60c-4045-afe3-7118e97c1333_3000x1949.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!bty-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a15f98b-f60c-4045-afe3-7118e97c1333_3000x1949.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Typical file size breakdown of modern websites inspired by <em>Kitchen Scene with Christ at Emmaus </em>(1560s) by Joachim Beuckelaer, oil on wood, 109.5 &#215; 169 cm</figcaption></figure></div><h2>The demo</h2><p>You probably came here from the demonstration I prepared, but if you haven't seen it yet, I encourage you to do so before proceeding.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://www.planujemywesele.pl/sxg-tests/offline-abuse" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5MBd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7c05c08-849e-4c71-aee5-4e2765ed5279_1333x1308.png 424w, https://substackcdn.com/image/fetch/$s_!5MBd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7c05c08-849e-4c71-aee5-4e2765ed5279_1333x1308.png 848w, https://substackcdn.com/image/fetch/$s_!5MBd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7c05c08-849e-4c71-aee5-4e2765ed5279_1333x1308.png 1272w, https://substackcdn.com/image/fetch/$s_!5MBd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7c05c08-849e-4c71-aee5-4e2765ed5279_1333x1308.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5MBd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7c05c08-849e-4c71-aee5-4e2765ed5279_1333x1308.png" width="1333" height="1308" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b7c05c08-849e-4c71-aee5-4e2765ed5279_1333x1308.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1308,&quot;width&quot;:1333,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:253532,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:&quot;https://www.planujemywesele.pl/sxg-tests/offline-abuse&quot;,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.pawelpokrywka.com/i/159056741?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7c05c08-849e-4c71-aee5-4e2765ed5279_1333x1308.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!5MBd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7c05c08-849e-4c71-aee5-4e2765ed5279_1333x1308.png 424w, https://substackcdn.com/image/fetch/$s_!5MBd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7c05c08-849e-4c71-aee5-4e2765ed5279_1333x1308.png 848w, https://substackcdn.com/image/fetch/$s_!5MBd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7c05c08-849e-4c71-aee5-4e2765ed5279_1333x1308.png 1272w, https://substackcdn.com/image/fetch/$s_!5MBd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7c05c08-849e-4c71-aee5-4e2765ed5279_1333x1308.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.planujemywesele.pl/sxg-tests/offline-abuse&quot;,&quot;text&quot;:&quot;See the demo&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.planujemywesele.pl/sxg-tests/offline-abuse"><span>See the demo</span></a></p><p>The demo requires the original Google Chrome browser and doesn&#8217;t work on iOS. If you are reading this post on an iPhone or iPad, don&#8217;t have or want Google Chrome on your device, or can&#8217;t/don&#8217;t want to run the demonstration for other reasons, I have prepared a short video that allows you to experience it passively. Note that since I recorded the demo, I have updated it based on feedback from Reddit and Hacker News.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;8acb99c2-b178-4c0f-aca5-2a7e8823767a&quot;,&quot;duration&quot;:null}"></div><blockquote><p>The video<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> is also <a href="https://www.youtube.com/watch?v=6cixE1x5AeU">available on YouTube</a> if you prefer.</p></blockquote><p>In the video, I used two phones. The screen on the left shows the instructions guiding me through the entire process, while on the right screen, you can see me performing the actual actions.</p><p>Completing all the steps resulted in loading a website with a 19 MB video file. This would be perfectly normal, except:</p><ul><li><p>I was offline when navigating to the page.</p></li><li><p>I had never visited this website on this device before (a fresh incognito mode was used, ensuring an empty cache).</p></li><li><p>Dinosaurs can&#8217;t speak!</p></li></ul><h2>It&#8217;s a trick</h2><p>The browser obviously can&#8217;t transmit data without network access.</p><p>I designed the demo to be playful, surprising you into questioning how offline mode and browser prefetching really behave.</p><p>My goal wasn&#8217;t to trick anyone for its own sake, but to create a memorable experience that highlights a very underappreciated part of web performance.</p><h2>How I did it</h2><p>First, I copied the default offline page (the one with the dinosaur) from Chrome and modified it to include a speech bubble with a link. Then, I added a hidden video that activates when clicking this link.</p><p>Then, <strong>I asked Google to prefetch (that is, download in advance) this page along with the 19 MB video file</strong> when the user performed a Google search. I even prepared a fake progress bar on the instructions page to ensure the prefetching had enough time to complete.</p><blockquote><p>If you are wondering why the CODE needs to be typed manually, it prevents mindless clicking. This approach ensures users actively engage with the instructions and provides extra time for prefetching.</p></blockquote><p>I've <a href="https://github.com/pepawel/stretching-prefetching">released the demo</a> as an open-source project on GitHub. There, you'll find a much more detailed explanation of how it works.</p><h2>Asking Google to prefetch the website</h2><p>But how do you convince Google to prefetch the website? It&#8217;s easy&#8212;you don&#8217;t need to do anything. Google <a href="https://developer.chrome.com/blog/search-speculation-rules">automatically</a> prefetches the top two search results and, on desktop, also prefetches results you hover over. It uses so-called Speculation Rules for this.</p><p><strong>However, there is a caveat:</strong> Google prefetches only the HTML. It cannot prefetch subresources referenced within the main document, such as images or&#8230; videos.</p><h2>Prefetching a complete website</h2><p>A little-known technology called Signed Exchanges allows one to prefetch an entire website, including subresources. I (ab)used this on the demo page to prefetch the 19 MB video. </p><blockquote><p>I found that the maximum amount of data possible to prefetch is approximately 21 MB.</p></blockquote><h2>The point of the demo</h2><p>Rickrolling people is fun, but a more serious application is <strong>optimizing website load speed</strong> for users coming from Google search results.</p><p>If all the data required to render a page is prefetched, then the user visiting it will have the best possible experience&#8212;the page will load instantaneously. The Largest Contentful Paint (<a href="https://web.dev/articles/lcp">LCP</a>) will be reduced to almost zero, as you can see on the video below:</p><div id="youtube2-qnyrWaR-mzk" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;qnyrWaR-mzk&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/qnyrWaR-mzk?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p>Most websites are far smaller than 21 MB, so in most cases, the browser will have enough time to prefetch all necessary resources while the user still decides which link to click.</p><p>My goal was to show you a powerful technology you can use to:</p><ul><li><p>Optimize <a href="https://en.wikipedia.org/wiki/User_experience">user experience</a> by giving new visitors a great first impression.</p></li><li><p>Improve LCP for <a href="https://developers.google.com/search/docs/appearance/page-experience">SEO benefits</a>.</p></li><li><p><a href="https://web.dev/case-studies/renault">Increase conversion rates</a>, as faster websites drive higher engagement and sales.</p></li></ul><h2>Sounds great. What&#8217;s the catch?</h2><p>To implement Signed Exchanges on your website, you first need to understand how they work in practice.</p><p>Because I couldn&#8217;t find a complete, implementation-focused guide anywhere, I decided to create one &#8212; a series of free blog posts written for typical developers.</p><p>This guide is based on real-world experience optimizing a high-traffic, dynamic website. It&#8217;s packed with insights you won&#8217;t easily find elsewhere, because many of the challenges I faced had no ready-made solutions.</p><p>If you want to dive deep into performance optimization for Google-referred users, <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">start here</a>.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;402afcb0-9c82-476f-a5a7-6132044928e8&quot;,&quot;caption&quot;:&quot;Originally published December 2023, substantially updated January 2025.&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;How I brought LCP down to under 350 ms for Google-referred users on my website&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:41077879,&quot;name&quot;:&quot;Pawe&#322; Pokrywka&quot;,&quot;bio&quot;:&quot;I write about security, privacy, and complex systems&#8212;exploring them from the messy middle ground between engineering and research.&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1c35edc2-abb7-4761-8eb4-f379f25e519a_800x800.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-01-08T17:52:00.000Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575b30f4-3dd9-4ba8-8839-84cd2c6054d8_877x640.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms&quot;,&quot;section_name&quot;:&quot;Performance&quot;,&quot;video_upload_id&quot;:null,&quot;id&quot;:139127541,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:8,&quot;comment_count&quot;:2,&quot;publication_id&quot;:null,&quot;publication_name&quot;:&quot;Pawe&#322; Pokrywka's Lab&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe44a7fc6-d5ec-4a99-984a-7a7e0bc80a17_800x800.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><h2>Thank you</h2><p>I hope you found Signed Exchanges worth exploring.</p><p>If you think others might find it interesting too, sharing the <a href="https://www.planujemywesele.pl/sxg-tests/offline-abuse">demo link</a> could be a great way to spark their curiosity.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.planujemywesele.pl/sxg-tests/offline-abuse&quot;,&quot;text&quot;:&quot;See the demo&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.planujemywesele.pl/sxg-tests/offline-abuse"><span>See the demo</span></a></p><p>Lastly, if you want to experience Signed Exchanges in a real-world, production environment, you can visit <a href="https://www.planujemywesele.pl/">PlanujemyWesele</a> &#8212; a Polish wedding planning website where I implemented full SXG support.<br>(Note: the site is in Polish, and its audience is Polish couples &#8212; but it provides a good technical example.)</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lL4l!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98cec8e5-2c1f-4e78-b4f1-c4d759d96df7_1500x811.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lL4l!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98cec8e5-2c1f-4e78-b4f1-c4d759d96df7_1500x811.webp 424w, https://substackcdn.com/image/fetch/$s_!lL4l!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98cec8e5-2c1f-4e78-b4f1-c4d759d96df7_1500x811.webp 848w, https://substackcdn.com/image/fetch/$s_!lL4l!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98cec8e5-2c1f-4e78-b4f1-c4d759d96df7_1500x811.webp 1272w, https://substackcdn.com/image/fetch/$s_!lL4l!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98cec8e5-2c1f-4e78-b4f1-c4d759d96df7_1500x811.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lL4l!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98cec8e5-2c1f-4e78-b4f1-c4d759d96df7_1500x811.webp" width="1200" height="648.6263736263736" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/98cec8e5-2c1f-4e78-b4f1-c4d759d96df7_1500x811.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:787,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:137114,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.pawelpokrywka.com/i/159056741?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98cec8e5-2c1f-4e78-b4f1-c4d759d96df7_1500x811.webp&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lL4l!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98cec8e5-2c1f-4e78-b4f1-c4d759d96df7_1500x811.webp 424w, https://substackcdn.com/image/fetch/$s_!lL4l!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98cec8e5-2c1f-4e78-b4f1-c4d759d96df7_1500x811.webp 848w, https://substackcdn.com/image/fetch/$s_!lL4l!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98cec8e5-2c1f-4e78-b4f1-c4d759d96df7_1500x811.webp 1272w, https://substackcdn.com/image/fetch/$s_!lL4l!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98cec8e5-2c1f-4e78-b4f1-c4d759d96df7_1500x811.webp 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">From <em>The Peasants</em> (2023), directed by DK and Hugh Welchman: A Polish village wedding, late 19th/early 20th century, rendered in stunning oil-painting animation (100 artists involved!). Movie <a href="https://www.youtube.com/results?search_query=the+peasants+2023+trailer">trailer</a>, image <a href="https://www.filmweb.pl/film/Ch%C5%82opi-2023-857962/photos/1160921">source</a>.</figcaption></figure></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>I used a few seconds from <em>Never Gonna Give You Up</em> by Rick Astley and a track downloaded from Royalty Free Music: Bensound.com, Artist: Benjamin Tissot, License code: GRZCWM8R2D9UCFEA</p><p></p></div></div>]]></content:encoded></item><item><title><![CDATA[Other causes of Signed Exchanges errors]]></title><description><![CDATA[From various limits to global outages&#8212;how to tackle SXG challenges (part 6 of 10)]]></description><link>https://www.pawelpokrywka.com/p/other-errors-with-signed-exchanges</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/other-errors-with-signed-exchanges</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Mon, 03 Mar 2025 16:02:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!waRD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4c0cf7-0437-4706-8021-6c0c32e5d043_6000x4520.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!waRD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4c0cf7-0437-4706-8021-6c0c32e5d043_6000x4520.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset image2-full-screen"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!waRD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4c0cf7-0437-4706-8021-6c0c32e5d043_6000x4520.jpeg 424w, https://substackcdn.com/image/fetch/$s_!waRD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4c0cf7-0437-4706-8021-6c0c32e5d043_6000x4520.jpeg 848w, https://substackcdn.com/image/fetch/$s_!waRD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4c0cf7-0437-4706-8021-6c0c32e5d043_6000x4520.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!waRD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4c0cf7-0437-4706-8021-6c0c32e5d043_6000x4520.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!waRD!,w_5760,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4c0cf7-0437-4706-8021-6c0c32e5d043_6000x4520.jpeg" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2d4c0cf7-0437-4706-8021-6c0c32e5d043_6000x4520.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;full&quot;,&quot;height&quot;:1097,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6275939,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-fullscreen" alt="" srcset="https://substackcdn.com/image/fetch/$s_!waRD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4c0cf7-0437-4706-8021-6c0c32e5d043_6000x4520.jpeg 424w, https://substackcdn.com/image/fetch/$s_!waRD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4c0cf7-0437-4706-8021-6c0c32e5d043_6000x4520.jpeg 848w, https://substackcdn.com/image/fetch/$s_!waRD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4c0cf7-0437-4706-8021-6c0c32e5d043_6000x4520.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!waRD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d4c0cf7-0437-4706-8021-6c0c32e5d043_6000x4520.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Debugging of SXG prefetching errors continues, inspired by <em>The Anatomy Lesson of Dr. Nicolaes Tulp</em> (1632) by Rembrandt, oil on canvas, 216.5 &#215; 169.5 cm</figcaption></figure></div><blockquote><p>Context (Oct 2025): After deep work with SXG, I suspected changes might be coming&#8212;just not this quickly. Cloudflare <a href="https://community.cloudflare.com/t/amp-and-signed-exchanges-deprecation-october-20th/831238">announced SXG deprecation</a>, and I observed SXG stop working around Sept 19, 2025.</p><p>If you still want SXG, the current path is to self-host&#8212;but the tooling looks abandoned, and setup is non-trivial. Also, Google may follow Cloudflare by shutting down the SXG cache and removing SXG support in Chrome.</p></blockquote><p>You learned about Signed Exchanges (SXG) prefetching errors caused by mutable subresources in the two <a href="https://www.pawelpokrywka.com/p/debugging-complex-signed-exchanges-subresource-issue">previous</a> <a href="https://www.pawelpokrywka.com/p/fixing-sxg-prefetching-errors-caused-by-mutable-subresources">posts</a>. Now, you understand a good SXG experience requires keeping your subresources unchanged over time. Also, you know how to deal with threats to their immutability.</p><p>In this post, I will explore the remaining errors I identified. As I write, <strong>there is no documentation on most of these or the documentation is outdated</strong>.</p><p>Just getting started with improving website performance? Start here with my beginner-friendly <a href="http://Just getting started with improving website performance? Start here with our beginner-friendly introduction. If you're already familiar with Signed Exchanges (SXG), feel free to continue to the technical details below.">introduction to Signed Exchanges</a>. If you're already familiar with SXG, please continue to the technical details below.</p><h2>How big Google's mouth is?</h2><h4>Trouble with images continues</h4><p>Even after implementing a workaround for <a href="https://www.pawelpokrywka.com/p/fixing-sxg-prefetching-errors-caused-by-mutable-subresources">the progressive loading issue</a>, I was still struggling with prefetching images.</p><p>This time however the issue had different characteristics. Instead of occurring for every JPEG file, it affected only a few specific images.</p><p>After inspecting them I found a common trait: they were huge and it was clearly causing the issue!</p><p>My website uses responsive images to look sharp on a 4K screen while loading fast on low-end devices. It seems the image-processing solution we use for optimizing images uploaded by our users sometimes fails which results in unoptimized images. An unoptimized high-resolution image could easily reach 1 MB and above.</p><p>Later I found the same issue can happen with other subresource types. In my case, some of the javascript chunks produced by Next.js were too large.</p><h4>SXG maximum size</h4><p>The <a href="https://github.com/google/webpackager/blob/main/docs/cache_requirements.md">official Google documentation</a> mentions 8 MB as the maximum SXG size. Interestingly, this value is honored by Cloudflare ASX (it can produce SXGs up to this size), but <strong>not by Google itself</strong>. It will refuse to store such large files.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WXPR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9710e4b-8af1-41f1-aff6-738a74321703_2688x3051.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WXPR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9710e4b-8af1-41f1-aff6-738a74321703_2688x3051.jpeg 424w, https://substackcdn.com/image/fetch/$s_!WXPR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9710e4b-8af1-41f1-aff6-738a74321703_2688x3051.jpeg 848w, https://substackcdn.com/image/fetch/$s_!WXPR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9710e4b-8af1-41f1-aff6-738a74321703_2688x3051.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!WXPR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9710e4b-8af1-41f1-aff6-738a74321703_2688x3051.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WXPR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9710e4b-8af1-41f1-aff6-738a74321703_2688x3051.jpeg" width="1456" height="1653" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c9710e4b-8af1-41f1-aff6-738a74321703_2688x3051.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1653,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:10562737,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!WXPR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9710e4b-8af1-41f1-aff6-738a74321703_2688x3051.jpeg 424w, https://substackcdn.com/image/fetch/$s_!WXPR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9710e4b-8af1-41f1-aff6-738a74321703_2688x3051.jpeg 848w, https://substackcdn.com/image/fetch/$s_!WXPR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9710e4b-8af1-41f1-aff6-738a74321703_2688x3051.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!WXPR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9710e4b-8af1-41f1-aff6-738a74321703_2688x3051.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Prefetching of too large SXG subresources will inevitably fail, inspired by <em>Sisyphus</em> (1548&#8211;1549) by Titian, oil on canvas, 237 &#215; 216 cm</figcaption></figure></div><p>I had to <a href="https://www.planujemywesele.pl/sxg-tests/too-large-subresource">determine it experimentally</a>, by generating files of different sizes and testing which ones were cached by Google. Here is the result:</p><div class="pullquote"><p>The maximum size of a file that can be stored in a Google SXG cache is roughly 1044 KB (1044000 bytes).</p></div><p>You may ask why it&#8217;s such a weird number, not a round 1 MiB (2<sup>20</sup> bytes) or at least 1 MB (10<sup>6</sup> bytes). I think Google limits the size of the entire cache entry to 1 MiB and apart from the actual file contents, it also includes:</p><ul><li><p>HTTP response headers,</p></li><li><p>SXG metadata and cryptographic stuff,</p></li><li><p>maybe some additional, Google cache&#8217;s specific metadata.</p></li></ul><p>The size of those depends on various elements, therefore it&#8217;s impossible to reliably determine the exact maximum file size.</p><blockquote><p>To ensure a great user experience and minimize data usage, keep your subresource sizes well below the limit. While Google allows subresources up to this size, smaller files help protect users from excessive data consumption and lead to faster page loads.</p></blockquote><h4>Size of HTML document</h4><p>Though unlikely, your HTML documents should not exceed the limit of 1044 KB too. Otherwise, you will get one of the following messages in the <strong>Warning</strong> HTTP header of the response generated by the Google SXG cache:</p><pre><code>199 - "debug: content has ingestion error: Not a valid signed-exchange."</code></pre><pre><code>199 - "debug: content has ingestion error: Error fetching resource: missing magic prefix; extracting prologue"</code></pre><p>You can <a href="https://www.planujemywesele.pl/sxg-tests/too-large-document">check it yourself</a> on the demo page I prepared.</p><h4>Total size of prefetched data</h4><p>With the ability to prefetch 20 subresources plus 1 HTML file, you can easily calculate the maximum data size available for prefetching. It's approximately 21 MB.</p><p>If you'd like to experience what it feels like to reach this limit, <a href="https://www.pawelpokrywka.com/p/stretching-google-prefetching">check out the interactive demo</a> I created. You'll enjoy it!</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;002973d6-3078-4208-8adf-01203d8c3b87&quot;,&quot;caption&quot;:&quot;The demo&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Stretching Google prefetching&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:41077879,&quot;name&quot;:&quot;Pawe&#322; Pokrywka&quot;,&quot;bio&quot;:&quot;I write about security, privacy, and complex systems&#8212;exploring them from the messy middle ground between engineering and research.&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1c35edc2-abb7-4761-8eb4-f379f25e519a_800x800.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-04-09T15:40:15.766Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a15f98b-f60c-4045-afe3-7118e97c1333_3000x1949.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.pawelpokrywka.com/p/stretching-google-prefetching&quot;,&quot;section_name&quot;:&quot;Performance&quot;,&quot;video_upload_id&quot;:null,&quot;id&quot;:159056741,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:0,&quot;comment_count&quot;:0,&quot;publication_id&quot;:null,&quot;publication_name&quot;:&quot;Pawe&#322; Pokrywka's Lab&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe44a7fc6-d5ec-4a99-984a-7a7e0bc80a17_800x800.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><h4>How to keep your subresources small</h4><p>The only proper solution is to ensure your subresources don&#8217;t exceed the limit. If some of the images are not optimized, optimize them. Consider decreasing image quality and/or resolution if the file size remains over the limit after optimization.</p><p>Try to keep your scripts compact. Minification is a standard practice, but it has to be mentioned here in case you don&#8217;t use it for some reason. Avoid using large libraries that need to be included in your bundle. Consider dynamic loading for less frequently used logic.</p><p>You may also want to adjust how the scripts are split into chunks to ensure no chunk exceeds the limit. For example, Next.js won&#8217;t split the <strong>_app</strong> <a href="https://github.com/vercel/next.js/blob/790efc5941e41c32bb50cd915121209040ea432c/packages/next/src/build/webpack-config.ts#L1029">by default</a>. To force it to do it, you can extend your <strong>next.config.js</strong> using the following template:</p><pre><code>function forceChunking(config, pages) {
  const originalChunks = config.optimization.splitChunks.chunks;
  config.optimization.splitChunks.chunks = (chunk) =&gt; {
    // Allow to split certain pages because it may eventually
    // become too large for SXG. For the full context see:
    // https://www.pawelpokrywka.com/p/other-errors-with-signed-exchanges
    if (pages.includes(chunk.name)) return true;
    return originalChunks(chunk);
  }
}

const nextConfig = {

  // Your current config options

  webpack: (config, options) =&gt; {
    <strong>if (!options.isServer) forceChunking(config, ['pages/_app']);</strong>

    // Your current webpack customizations

    return config;
  }
}
 
module.exports = nextConfig</code></pre><p>Other frameworks that use webpack underneath may experience similar issues, and the above webpack configuration change may help. However, adjustments may be needed.</p><h4>Choosing not to prefetch</h4><p>If your subresources are still too large, the solution of last resort is to not prefetch them. This will impact your page load speed, but at least other subresources will be prefetched.</p><p>To do this, you can delete the <strong>&lt;link&gt;</strong> tag, but if you want to retain prefetching your assets for Early Hints (I mention them in <a href="https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges">the second part</a>) and for standard, non-SXG page prefetching, there is a better option.</p><p>Just add the <strong>data-sxg-no-header</strong> attribute to <strong>&lt;link&gt;</strong> tags you want to exclude from SXG but keep for everything else:</p><pre><code>&lt;link rel="preload" href="2big.jpg" as="image" <strong>data-sxg-no-header</strong> /&gt;</code></pre><p>The attribute is documented <a href="https://github.com/google/sxg-rs/blob/main/README.md">here</a>.</p><h4>The idea for a potential Cloudflare-side solution</h4><p>Cloudflare already limits SXG it generates to 20 subresources to ensure compatibility with Google's SXG cache. It would be consistent to also exclude subresources during SXG generation if they exceed Google SXG cache's size limit.</p><h2>403 forbidden errors</h2><p>Sometimes, during debugging, you may encounter this message in the <strong>Warning</strong> HTTP header of the response generated by the Google SXG cache:</p><pre><code>199 - "debug: content has ingestion error: Error fetching resource: origin response code = 403"</code></pre><p>This means instead of a normal HTTP/2xx response, Googlebot got an HTTP/403 (forbidden) response while fetching the page to be put into the Google SXG cache later.</p><p>Unless your app generated an HTTP/403 error (which may happen for example on pages requiring being logged in), the most probable explanation is that Cloudflare blocked this request.</p><h4>Why does Cloudflare block some requests?</h4><p>Cloudflare allows website owners to <a href="https://developers.cloudflare.com/bots/">protect their property against bots</a>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!23_j!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20922653-a5b2-4818-8bbb-00438af751ff_5906x3921.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!23_j!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20922653-a5b2-4818-8bbb-00438af751ff_5906x3921.jpeg 424w, https://substackcdn.com/image/fetch/$s_!23_j!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20922653-a5b2-4818-8bbb-00438af751ff_5906x3921.jpeg 848w, https://substackcdn.com/image/fetch/$s_!23_j!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20922653-a5b2-4818-8bbb-00438af751ff_5906x3921.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!23_j!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20922653-a5b2-4818-8bbb-00438af751ff_5906x3921.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!23_j!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20922653-a5b2-4818-8bbb-00438af751ff_5906x3921.jpeg" width="1456" height="967" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/20922653-a5b2-4818-8bbb-00438af751ff_5906x3921.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:967,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:13661455,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!23_j!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20922653-a5b2-4818-8bbb-00438af751ff_5906x3921.jpeg 424w, https://substackcdn.com/image/fetch/$s_!23_j!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20922653-a5b2-4818-8bbb-00438af751ff_5906x3921.jpeg 848w, https://substackcdn.com/image/fetch/$s_!23_j!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20922653-a5b2-4818-8bbb-00438af751ff_5906x3921.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!23_j!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20922653-a5b2-4818-8bbb-00438af751ff_5906x3921.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Your app is under constant attack from bots, inspired by <em>The Siege and Destruction of Jerusalem by the Romans Under the Command of Titus, A.D. 70</em> (1850) by David Roberts, oil on canvas, 7&#215;12 feet</figcaption></figure></div><p>When enabled, Cloudflare scans requests for signs of automated traffic. Depending on your plan, you can choose what to do with bot-generated requests. For example, in the Pro plan (the minimum for SXG support) you can allow bots, block them, or challenge them with a captcha. If a person is mistakenly classified as a bot, using a captcha enables them to still access the site.</p><p>As some bots are useful (such as Googlebot), Cloudflare gives you an option to allow so-called good bots. You can see all the options below:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dy2m!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7d7b5e5-66f9-4408-bda0-f121be203a31_2027x2744.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dy2m!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7d7b5e5-66f9-4408-bda0-f121be203a31_2027x2744.png 424w, https://substackcdn.com/image/fetch/$s_!dy2m!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7d7b5e5-66f9-4408-bda0-f121be203a31_2027x2744.png 848w, https://substackcdn.com/image/fetch/$s_!dy2m!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7d7b5e5-66f9-4408-bda0-f121be203a31_2027x2744.png 1272w, https://substackcdn.com/image/fetch/$s_!dy2m!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7d7b5e5-66f9-4408-bda0-f121be203a31_2027x2744.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dy2m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7d7b5e5-66f9-4408-bda0-f121be203a31_2027x2744.png" width="1456" height="1971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b7d7b5e5-66f9-4408-bda0-f121be203a31_2027x2744.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:599191,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dy2m!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7d7b5e5-66f9-4408-bda0-f121be203a31_2027x2744.png 424w, https://substackcdn.com/image/fetch/$s_!dy2m!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7d7b5e5-66f9-4408-bda0-f121be203a31_2027x2744.png 848w, https://substackcdn.com/image/fetch/$s_!dy2m!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7d7b5e5-66f9-4408-bda0-f121be203a31_2027x2744.png 1272w, https://substackcdn.com/image/fetch/$s_!dy2m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7d7b5e5-66f9-4408-bda0-f121be203a31_2027x2744.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The recommended configuration for public websites is to block or challenge malicious bots while allowing legitimate ones like Googlebot. However, even with these settings, <strong>Cloudflare may still inadvertently block Googlebot's access!</strong></p><h4>Why does Cloudflare block Googlebot?</h4><p>First, I thought the blocking was happening because I was repeatedly triggering Googlebot to perform many debug requests to strange-looking test URLs. Maybe Cloudflare uses machine learning to distinguish those from normal Googlebot traffic?</p><p>Then, I opened the Google Search Console and saw a lot of HTTP/4xx responses for normal, non-debug Googlebot requests. Up to 5% of the traffic was blocked. It wasn&#8217;t looking good. Blocking even some of the Googlebot requests is dangerous for SEO.</p><p>When I allowed all bots, the issue disappeared.</p><h4>A better solution</h4><p>The bot-blocking feature is great; I didn&#8217;t want to disable it. However, I didn&#8217;t want to prevent Googlebot from accessing my site, which led me to the solution based on the Cloudflare Web Application Firewall (WAF).</p><p>I created a rule that skips bot blocking for traffic from Google IP addresses.</p><p>To do that, go to the <strong>Security</strong> section and open the <strong>WAF</strong> submenu. Choose the <strong>Custom rules</strong> tab and hit the <strong>Create rule</strong> button. Then, configure the rule as shown below and click the <strong>Deploy</strong> button:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Rw0b!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04eac05d-af57-442b-a7e4-f6ebcc08628a_2131x2538.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Rw0b!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04eac05d-af57-442b-a7e4-f6ebcc08628a_2131x2538.png 424w, https://substackcdn.com/image/fetch/$s_!Rw0b!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04eac05d-af57-442b-a7e4-f6ebcc08628a_2131x2538.png 848w, https://substackcdn.com/image/fetch/$s_!Rw0b!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04eac05d-af57-442b-a7e4-f6ebcc08628a_2131x2538.png 1272w, https://substackcdn.com/image/fetch/$s_!Rw0b!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04eac05d-af57-442b-a7e4-f6ebcc08628a_2131x2538.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Rw0b!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04eac05d-af57-442b-a7e4-f6ebcc08628a_2131x2538.png" width="1456" height="1734" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/04eac05d-af57-442b-a7e4-f6ebcc08628a_2131x2538.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1734,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:456333,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Rw0b!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04eac05d-af57-442b-a7e4-f6ebcc08628a_2131x2538.png 424w, https://substackcdn.com/image/fetch/$s_!Rw0b!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04eac05d-af57-442b-a7e4-f6ebcc08628a_2131x2538.png 848w, https://substackcdn.com/image/fetch/$s_!Rw0b!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04eac05d-af57-442b-a7e4-f6ebcc08628a_2131x2538.png 1272w, https://substackcdn.com/image/fetch/$s_!Rw0b!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F04eac05d-af57-442b-a7e4-f6ebcc08628a_2131x2538.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Adding a custom Cloudflare rule to skip bot blocking for Google.</figcaption></figure></div><p>Instead of specifying an ever-changing list of Google IPs, I matched against a single <a href="https://en.wikipedia.org/wiki/Autonomous_system_(Internet)">Autonomous System</a> (AS) number. You can use&nbsp;<a href="https://stat.ripe.net/app/launchpad/S1_15169_C13C31C4C34C9C22C28C20C6C7C26C29C30C14C17C2C21C33C16C10">RIPEstat</a>&nbsp;or any other online tool to find the number assigned to Google.</p><h2>Google SXG cache ingestion issue</h2><p>One day I observed a dramatically increased number of SXG prefetching errors. The following days were even worse and finally, SXG stopped working entirely. I believe it was caused by <a href="https://support.google.com/webmasters/thread/308247718/google-sxg-cache-webpkgcache-com-stopped-working-globally">the global SXG cache outage</a>.</p><p>It lasted for about 2 weeks. I experienced it only once, but potentially it may happen again. If your SXG metrics go crazy without any sensible reasons, <a href="https://www.planujemywesele.pl/sxg-tests/is-alive">checking if the Google SXG cache works</a> may be a good idea.</p><p>Sometimes, Google SXG cache ingestion rate is so slow, that it seems dead. This is likely due to the cache being under heavy load. I&#8217;ve occasionally experienced this during peak hours, especially when the U.S. wakes up.</p><h2>Temporary errors</h2><p>Even if you do everything correctly, you may encounter this message in the <strong>Warning</strong> HTTP header of the response generated by the Google SXG cache:</p><pre><code>199 - "debug: content has ingestion error: Not a valid signed-exchange."</code></pre><p>The warning will eventually vanish in a few hours max. It may be related to the number of new (not cached yet) subresources the page depends on. If the number is high enough, the error may occur temporarily.</p><blockquote><p>If you know some errors are temporary, you won&#8217;t waste time trying to find and fix the inexistent cause.</p><p>Note however, the exact same error may communicate you have to fix your page because Cloudflare fails to generate SXG version of it. Please refer to the first post in the series on <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">how to adjust your page to be SXG-ready</a>.</p></blockquote><h4>How many subresources are too many?</h4><p>In the official documentation, you will find that <a href="https://github.com/google/webpackager/blob/main/docs/cache_requirements.md#:~:text=There%20may%20be%20no%20more%20than%2020">the maximum number of subresources is 20</a>. This is a hard limit you won&#8217;t be able to exceed.</p><p>However, there is also a soft limit. Exceeding it will cause temporary errors until all the subresources are cached.</p><p><a href="https://www.planujemywesele.pl/sxg-tests/new-vs-established-subresources">In my experiments</a>, the error mentioned above may manifest itself when the page introduces 14 or more new subresources. By testing various scenarios, I came to the following formula:</p><pre><code>2 x new + existing &lt; 34</code></pre><p>If you substitute <strong>new</strong> and <strong>existing</strong> variables with your values and the formula is true, your page should have no issues. For example, having 13 new and 7 existing subresources will be ok, while 15 new and 5 existing subresources may lead to (temporary) problems.</p><blockquote><p>Cloudflare ASX utilization fluctuations may impact the formula, so take it with a grain of salt.</p></blockquote><h4>Why do these errors occur?</h4><p>Based on my research, I conclude the cause is related to the latency guarantees of Cloudflare ASX. I believe there is a maximal latency ASX could introduce while generating an SXG-wrapped page. ASX tries to avoid exceeding it by having a plan B: returning the original page, without SXG wrapping.</p><p>If this happens, the Google SXG cache receives a response of a different type than expected. That&#8217;s why the warning about an invalid signed exchange appears.</p><p>Calculating integrity hashes from subresources takes time, so Cloudflare ASX uses a special-purpose cache I <a href="https://www.pawelpokrywka.com/p/debugging-complex-signed-exchanges-subresource-issue">discovered</a> accidentally. If all the integrity hashes are cached, SXG will be generated quickly. The more integrity hashes have to be calculated, the more time it takes, eventually exceeding the limit.</p><p>Retrieving the integrity hash from the ASX cache takes some time too. Although it&#8217;s probably a fraction of the time spent to calculate an integrity hash, it&#8217;s non-zero. That&#8217;s why the number of existing (cached) subresources is also taken into account.</p><p>In my tests, the size of the subresource doesn&#8217;t impact the ASX behavior&#8212;the formula stays the same. It probably means ASX estimates the time it will take to include all the integrity hashes based on the number of new and cached subresources instead of actually measuring how long it takes.</p><h4>Expiration error</h4><p>Another temporary error is related to SXG expiration date.</p><pre><code>199 - "debug: content has ingestion error: SXG expiration date is closer than minimum cache duration."</code></pre><p>I was unable to reproduce it reliably and thus failed to understand its cause. It appears from time to time and should vanish quickly if you followed instructions from my earlier posts.</p><blockquote><p>If you experience this issue permanently, I suggest checking if the <strong>Cache-Control</strong> HTTP header is set correctly. You will find more information on that in the <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">first part</a> of the series.</p></blockquote><h2>Things you can&#8217;t control directly</h2><p>Some errors happen because of reasons outside of your control, at least partially. Here are the scenarios I identified, but there may be others:</p><h4>Not enough time</h4><p>The user may click on the Google search result before all the subresources are prefetched. This could be due to a slow or unreliable connection and/or the user acting very quickly.</p><p>While you cannot control either of these factors, you can attempt to reduce the total size of the subresources to be prefetched. You may need to balance the prefetched page completeness and the SXG error rate.</p><blockquote><p>You will learn how to <a href="https://www.pawelpokrywka.com/p/different-methods-of-prefetching">measure the SXG error rate</a> in the next part of the series.</p></blockquote><h4>New tab</h4><p>The user may open the page in a new tab. In my tests, it breaks the prefetching of subresources (and in some cases, SXG-prefetching entirely, see the update below).</p><blockquote><p>Interestingly, it doesn&#8217;t impact the prefetched HTML page&#8212;it is still used by the browser. Only subresources are dropped.</p><p>My <em>unconfirmed</em> explanation of this phenomenon is that the Chrome browser uses different caches for SXG pages and SXG subresources. The former is similar to a normal browser cache&#8212;it&#8217;s shared between tabs. The latter uses an isolated, per-tab cache, probably for the same reason as the <em>all-or-nothing</em> principle I described in <a href="https://www.pawelpokrywka.com/p/understanding-cors-sxg-errors">the third part</a>: privacy.</p><h5><strong>2025-05-18 UPDATE</strong></h5><p>The behavior described above can be observed when using the <strong>CTRL+click</strong> method to open a new tab on desktop. If you choose &#8220;Open link in new tab&#8221; from the context menu on desktop, or open the link in the new tab on mobile, the browser <strong>will not use the SXG-prefetched HTML at all.</strong></p><h5>Why does it behave like this?</h5><p>Google sets the non-SXG target page URL in the <strong>href</strong> attribute of the result link. This ensures that when you hover over the link, you see the normal URL instead of the less user-friendly <strong>webpkgcache.com</strong> URL. However, when you click the link, Google dynamically replaces the <strong>href</strong> attribute with the <strong>webpkgcache.com</strong> URL, allowing your browser to load the prefetched version of the page.</p><p>The &#8220;Open link in new tab&#8221; method bypasses JavaScript, so the <strong>href</strong> attribute is not updated, and the browser opens the original target URL instead of the SXG-prefetched version. In contrast, using <strong>CTRL+click</strong> triggers the click event, allowing the script to rewrite the URL to the SXG version before the new tab is opened.</p></blockquote><h4>Browser deciding not to prefetch (unconfirmed)</h4><p>The browser is <a href="https://web.dev/articles/link-prefetch#prefetching_under_the_hood">not required</a> to perform prefetching.</p><p>In theory, in suboptimal conditions such as slow connection speed, low battery, etc. the browser may decide to conserve resources and avoid prefetching (such as implemented in the <a href="https://getquick.link/">quicklink</a> library).</p><p>I tried triggering this behavior on my Android phone by disabling wifi, downgrading to 3G, then enabling data and battery saver. The Chrome browser slowly, but happily prefetched everything.</p><p>I was not able to observe the browser avoiding prefetching, but the possibility is there. Even if prefetching currently works all the time on all browsers supporting SXG, it may change in the future.</p><h4>(Sub)resources expiring in Google SXG cache</h4><p>Each SXG has an expiration date enforced by cryptographic signature validity. I found that Google sometimes performs refresh requests, but I&#8217;m sure there are scenarios when the user&#8217;s browser fetches expired SXG.</p><p>I could not reproduce those errors reliably, but <strong>I observe them regularly</strong> in my monitoring system. This leads to breaking the SXG experience entirely if the main document has expired or partially if only one or more subresources have expired.</p><p>I was able to simulate the expired subresource error in the browser. In the Chrome Developer Tools&#8217; <strong>Network</strong> tab, the failed request should have the following status:</p><pre><code>(failed) net::ERR_INVALID_SIGNED_EXCHANGE</code></pre><p>If you open the <strong>Preview</strong> tab in the request details, you should see the following error:</p><pre><code>Invalid timestamp. creation_time: 1739777019, expires_time: 1740381819, verification_time: 1740383698
Failed to verify the signed exchange header.</code></pre><p>Additionally, the <strong>Date</strong> and <strong>Expires</strong> in the <strong>Signature</strong> section should be marked in red.</p><h4>Subresources being evicted from Google SXG cache (unconfirmed)</h4><p>I suspect Google SXG cache entries can be removed before expiration due to several reasons, the most important is disk space conservation. When this happens, it may cause prefetching failures manifesting as CORS errors.</p><p>However, I was unable to reproduce it. I believe the cache should include an eviction mechanism, but I can also imagine other ways to conserve disk space.</p><h4>Temporarily missing SXG certificates</h4><p>I've written about these types of errors <a href="https://www.pawelpokrywka.com/p/understanding-cors-sxg-errors">before</a>, but for the sake of completeness, I'm posting it here as well.</p><p>I witnessed a few cases where a certificate used in the SXG signature was missing from the Google SXG cache. In those scenarios, the browser can&#8217;t validate the affected signature leading to the following subresource status in the <strong>Network</strong> tab of Chrome Developer Tools:</p><pre><code>(failed) net::ERR_INVALID_SIGNED_EXCHANGE</code></pre><p>I'm not sure why these errors occur. I suspect it's an issue in Cloudflare ASX or Google SXG cache, so it can be fixed only by those companies. On the bright side, these errors should be rare and tend to disappear quickly.</p><h2>Summary</h2><p>This post is the final one in the category of explaining SXG errors. Here's what you've learned about them:</p><ul><li><p><strong>Non-temporary CORS errors</strong> while prefetching SXG pages indicate issues with Google&#8217;s SXG cache handling subresources.</p></li><li><p><strong>Subresources must be immutable</strong>, including their HTTP headers. Common issues arise from the absence of the <strong>Vary</strong> header, Cloudflare's HTTP/2 prioritization, and <strong>Etag</strong> headers tied to modification times.</p></li><li><p><strong>Size limitations</strong>: Each element of your page, including the HTML document and all subresources intended for prefetching, must be smaller than 1044 KB.</p></li><li><p><strong>Avoid excessive prefetching</strong>: Using SXG to prefetch too much data can increase error rates.</p></li><li><p><strong>Bot protection</strong>: If you want to protect your site from bots, always accept Googlebot requests. This requires configuring Cloudflare's WAF appropriately.</p></li><li><p><strong>External factors</strong>: Some errors may arise from issues beyond your control.</p></li></ul><p>In the next part, I will guide you on how to <a href="https://www.pawelpokrywka.com/p/different-methods-of-prefetching">measure the impact of SXG</a> on your website properly.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;be090e81-46fa-433c-a587-9164944fa0dd&quot;,&quot;caption&quot;:&quot;When you find a page on Google, you probably don't think much about what happens before you click it. Perhaps you've heard about prefetching, but did you know that Google employs 5 or more methods (depending on how you classify&#8230;&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;15 ways your website loads from Google Search and how to measure each one&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:41077879,&quot;name&quot;:&quot;Pawe&#322; Pokrywka&quot;,&quot;bio&quot;:&quot;I write about security, privacy, and complex systems&#8212;exploring them from the messy middle ground between engineering and research.&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1c35edc2-abb7-4761-8eb4-f379f25e519a_800x800.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-09-03T12:43:17.804Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!dweN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa50cba8b-c83d-40a4-98e7-fc1c0f6845d6_4000x2305.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.pawelpokrywka.com/p/different-methods-of-prefetching&quot;,&quot;section_name&quot;:&quot;Performance&quot;,&quot;video_upload_id&quot;:null,&quot;id&quot;:166600480,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:0,&quot;comment_count&quot;:0,&quot;publication_id&quot;:null,&quot;publication_name&quot;:&quot;Pawe&#322; Pokrywka's Lab&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!gJuv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe44a7fc6-d5ec-4a99-984a-7a7e0bc80a17_800x800.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><h2>Thanks</h2><p>I hope you found this post helpful. Thank you for reading!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">If this resonated with you, there&#8217;s a lot more I want to share. Subscribe below to get valuable content delivered straight to your inbox.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Debugging mutable subresources: a detective story]]></title><description><![CDATA[The bizarre case of Signed Exchanges: how frequent deployments increased the error rate and revealed hidden cache poisoning (part 5 of 10)]]></description><link>https://www.pawelpokrywka.com/p/debugging-complex-signed-exchanges-subresource-issue</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/debugging-complex-signed-exchanges-subresource-issue</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Fri, 21 Feb 2025 12:36:18 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!YkE3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24ea74fc-d89a-499e-9dfd-915527938490_3476x2532.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YkE3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24ea74fc-d89a-499e-9dfd-915527938490_3476x2532.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset image2-full-screen"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YkE3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24ea74fc-d89a-499e-9dfd-915527938490_3476x2532.jpeg 424w, https://substackcdn.com/image/fetch/$s_!YkE3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24ea74fc-d89a-499e-9dfd-915527938490_3476x2532.jpeg 848w, https://substackcdn.com/image/fetch/$s_!YkE3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24ea74fc-d89a-499e-9dfd-915527938490_3476x2532.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!YkE3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24ea74fc-d89a-499e-9dfd-915527938490_3476x2532.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YkE3!,w_5760,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24ea74fc-d89a-499e-9dfd-915527938490_3476x2532.jpeg" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/24ea74fc-d89a-499e-9dfd-915527938490_3476x2532.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;full&quot;,&quot;height&quot;:1061,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2709347,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-fullscreen" alt="" srcset="https://substackcdn.com/image/fetch/$s_!YkE3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24ea74fc-d89a-499e-9dfd-915527938490_3476x2532.jpeg 424w, https://substackcdn.com/image/fetch/$s_!YkE3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24ea74fc-d89a-499e-9dfd-915527938490_3476x2532.jpeg 848w, https://substackcdn.com/image/fetch/$s_!YkE3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24ea74fc-d89a-499e-9dfd-915527938490_3476x2532.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!YkE3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24ea74fc-d89a-499e-9dfd-915527938490_3476x2532.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">&#8220;Programming allows you to think about thinking, and while debugging you learn learning&#8221; Nicholas Negroponte. Background: <em>Hunter with dog</em> (1891) by Bruno Liljefors, oil on canvas, 51 &#215; 70 cm.</figcaption></figure></div><blockquote><p>Context (Oct 2025): After deep work with SXG, I suspected changes might be coming&#8212;just not this quickly. Cloudflare <a href="https://community.cloudflare.com/t/amp-and-signed-exchanges-deprecation-october-20th/831238">announced SXG deprecation</a>, and I observed SXG stop working around Sept 19, 2025.</p><p>If you still want SXG, the current path is to self-host&#8212;but the tooling looks abandoned, and setup is non-trivial. Also, Google may follow Cloudflare by shutting down the SXG cache and removing SXG support in Chrome.</p></blockquote><p>Prefetching errors eliminate 99% of the benefits of using Signed Exchanges (SXG). This is why it's critical to resolve these errors. While you learned how to handle two common errors in <a href="https://www.pawelpokrywka.com/p/fixing-sxg-prefetching-errors-caused-by-mutable-subresources">the previous post</a>, this article will address a more complex error that shares some similarities with those issues.</p><p>As I write, <strong>there is no official or unofficial documentation</strong> on the issue described in this post.</p><blockquote><p>If you don&#8217;t know what SXG is, or need a refresher, please start from <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">the beginning</a>.</p></blockquote><h2>This one was fun</h2><p>The error I observed was different from the previous two. This time:</p><ul><li><p>all subresource types were affected,</p></li><li><p>subresources were targeted randomly, but not all at once,</p></li><li><p>if a given subresource was affected, the error occurred for some time (a day for example) and then disappeared (or <em>moved</em> to another subresource).</p></li></ul><p>To make things even more interesting, the error rate was proportional to our development activity. Deploying more often caused more errors to occur. The obvious solution was to fire developers and stop working on the app!</p><p>Since my role as a programmer was at risk, I had to find another way to fix it. I found a workaround pretty quickly, but it took me a long time to understand the cause. I demonstrate my thought process below.</p><h2>Comparing fresh and cached versions</h2><p>First, I fetched the problematic, SXG-wrapped page with the <a href="https://github.com/WICG/webpackage/blob/main/go/signedexchange/README.md">dump-signedexchange</a> tool (you have to remember to selectively or globally deactivate Cloudflare bot protection when using it):</p><pre><code>$ dump-signedexchange -uri https://www.planujemywesele.pl/bad-page

...
response:
  status: 200
  headers:
    ...
    Link: &lt;https://www.planujemywesele.pl/sub.css&gt;;rel=preload;as=style,&lt;https://www.planujemywesele.pl/sub.css&gt;;rel=allowed-alt-sxg;<strong>header-integrity="sha256-/6M5qdrjQUl+dCdWtFqN50BmBvAHZ+sJxiLaoraBO2s="</strong>
signature: ...
header integrity: ...</code></pre><p>I removed almost everything from the output and made the important part bold: the subresource header integrity hash.</p><p>Then I used the same tool to fetch the problematic SXG-wrapped subresource:</p><pre><code>$ dump-signedexchange -uri https://www.planujemywesele.pl/sub.css

...
<strong>header integrity: sha256-/6M5qdrjQUl+dCdWtFqN50BmBvAHZ+sJxiLaoraBO2s=</strong></code></pre><p>The integrity hash of the subresource is the same in the document and in the subresource itself. That&#8217;s the way it should work.</p><p>Now, let&#8217;s see what Google cached. We can use the same tool to examine it:</p><pre><code>$ dump-signedexchange -uri https://www-planujemywesele-pl.webpkgcache.com/doc/-/s/www.planujemywesele.pl/bad-page

...
response:
  status: 200
  headers:
    ...
    Cf-Ray: 8f61c1c4a09ce802-ORD
    Link: &lt;https://www.planujemywesele.pl/sub.css&gt;;rel=preload;as=style,&lt;https://www.planujemywesele.pl/sub.css&gt;;rel=allowed-alt-sxg;<strong>header-integrity="sha256-gxoxVoIaE8MKl8tf283F3FKVF8NwrLqz2jMT/vwyO9c="</strong>
signature: ...
header integrity: ...</code></pre><p>The header-integrity hash is different!</p><p>Let&#8217;s examine the HTTP response carrying Signed Exchange payload using curl:</p><pre><code>$ curl -iH "Accept: application/signed-exchange;v=b3" https://www-planujemywesele-pl.webpkgcache.com/doc/-/s/www.planujemywesele.pl/bad-page

...
link: &lt;https://www-planujemywesele-pl.webpkgcache.com/sub/<strong>gxoxVoIaE8MK</strong>/s/www.planujemywesele.pl/sub.css&gt;;rel="alternate";type="application/signed-exchange;v=b3";anchor="https://www.planujemywesele.pl/sub.css"
...</code></pre><p>The <strong>Link</strong> header points to a URL to a cached version of SXG-wrapped subresource, so that the browser can prefetch it. This URL is constructed from the URL and a part of the integrity hash (in bold).</p><p>When we try to fetch it using curl:</p><pre><code>$ curl -iH "Accept: application/signed-exchange;v=b3" https://www-planujemywesele-pl.webpkgcache.com/sub/gxoxVoIaE8MK/s/www.planujemywesele.pl/sub.css

...
<strong>location: https://www.planujemywesele.pl/sub.css
content-type: text/html; charset=UTF-8</strong>
...

&lt;HTML&gt;&lt;HEAD&gt;
&lt;meta http-equiv="content-type" content="text/html;charset=utf-8"&gt;
&lt;TITLE&gt;<strong>Redirecting</strong>&lt;/TITLE&gt;
&lt;META HTTP-EQUIV="<strong>refresh</strong>" content="0; url=https://www.planujemywesele.pl/sub.css"&gt;
&lt;/HEAD&gt;
&lt;BODY onLoad="<strong>location.replace('https://www.planujemywesele.pl/sub.css'+document.location.hash)</strong>"&gt;</code></pre><p>The fallback page will be returned telling us, that the entry doesn&#8217;t exist in the Google SXG cache.</p><p>Now, it is very clear that the SXG-wrapped page cached by Google is different because it links to another subresource. And this subresource can&#8217;t be found in the Google SXG cache. This is a cause for prefetching failure and CORS error.</p><h2>Why did Google cache a different page?</h2><p>To get Google&#8217;s perspective, we can use Google Search Console. It contains a <strong>URL inspection tool</strong> that has a <strong>Live test</strong> feature. It lets you use Googlebot to fetch any URL on your website and examine the results.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!eIdJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97e33eed-e9cd-4547-b0df-fb7d68f4569b_3181x795.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!eIdJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97e33eed-e9cd-4547-b0df-fb7d68f4569b_3181x795.png 424w, https://substackcdn.com/image/fetch/$s_!eIdJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97e33eed-e9cd-4547-b0df-fb7d68f4569b_3181x795.png 848w, https://substackcdn.com/image/fetch/$s_!eIdJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97e33eed-e9cd-4547-b0df-fb7d68f4569b_3181x795.png 1272w, https://substackcdn.com/image/fetch/$s_!eIdJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97e33eed-e9cd-4547-b0df-fb7d68f4569b_3181x795.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!eIdJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97e33eed-e9cd-4547-b0df-fb7d68f4569b_3181x795.png" width="1456" height="364" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/97e33eed-e9cd-4547-b0df-fb7d68f4569b_3181x795.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:364,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:157871,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!eIdJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97e33eed-e9cd-4547-b0df-fb7d68f4569b_3181x795.png 424w, https://substackcdn.com/image/fetch/$s_!eIdJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97e33eed-e9cd-4547-b0df-fb7d68f4569b_3181x795.png 848w, https://substackcdn.com/image/fetch/$s_!eIdJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97e33eed-e9cd-4547-b0df-fb7d68f4569b_3181x795.png 1272w, https://substackcdn.com/image/fetch/$s_!eIdJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F97e33eed-e9cd-4547-b0df-fb7d68f4569b_3181x795.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Google Search Console URL Inspection tool with Live test results.</figcaption></figure></div><p>When you open the <strong>More info</strong> tab and click the <strong>HTTP Response</strong>, you will see the HTTP headers. Here they are, redacted to include only interesting bits:</p><pre><code>HTTP/1.1 200 OK
...
cf-ray: 8f6092f8e08e2231-ORD
link: &lt;https://www.planujemywesele.pl/sub.css&gt;;rel=preload;as=style,&lt;https://www.planujemywesele.pl/sub.css&gt;;rel=allowed-alt-sxg;<strong>header-integrity="sha256-gxoxVoIaE8MKl8tf283F3FKVF8NwrLqz2jMT/vwyO9c="</strong>
...</code></pre><p>The header-integrity hash is the same as in the Google SXG cache. So we have the answer to the most recent question: Google cached what it saw which is different from what we see.</p><h2>Why does Google see the world differently than we do?</h2><p>My first thought was that Cloudflare treats Googlebot differently than others. I even formulated an entire theory based on this assumption. But it was a dead end.</p><p>Cloudflare has data centers all around the world. What if two different data centers return different pages? Particularly, what if the data center close to Google returns a different page than the data center near me? To verify it, the first step is to find the data center used for Googlebot traffic.</p><h4>Which Cloudflare data center handles Googlebot traffic?</h4><p>In the SXG responses intended for Googlebot, Cloudflare includes a <strong>Cf-Ray</strong> header. This header contains <a href="https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-ray">an identifier of the Cloudflare data center</a> used to handle the request.</p><blockquote><p>My suspicion about Cloudflare treating Googlebot differently was correct because this header is served only for this bot.</p><p>I tried to impersonate Googlebot by setting the <strong>User-Agent</strong> request header, but the <strong>Cf-Ray</strong> header was not added as a result. They probably use a more reliable, IP-based detection of Googlebot.</p></blockquote><p>In the responses you saw above, the data center is identified as ORD. Cloudflare has a <a href="https://www.cloudflarestatus.com/">status page</a> listing all data centers and their geographical locations:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QyO-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91dad6ad-2cc9-4f61-806a-9ba93f425728_1607x812.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QyO-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91dad6ad-2cc9-4f61-806a-9ba93f425728_1607x812.png 424w, https://substackcdn.com/image/fetch/$s_!QyO-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91dad6ad-2cc9-4f61-806a-9ba93f425728_1607x812.png 848w, https://substackcdn.com/image/fetch/$s_!QyO-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91dad6ad-2cc9-4f61-806a-9ba93f425728_1607x812.png 1272w, https://substackcdn.com/image/fetch/$s_!QyO-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91dad6ad-2cc9-4f61-806a-9ba93f425728_1607x812.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QyO-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91dad6ad-2cc9-4f61-806a-9ba93f425728_1607x812.png" width="1456" height="736" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/91dad6ad-2cc9-4f61-806a-9ba93f425728_1607x812.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:736,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:135030,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!QyO-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91dad6ad-2cc9-4f61-806a-9ba93f425728_1607x812.png 424w, https://substackcdn.com/image/fetch/$s_!QyO-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91dad6ad-2cc9-4f61-806a-9ba93f425728_1607x812.png 848w, https://substackcdn.com/image/fetch/$s_!QyO-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91dad6ad-2cc9-4f61-806a-9ba93f425728_1607x812.png 1272w, https://substackcdn.com/image/fetch/$s_!QyO-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91dad6ad-2cc9-4f61-806a-9ba93f425728_1607x812.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Example Cloudflare data centers in North America.</figcaption></figure></div><p>It seems Cloudflare uses airport codes for data center identifiers. As you can see, ORD is Chicago.</p><p>It means Google accessed my website using the Cloudflare data center located in Chicago, at least for requests I examined, and at the moment I did it. It may change in the future, but it&#8217;s quite consistent for now.</p><h4>Sitting where Google sits</h4><p>Now, that I knew the data center, I tunneled to Chicago using a VPN:</p><pre><code>$ mullvad relay set location us-chi-wg-101
Relay constraints updated
$ mullvad reconnect -w
Connecting
    Relay:                  us-chi-wg-101
    Features:               LAN Sharing, Quantum Resistance
    Visible location:       USA, Chicago, IL
Connected
    Relay:                  us-chi-wg-101
    Features:               LAN Sharing, Quantum Resistance
    Visible location:       USA, Chicago, IL</code></pre><p>Finally, I checked if Cloudflare routed me to the correct data center. The <strong>Cf-Ray</strong> header <strong>for non-SXG responses</strong> is accessible to anybody, not only Googlebot:</p><pre><code>$ curl -si https://www.planujemywesele.pl/ | grep cf-ray
cf-ray: 8f6229558f9710c2-ORD</code></pre><p>Ok, I was using the same data center as Googlebot. Now, I fetched the SXG:</p><pre><code>$ dump-signedexchange -uri https://www.planujemywesele.pl/bad-page

...
response:
  status: 200
  headers:
    ...
    Link: &lt;https://www.planujemywesele.pl/sub.css&gt;;rel=preload;as=style,&lt;https://www.planujemywesele.pl/sub.css&gt;;rel=allowed-alt-sxg;<strong>header-integrity="sha256-gxoxVoIaE8MKl8tf283F3FKVF8NwrLqz2jMT/vwyO9c="</strong>
signature: ...
header integrity: ...</code></pre><p>The header-integrity hash is the same as in the cached version and as seen by Googlebot, but different from what I saw when accessing the website without VPN.</p><p>This proves that the Chicago data center serves a different version of a page than the data center close to me.</p><h2>Which version of the page is correct?</h2><p>I fetched the subresource while using the Chicago data center via VPN:</p><pre><code>$ dump-signedexchange -uri https://www.planujemywesele.pl/sub.css

...
<strong>header integrity: sha256-/6M5qdrjQUl+dCdWtFqN50BmBvAHZ+sJxiLaoraBO2s=</strong></code></pre><p>The header integrity is the same as when I fetched it without VPN, from my original data center.</p><p>Given the following facts:</p><ul><li><p>My non-VPN data center returns a page linking to a subresource with integrity hash X.</p></li><li><p>Chicago data center returns a page linking to a subresource with integrity hash Y.</p></li><li><p>Both data centers return a subresource with integrity hash X.</p></li></ul><p>It becomes clear the Chicago data center returned a broken page.</p><h2>What&#8217;s wrong with Chicago?</h2><p>Is the Chicago data center special? It seemed so because I was getting consistently good results when switching to different data centers using a VPN. Only Chicago returned an invalid page.</p><p>I heard that in Chicago, they don&#8217;t put ketchup on their hot dogs. That could be the reason, but I kept searching for other explanations.</p><p>The subresources causing troubles were most often shared between different pages. I tested many of those pages from Chicago with a VPN. Each page linked to a subresource with an invalid integrity hash.</p><h4>Is the cache entry for a page broken?</h4><p>Each Cloudflare data center has its own cache. Maybe the cache was to blame? What if for some reason, the Chicago data center cache contains broken entries for the pages I tested?</p><p>This led me to test a page that I was sure wasn&#8217;t accessed before and, therefore missing from the cache. To my surprise, this fresh page still referenced subresources with invalid integrity hashes!</p><h4>Is the cache entry for a subresource broken?</h4><p>No matter which data center I used to obtain the subresource, it gave me the same data. So the subresource was fine.</p><p>Just to be sure I purged the page and the subresource from the Cloudflare cache and performed the test again. The result was the same - I got a page linking to a subresource with an invalid integrity hash.</p><p>It seemed the data center had <em>remembered</em> the wrong integrity hash and used it to generate SXG responses.</p><p>Remembering things is the role of a cache. But I cleared it! It didn&#8217;t make any sense, unless&#8230;</p><h2>There is another, hidden cache!</h2><p>When I think about it now, it&#8217;s obvious. Cloudflare uses a special-purpose cache for integrity hashes to minimize latency when generating SXG responses. As with Automated Signed Exchanges (ASX) instances, this cache is local to every data center.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!B2BN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55b2aa88-a46a-491f-9946-8bd09695ddba_2856x1556.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!B2BN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55b2aa88-a46a-491f-9946-8bd09695ddba_2856x1556.png 424w, https://substackcdn.com/image/fetch/$s_!B2BN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55b2aa88-a46a-491f-9946-8bd09695ddba_2856x1556.png 848w, https://substackcdn.com/image/fetch/$s_!B2BN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55b2aa88-a46a-491f-9946-8bd09695ddba_2856x1556.png 1272w, https://substackcdn.com/image/fetch/$s_!B2BN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55b2aa88-a46a-491f-9946-8bd09695ddba_2856x1556.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!B2BN!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55b2aa88-a46a-491f-9946-8bd09695ddba_2856x1556.png" width="1200" height="653.5714285714286" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/55b2aa88-a46a-491f-9946-8bd09695ddba_2856x1556.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:793,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:151679,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!B2BN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55b2aa88-a46a-491f-9946-8bd09695ddba_2856x1556.png 424w, https://substackcdn.com/image/fetch/$s_!B2BN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55b2aa88-a46a-491f-9946-8bd09695ddba_2856x1556.png 848w, https://substackcdn.com/image/fetch/$s_!B2BN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55b2aa88-a46a-491f-9946-8bd09695ddba_2856x1556.png 1272w, https://substackcdn.com/image/fetch/$s_!B2BN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F55b2aa88-a46a-491f-9946-8bd09695ddba_2856x1556.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Network diagram showing some of Cloudflare's data center components.</figcaption></figure></div><p>I validated this hypothesis by putting logging workers before subresources linked by the page. This way every request was logged, because workers are invoked every time, even for cached entries.</p><p>Then I requested SXG of the page and observed logs. The subresources were accessed only once per data center. Further requests for SXG didn&#8217;t result in any logs, even after purging the cache.</p><p><strong>The hidden cache I discovered is not mentioned anywhere in the documentation.</strong> It can&#8217;t be purged just like the normal Cloudflare cache. It&#8217;s an opaque black box.</p><p>Sometimes, this black box contains invalid integrity hashes leading to populating the Google SXG cache with invalid pages and breaking the SXG experience for the end users. I would say the hidden ASX cache seems <em>poisoned</em>.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Want more in-depth articles like this one? Subscribe to never miss a post.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><h2>Why is the ASX cache poisoned?</h2><p>I believe it&#8217;s caused by a delayed subresource mutation.</p><p>When a client requests the SXG page for the first time, ASX may retrieve the subresource and store its integrity hash in the local ASX cache. Later, the subresource mutates, but this fact remains hidden as long as the original version remains in the Cloudflare cache. The issue starts to manifest after it is evicted from the Cloudflare cache: the page links to a subresource that is no longer there.</p><p>The Google SXG cache obscures the issue further because it stores both versions of the subresource. However, user-visible issues can arise in two scenarios:</p><ol><li><p>When the Google SXG cache evicts the original version before it expires.</p></li><li><p>When technical problems (such as network issues) prevent the Google SXG cache from successfully storing the original version during the initial data collection.</p></li></ol><p>In either case, this results in the Google SXG cache only having the mutated version of the subresource, while the original version becomes unavailable. As the page refers to the original version it causes prefetching errors. And because the ASX cache is poisoned, the error will stay there no matter how often the Google SXG cache tries to refresh the entry.</p><p>I could not validate the two scenarios described above because they depend on hard-to-control conditions. Therefore it should be treated as a hypothesis. However, I successfully <a href="https://www.planujemywesele.pl/sxg-tests/asx-cache-poisoning">demonstrated</a> ASX cache poisoning by artificially simulating technical issues while Google's SXG cache performed its initial fetch of the subresource.</p><h2>How to fix ASX cache poisoning?</h2><p>Now that I knew the cause&#8212;delayed subresource mutation&#8212;I could solve the problem by eliminating this mutation. By comparing various responses I was able to find the source of the mutation.</p><p>It was the <strong>Etag</strong> header.</p><h2>Etags and deployments</h2><p>The <strong>Etag</strong> HTTP header is an optional identifier sent along with the response. It should stay the same unless the response content changes. When the browser fetches the URL again, it includes the <strong>Etag</strong> value of the locally cached response. The server compares the received value with the current value. If they differ, it sends the response as usual. But if they are the same, it returns an empty response with <strong>304 Not Modified</strong> status, saving bandwidth.</p><p>Etags may be used for every response type, but in the case of SXG the most important are static assets, so let&#8217;s focus on those.</p><p>This is how etags for static assets are generated by various components of my stack:</p><ul><li><p>Nginx generates etag for a given asset <a href="https://github.com/nginx/nginx/blob/e3a9b6ad08a86e799a3d77da3f2fc507d3c9699e/src/http/ngx_http_core_module.c#L1701">using its size and modification time</a>.</p></li><li><p>Next.js app uses the <a href="https://github.com/pillarjs/send/blob/dc6b5d4ec29355ffcf1ab122e52c27a98c392c15/index.js#L765C6-L765C7">same</a> <a href="https://github.com/jshttp/etag/blob/36e457a99da03db227701276c15255ee3fbf96bb/index.js#L130">approach</a>.</p></li><li><p>Ruby on Rails app leaves it to the web server (it this case nginx).</p></li></ul><p>If your deployment method (like mine) writes the static assets to disk even if they didn&#8217;t change, then their modification time will be updated as well. In effect, nginx and Next.js will generate new etags even if the files remain the same.</p><blockquote><p>I suppose other web servers and framworks behave the same.</p></blockquote><p>It will cause some clients to download assets again. It&#8217;s a minor issue because assets are typically cached for a long time and etags are not needed until the cache expires.</p><h4>Etags impact on SXG</h4><p>However, the <strong>Etag</strong> header is included in the generated SXG subresource. If the etag changes, the subresource mutates. And given some conditions are met it leads to SXG prefetching errors.</p><p>In my case, the more often I deployed, the more often the subresource mutated. This was the reason why the errors intensified during increased development activity!</p><h4>How to make etags and SXG work together?</h4><p>It may be a good idea to ensure the modification times of your assets stay the same unless actually modified. I can&#8217;t explain how to achieve it in detail because it depends on the deployment method. And sometimes it may be non-trivial or even impossible to achieve.</p><p>Therefore, I suggest an alternative approach. Do not send the <strong>Etag</strong> header in responses for static assets. If you use a 1-year expiration date as I recommend, it's not a big deal anyway.</p><p>For nginx, you can set it globally with the following directive in the HTTP context, preferably in the <strong>nginx.conf</strong> file:</p><pre><code>etag off; # Disable etag generation for static assets</code></pre><p>Next.js allows <a href="https://nextjs.org/docs/app/api-reference/config/next-config-js/generateEtags">disabling etags</a> entirely. I don&#8217;t like this approach, because it also disables it for HTML responses. Instead, it can be done using nginx again, by removing the <strong>Etag</strong> header from the responses for static assets only.</p><p>I use the following nginx configuration snippet in locations containing static files:</p><pre><code><code>location ~* \.(?:css|js|gif|png|jpeg|jpg|ico|ttf|woff|woff2|svg)$ {
  # Clear etags set by Next.js as they vary between deployments
  # even if the assets remain the same. This may leads to SXG failures.
  # For more info see: https://www.pawelpokrywka.com/p/debugging-complex-signed-exchanges-subresource-issue
  more_clear_headers 'Etag';

  # The rest of assets-related configuration, such as cache expiration
}</code></code></pre><p>Check the documentation of your web server and framework for hints on how to configure etags. For example, Apache offers <a href="https://httpd.apache.org/docs/2.4/mod/core.html#FileETag">an option</a> to generate etags from file contents. This approach ensures immutability and eliminates the need to disable etags.</p><p>Alternatively, you can create a Cloudflare transform rule to remove the <strong>Etag</strong> header from all static assets. I have already explained how to create such a rule in the <a href="https://www.pawelpokrywka.com/p/fixing-sxg-prefetching-errors-caused-by-mutable-subresources">previous part</a>. The only difference lies in step 6: instead of selecting <em>Set static</em> from the menu, select <em>Remove</em> and enter <em>Etag</em> in the <strong>Header name</strong> field.</p><blockquote><p>As there is no way to purge the hidden ASX cache, remember to regenerate URLs to invalidate the cache (as described in <a href="https://www.pawelpokrywka.com/p/fixing-sxg-prefetching-errors-caused-by-mutable-subresources">the previous part</a>) after implementing the changes.</p></blockquote><h2>Quick-fix for ASX cache poisoning</h2><p>I found that bypassing the Cloudflare cache makes the ASX stop using the poisoned cache as long as the bypass is in place.</p><blockquote><p>It means both caches share some logic. If Cloudflare went one step further and synchronized evictions between those caches, the issue described in this post would disappear instantly.</p></blockquote><p>When I tried to disable the Cloudflare cache globally, the ASX stopped generating SXG-wrapped pages. If the cache is not used for too many assets, the SXG will break. It has to be disabled selectively.</p><p>To bypass the Cloudflare cache for a specific asset, go to the <strong>Cache Rules</strong> in the <strong>Caching</strong> section. Hit the <strong>Create rule</strong> button, fill out the form according to the template below changing the URL to match your asset<strong>,</strong> and click the <strong>Deploy</strong> button.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DjTn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4aa57bb-0c55-4ef6-ae14-519a479f9d7d_2163x2143.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DjTn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4aa57bb-0c55-4ef6-ae14-519a479f9d7d_2163x2143.png 424w, https://substackcdn.com/image/fetch/$s_!DjTn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4aa57bb-0c55-4ef6-ae14-519a479f9d7d_2163x2143.png 848w, https://substackcdn.com/image/fetch/$s_!DjTn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4aa57bb-0c55-4ef6-ae14-519a479f9d7d_2163x2143.png 1272w, https://substackcdn.com/image/fetch/$s_!DjTn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4aa57bb-0c55-4ef6-ae14-519a479f9d7d_2163x2143.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DjTn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4aa57bb-0c55-4ef6-ae14-519a479f9d7d_2163x2143.png" width="1456" height="1443" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b4aa57bb-0c55-4ef6-ae14-519a479f9d7d_2163x2143.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1443,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:264116,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!DjTn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4aa57bb-0c55-4ef6-ae14-519a479f9d7d_2163x2143.png 424w, https://substackcdn.com/image/fetch/$s_!DjTn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4aa57bb-0c55-4ef6-ae14-519a479f9d7d_2163x2143.png 848w, https://substackcdn.com/image/fetch/$s_!DjTn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4aa57bb-0c55-4ef6-ae14-519a479f9d7d_2163x2143.png 1272w, https://substackcdn.com/image/fetch/$s_!DjTn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4aa57bb-0c55-4ef6-ae14-519a479f9d7d_2163x2143.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Adding a cache rule to bypass caching of a given asset. I removed unimportant parts of the form for readability.</figcaption></figure></div><p>I don&#8217;t recommend bypassing the cache as it impacts the performance. But if you want to quickly check if the issue is related to ASX cache poisoning, you can use this technique.</p><h2>That&#8217;s it!</h2><p>I've demonstrated how to diagnose, understand, and fix this issue that's not documented anywhere. I hope you find it useful and perhaps learned something new about approaching unfamiliar problems.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Want more practical troubleshooting stories like this? Subscribe to my newsletter.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>While this concludes our discussion of <em>mutable</em> category errors, there are still a few other issues to address. I'll cover these remaining problems and their solutions in my next post.</p><p>Thank you for reading!</p>]]></content:encoded></item><item><title><![CDATA[The mystery of mutable subresources in Signed Exchanges]]></title><description><![CDATA[What they are, how they break caching, and how to fix them (part 4 of 10)]]></description><link>https://www.pawelpokrywka.com/p/fixing-sxg-prefetching-errors-caused-by-mutable-subresources</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/fixing-sxg-prefetching-errors-caused-by-mutable-subresources</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Tue, 11 Feb 2025 13:55:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!YaCg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda5e2fb8-02fe-4ad1-a83a-a946fec4ae32_7389x4621.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!YaCg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda5e2fb8-02fe-4ad1-a83a-a946fec4ae32_7389x4621.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset image2-full-screen"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!YaCg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda5e2fb8-02fe-4ad1-a83a-a946fec4ae32_7389x4621.jpeg 424w, https://substackcdn.com/image/fetch/$s_!YaCg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda5e2fb8-02fe-4ad1-a83a-a946fec4ae32_7389x4621.jpeg 848w, https://substackcdn.com/image/fetch/$s_!YaCg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda5e2fb8-02fe-4ad1-a83a-a946fec4ae32_7389x4621.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!YaCg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda5e2fb8-02fe-4ad1-a83a-a946fec4ae32_7389x4621.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!YaCg!,w_5760,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda5e2fb8-02fe-4ad1-a83a-a946fec4ae32_7389x4621.jpeg" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/da5e2fb8-02fe-4ad1-a83a-a946fec4ae32_7389x4621.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;full&quot;,&quot;height&quot;:911,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:10226042,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-fullscreen" alt="" srcset="https://substackcdn.com/image/fetch/$s_!YaCg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda5e2fb8-02fe-4ad1-a83a-a946fec4ae32_7389x4621.jpeg 424w, https://substackcdn.com/image/fetch/$s_!YaCg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda5e2fb8-02fe-4ad1-a83a-a946fec4ae32_7389x4621.jpeg 848w, https://substackcdn.com/image/fetch/$s_!YaCg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda5e2fb8-02fe-4ad1-a83a-a946fec4ae32_7389x4621.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!YaCg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda5e2fb8-02fe-4ad1-a83a-a946fec4ae32_7389x4621.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Mutable subresources as a cause of issues with SXG, inspired by <em>Perseus Rescuing Andromeda</em> (1727) by Charles-Antoine Coypel, oil on canvas, 131 &#215; 196 cm. Am I the only one who sees the monster crying?</figcaption></figure></div><blockquote><p>Context (Oct 2025): After deep work with SXG, I suspected changes might be coming&#8212;just not this quickly. Cloudflare <a href="https://community.cloudflare.com/t/amp-and-signed-exchanges-deprecation-october-20th/831238">announced SXG deprecation</a>, and I observed SXG stop working around Sept 19, 2025.</p><p>If you still want SXG, the current path is to self-host&#8212;but the tooling looks abandoned, and setup is non-trivial. Also, Google may follow Cloudflare by shutting down the SXG cache and removing SXG support in Chrome.</p></blockquote><p>In <a href="https://www.pawelpokrywka.com/p/understanding-cors-sxg-errors">the previous part</a>, you learned CORS errors mean (sub)resources are missing from the Google Signed Exchanges (SXG) cache. This and later posts will explain how to deal with that.</p><p>I suggest you also read the <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">first</a> <a href="https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges">two</a> parts to fully understand what is happening. They contain fundamentals on how to adjust your website to be SXG-compatible.</p><p>This and later posts summarize my research on issues related to the prefetching of SXG subresources. As I write, <strong>there is no official or unofficial documentation</strong> on most of the challenges you may encounter. My articles aim to fill that gap.</p><h2>Have a way to invalidate all the caches</h2><p>When you work with SXG subresources prefetching issues, you may want to make sure you can observe the impact on your changes. It may be non-trivial because of caching.</p><p>Many caches sit between your app and you. Here is a <strong>minimal</strong> set of caches:</p><ul><li><p>Cloudflare caches<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a></p></li><li><p>Google SXG cache</p></li><li><p>Browser cache</p></li></ul><p>Depending on your configuration, you may also use app cache, <a href="https://nextjs.org/docs/app/building-your-application/caching">framework</a> <a href="https://guides.rubyonrails.org/caching_with_rails.html">cache</a>, <a href="https://nginx.org/en/docs/http/ngx_http_proxy_module.html">web server cache</a>, <a href="https://developers.cloudflare.com/cache/how-to/tiered-cache/">additional layers at Cloudflare</a>, and potentially others.</p><p>After implementing the changes, you could clear all of the involved caches, but that&#8217;s a lot of work and sometimes it&#8217;s even impossible.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PnqS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19be768f-3ae3-4354-88f6-1c8a2bf2fd37_6527x4581.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PnqS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19be768f-3ae3-4354-88f6-1c8a2bf2fd37_6527x4581.jpeg 424w, https://substackcdn.com/image/fetch/$s_!PnqS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19be768f-3ae3-4354-88f6-1c8a2bf2fd37_6527x4581.jpeg 848w, https://substackcdn.com/image/fetch/$s_!PnqS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19be768f-3ae3-4354-88f6-1c8a2bf2fd37_6527x4581.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!PnqS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19be768f-3ae3-4354-88f6-1c8a2bf2fd37_6527x4581.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PnqS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19be768f-3ae3-4354-88f6-1c8a2bf2fd37_6527x4581.jpeg" width="1456" height="1022" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/19be768f-3ae3-4354-88f6-1c8a2bf2fd37_6527x4581.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1022,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6646008,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!PnqS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19be768f-3ae3-4354-88f6-1c8a2bf2fd37_6527x4581.jpeg 424w, https://substackcdn.com/image/fetch/$s_!PnqS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19be768f-3ae3-4354-88f6-1c8a2bf2fd37_6527x4581.jpeg 848w, https://substackcdn.com/image/fetch/$s_!PnqS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19be768f-3ae3-4354-88f6-1c8a2bf2fd37_6527x4581.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!PnqS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19be768f-3ae3-4354-88f6-1c8a2bf2fd37_6527x4581.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Two hard things in computer science, inspired by <em>Untitled, known as A Philosopher Giving that Lecture on the Orrery, in which a Lamp is put in place of the Sun or The Orrery</em> (~1766) by Joseph Wright of Derby, oil on canvas, 147.2 &#215; 203.2 cm</figcaption></figure></div><p>It&#8217;s much easier to just make sure the URLs you are about to test were not accessed before and, therefore are guaranteed to be absent from the cache. After making an SXG-specific change to your app or configuration, make sure that:</p><ol><li><p><strong>You are testing a different subpage of your website.</strong> In my case, I can access my app under different URLs for each Polish city &amp; vendor category combination. It gives an almost unlimited set of URLs I can use for testing.</p></li><li><p><strong>URLs of your assets change.</strong> This is important during the reconfiguration of HTTP headers and other subresource-specific modifications explained later. Every framework has its own ways of achieving that.</p></li></ol><p>As my website combines Ruby on Rails and Next.js, I will demonstrate how to invalidate cache in those frameworks. Different frameworks likely have their own ways of achieving that, but some concepts may remain the same.</p><h4>Invalidate the cache in Rails</h4><p>Rails makes it easy. As long as you use asset helper methods (<strong>image_tag</strong>, <strong>stylesheet_link_tag</strong>, etc) to reference your assets, just increment <a href="https://guides.rubyonrails.org/configuring.html#config-assets-version">the version of the assets</a> in <strong>config/initializers/assets.rb</strong> file:</p><pre><code>Rails.application.config.assets.version = '1'</code></pre><h4>Invalidate the cache in Next.js</h4><p>This framework uses two forms of referencing assets:</p><ol><li><p><strong>Direct</strong>: the developer specifies the path to the asset in the CSS, HTML tag, or React component. In the context of SXG, it is used mostly for images and fonts.</p></li><li><p><strong>Autogenerated</strong>: the framework compiles javascript and CSS into chunks and places them inside the HTML head of the document (and <a href="https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges">our custom Cloudflare worker</a> puts them in the <strong>Link</strong> header). The developer has limited control over the URLs.</p></li></ol><h5>Direct paths</h5><p>In the case of directly referenced assets, the simple solution is to move all your assets into a subdirectory named after a version and add a version prefix while referencing URLs.</p><p>So instead of using:</p><pre><code>&lt;img src="/icons/icon.svg"&gt;</code></pre><p>You will use:</p><pre><code>&lt;img src="/<strong>1</strong>/icons/icon.svg"&gt;</code></pre><p>When invalidating the cache, you rename the directory to &#8220;2&#8221; and update all references using global search-and-replace in your code editor. It&#8217;s a good idea to create a function returning the versioned asset URL and store the current version in a global ENV variable:</p><pre><code>function assetPath(path) {
  return `/${process.env.ASSETS_VERSION}/${path.replace(/^\//, '')}`;
}</code></pre><p>Make sure to include the current version in your <strong>.env</strong> file. This way you have a single place where you can update the version (along with renaming the assets version directory):</p><pre><code><code>ASSETS_VERSION=1</code></code></pre><p>Referencing the assets would look like this:</p><pre><code><code>&lt;img src={assetPath("/icons/icon.svg")}&gt;</code></code></pre><h5>Autogenerated paths</h5><blockquote><p>Next.js has a built-in <a href="https://nextjs.org/docs/app/api-reference/config/next-config-js/assetPrefix">assetPrefix</a> configuration parameter that makes it possible to embed version numbers into the URLs. You'll still need to move the assets to the prefixed location during deployment. The specific instructions will vary depending on your deployment method, therefore I won&#8217;t provide them here and describe a more generic solution instead.</p></blockquote><p>For autogenerated URLs, the Next.js config has to be adjusted to include the version number stored in the <strong>ASSETS_VERSION</strong> configuration variable. Extend the configuration object in your <strong>next.config.js</strong> file using the following template:</p><pre><code>const NextMiniCssExtractPlugin = require('next/dist/compiled/mini-css-extract-plugin');

function addVersionToAssets(config) {
  const ver = process.env.ASSETS_VERSION;
  const addVer = (text) =&gt; text.replace('[name]', `[name]-${ver}`);

  // Include version in script URLs. For more context see:
  // https://www.pawelpokrywka.com/p/fixing-sxg-prefetching-errors-caused-by-mutable-subresources
  const filename = config.output.filename;
  const chunkFilename = config.output.chunkFilename;
  if (filename &amp;&amp; chunkFilename &amp;&amp; filename.startsWith('static')) {
    config.output.filename = addVer(filename);
    config.output.chunkFilename = addVer(chunkFilename);
  }

  // CSS URLs are handled by a plugin. It needs to be reconstructed
  // with a template containing the version prefix.
  const index = config.plugins.findIndex(
    (plugin) =&gt; plugin.constructor.name === 'NextMiniCssExtractPlugin'
  );
  const template = `static/css/${ver}-[contenthash].css`;
  if (index !== -1) {
    config.plugins[index] = new NextMiniCssExtractPlugin({
      filename: template, chunkFilename: template
    });
  }
}


const nextConfig = {

  // Your current config options

  webpack: (config, options) =&gt; {
    <strong>if (!options.dev) addVersionToAssets(config);

    </strong>// Your current webpack customizations go here

    return config;
  }
}
 
module.exports = nextConfig</code></pre><p>After building the app, you will notice that your JS chunks and CSS files have embedded version numbers. To invalidate the cache, increment the version stored in <strong>ASSETS_VERSION</strong> in your <strong>.env</strong> file and rebuild.</p><blockquote><p>One of the chunks generated by Next.js, the <strong>polyfills</strong> chunk is:</p><ul><li><p>loaded with a <strong>nomodule</strong> attribute, which ensures that only legacy browsers without support for ES modules will download it,</p></li><li><p>treated specially by Next.js making it non-trivial to include a version in the URL.</p></li></ul><p>Rather than invalidating this particular chunk, it's more efficient to avoid preloading it altogether. Prefetching polyfills for modern browsers is wasteful since they don't need them. The worker that adds the <strong>Link</strong> header (described in <a href="https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges">the second part</a> of the series) already ignores <strong>&lt;script&gt;</strong> tags with the <strong>nomodule</strong> attribute.</p><p>Given all of the above, you don&#8217;t need to worry about invalidating the <strong>polyfills</strong> chunk.</p></blockquote><h4>Subresources stored externally</h4><p>I believe this should be rare, so I won't go into details here. Sometimes, you may need to change the HTTP headers of your subresources hosted externally and <a href="https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges">proxied using a Cloudflare worker</a>.</p><p>In this case, the URLs of subresources should change too.</p><ul><li><p>Your app should include version numbers in URLs of external subresources using an approach similar to the <strong>assetPath</strong> function I described earlier.</p></li><li><p>On the worker side, you can implement it by including the version number in the URL map.</p></li></ul><pre><code>const MAP = {
  'https://www.your-domain.com/cdn-proxy/<strong>PUT_THE_VERSION_HERE</strong>':
  'https://cdn.com/your-bucket/',
  // You may add more mappings such as:
  // 'external-sxg-prefetchable-url': 'cdn-url'
}

// Rest of the worker logic</code></pre><p>The production-grade solution would use a regexp accepting any version, so the worker code doesn&#8217;t need to be updated every time the version changes. I leave it as an exercise for the reader.</p><blockquote><p>In the case of a proxying worker, Cloudflare uses original, CDN-provided URLs as cache keys, not URLs the worker exposes to your users. Even if you modify the exposed URLs, the original URLs won&#8217;t change meaning stale cache entries will be used. Remember to purge the Cloudflare cache to avoid that, unless you modify your subresources in the worker only, without changing them on the CDN.</p></blockquote><h2>Google SXG cache ingestion process</h2><p>Before we dive deeply into the causes of missing entries in the SXG cache, it&#8217;s critical to understand how Google ingests your pages and how it cooperates with Cloudflare.</p><h4>Reverse engineering the process</h4><p>I modeled this process using reverse engineering. It was created out of necessity, to help me understand the issues I was experiencing. I was performing requests and observing responses. Then, I was analyzing logs from the server and Cloudflare.</p><p>As I write this post, the resulting process isn&#8217;t documented anywhere else.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">If you enjoy reverse engineering like I do, subscribe for more content like this.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><p>After landing on the Google search results page, this page tries to prefetch a result the user is predicted to click. The following diagram illustrates what happens. For simplicity, it assumes the page depends on only one subresource (CSS file):</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!V2WI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea514eb3-ca34-4b47-b8c9-fdd8821b163e_2508x3840.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!V2WI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea514eb3-ca34-4b47-b8c9-fdd8821b163e_2508x3840.png 424w, https://substackcdn.com/image/fetch/$s_!V2WI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea514eb3-ca34-4b47-b8c9-fdd8821b163e_2508x3840.png 848w, https://substackcdn.com/image/fetch/$s_!V2WI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea514eb3-ca34-4b47-b8c9-fdd8821b163e_2508x3840.png 1272w, https://substackcdn.com/image/fetch/$s_!V2WI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea514eb3-ca34-4b47-b8c9-fdd8821b163e_2508x3840.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!V2WI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea514eb3-ca34-4b47-b8c9-fdd8821b163e_2508x3840.png" width="728" height="1114.5" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ea514eb3-ca34-4b47-b8c9-fdd8821b163e_2508x3840.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:2229,&quot;width&quot;:1456,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:522956,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!V2WI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea514eb3-ca34-4b47-b8c9-fdd8821b163e_2508x3840.png 424w, https://substackcdn.com/image/fetch/$s_!V2WI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea514eb3-ca34-4b47-b8c9-fdd8821b163e_2508x3840.png 848w, https://substackcdn.com/image/fetch/$s_!V2WI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea514eb3-ca34-4b47-b8c9-fdd8821b163e_2508x3840.png 1272w, https://substackcdn.com/image/fetch/$s_!V2WI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fea514eb3-ca34-4b47-b8c9-fdd8821b163e_2508x3840.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Google SXG cache population diagram.</figcaption></figure></div><p>When the first user requests the page, the prefetching initially fails because the page has not yet been cached. However, <strong>this request triggers the cache population process</strong>. Consequently, the second user (starting at step <strong>15</strong>) benefits from the now-prefetched page, experiencing faster load times.</p><blockquote><p>You may want to open the diagram in a new tab to avoid repeated scrolling&#8212;it will be easier to switch between tabs as I reference it throughout this discussion.</p></blockquote><p>Understanding the Google SXG cache population process (steps <strong>3-14</strong>) is crucial in debugging potential errors. Observe the clean separation of responsibilities:</p><ul><li><p>Cloudflare Automated Signed Exchanges (ASX) is a layer wrapping HTTP responses into SXG and doing nothing else.</p></li><li><p>Cloudflare cache knows nothing about SXG, and neither does the server.</p></li></ul><h4>Requests differences</h4><p>During the cache ingestion, subresources are requested by different systems for different purposes:</p><ul><li><p><strong>Cloudflare ASX</strong> for computing integrity hashes that will be included in the SXG-wrapped page,</p></li><li><p><strong>Google SXG cache</strong> for making subresources available to browsers for prefetching.</p></li></ul><p>Let's compare the requests generated by these systems to better understand potential issues.</p><p>Here are interesting headers from an example request made by <strong>Cloudflare ASX</strong>:</p><pre><code>Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, br
Cf-Connecting-Ip: 2a06:98c0:3600::103</code></pre><p>Observations:</p><ul><li><p>The request lacks a <strong>User-Agent</strong> header.</p></li><li><p>The <strong>Accept</strong> header doesn&#8217;t mention Signed Exchanges at all.</p></li><li><p>The IP address <a href="https://ipinfo.io/2a06:98c0:3600::103">belongs to Cloudflare</a>.</p></li></ul><p>Let's examine some notable headers from a sample <strong>Google SXG cache</strong> request:</p><pre><code>Accept: */*;q=0.8,application/signed-exchange;v=b3
Cf-Connecting-Ip: 66.249.72.7
User-Agent: Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.119 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)</code></pre><p>Observations:</p><ul><li><p>The request lacks an <strong>Accept-Encoding</strong> header.</p></li><li><p>The <strong>Accept</strong> header indicates that Google prioritizes SXG over other formats.</p></li><li><p>The IP address <a href="https://ipinfo.io/66.249.72.7">belongs to Google</a>.</p></li></ul><h4>Watching the SXG cache ingestion live</h4><p>I created a tool to demonstrate the ingestion process. It allows you to <a href="https://www.planujemywesele.pl/sxg-tests/no-cache/requests-logging">trigger the SXG cache population and observe requests made by Google and ASX</a>. The tool uses the presence or absence of the <strong>User-Agent</strong> header to determine if the request is performed by Google or Cloudflare respectively.</p><p>Using the tool you may notice that trying to prefetch a page already present in the Google SXG cache (step <strong>15</strong>) often has a side-effect of triggering the cache population process to refresh the entry. I intentionally didn&#8217;t include it on the diagram to keep it readable.</p><blockquote><p>Keep in mind that the tool bypasses Cloudflare's cache, which is both a critical component in real-world applications and a significant indirect source of SXG-related errors.</p></blockquote><h2>Mutable subresources</h2><p>I define the <em>mutable</em> subresource, as one that changes over time but remains accessible under the same URL. In contrast, an <em>immutable</em> subresource doesn&#8217;t change.</p><p>From my experience, most of the hard-to-debug SXG issues are caused by mutable subresources.</p><h4>Why does a mutable subresource break things?</h4><p>Let&#8217;s assume we have a page with a subresource that changes on each request. It may be the Web 1.0 visit counter - each time it is requested, it returns an incremented number as an image. The server keeps the current number, increases it on every request, generates a GIF, and returns it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dZFg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45612b69-d38a-4a86-b1f4-68351af53d54_838x630.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dZFg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45612b69-d38a-4a86-b1f4-68351af53d54_838x630.jpeg 424w, https://substackcdn.com/image/fetch/$s_!dZFg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45612b69-d38a-4a86-b1f4-68351af53d54_838x630.jpeg 848w, https://substackcdn.com/image/fetch/$s_!dZFg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45612b69-d38a-4a86-b1f4-68351af53d54_838x630.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!dZFg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45612b69-d38a-4a86-b1f4-68351af53d54_838x630.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dZFg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45612b69-d38a-4a86-b1f4-68351af53d54_838x630.jpeg" width="838" height="630" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/45612b69-d38a-4a86-b1f4-68351af53d54_838x630.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:630,&quot;width&quot;:838,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:153134,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dZFg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45612b69-d38a-4a86-b1f4-68351af53d54_838x630.jpeg 424w, https://substackcdn.com/image/fetch/$s_!dZFg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45612b69-d38a-4a86-b1f4-68351af53d54_838x630.jpeg 848w, https://substackcdn.com/image/fetch/$s_!dZFg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45612b69-d38a-4a86-b1f4-68351af53d54_838x630.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!dZFg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45612b69-d38a-4a86-b1f4-68351af53d54_838x630.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">There was a time when a hit counter was a must-have.</figcaption></figure></div><p>We need to disable the Cloudflare cache. Otherwise, it would cache the counter, effectively stopping it.</p><blockquote><p>Please remember, this is just an example prepared to illustrate the point. It doesn&#8217;t make much sense to implement it in real-world use cases.</p></blockquote><p>What will happen, when Google tries to fetch this and put it into its SXG cache?</p><p>According to the diagram above:</p><ol><li><p>It requests the page from ASX (step <strong>3</strong>).</p></li><li><p>ASX requests the page from the server and the counter image subresource (steps <strong>4</strong> and <strong>6</strong>). The server generates an image presenting number 1.</p></li><li><p>ASX generates an SXG-wrapped page, includes the subresource URL along with its integrity hash, and returns it to Google (step <strong>10</strong>).</p></li><li><p>Google parses the SXG-wrapped page, finds it depends on the subresource, and downloads it from ASX (step <strong>11</strong>).</p></li><li><p>As Cloudflare cache is disabled, ASX downloads it directly from the server (step <strong>12</strong>). This time, the server generates an image with the number 2.</p></li><li><p>ASX wraps the image into the SXG package and returns it to Google (step <strong>13</strong>).</p></li><li><p>Google compares the integrity hash of the downloaded subresource with the integrity hash stored in the SXG-wrapped page. <strong>As those differ, Google rejects the subresource as invalid and doesn&#8217;t store it in the cache.</strong></p></li></ol><p>The integrity hashes were different because those were different images. First represented number 1, second number 2.</p><p>Still, the SXG-wrapped page will be stored in the Google SXG cache. However, it will depend on a subresource that can&#8217;t be prefetched, because it&#8217;s missing from the cache.</p><p>On Google search results, when prefetching the page, the browser will be instructed to prefetch the subresource located under a URL computed from the subresource integrity hash included in the SXG-wrapped page. <strong>As the URL doesn&#8217;t contain a subresource, the fallback HTML page will be returned instead causing a CORS error in the browser.</strong></p><h4>But my subresources are immutable!</h4><p>If you follow best practices in web development, you probably try to keep your assets immutable. The reason is to prevent issues caused by stale copies stored in caches.</p><p>Frameworks such as Rails and Next.js offer include built-in solutions. The idea is to bind the asset URL and its content by making the digest of the content part of the URL. This way changing the content changes the URL, so the asset won&#8217;t mutate.</p><p>It works well with the original stale-cache issue, but&#8230;</p><h4>SXG subresource &gt; file content + URL</h4><p>The above approach doesn&#8217;t consider HTTP headers returned along with the asset. Those headers are part of the SXG subresource. If one of the headers changes, the integrity hash changes as well.</p><p>Many HTTP headers are set by the web server, outside of the web application. The framework doesn&#8217;t control all of them, therefore it can&#8217;t include them in the digest.</p><p>Cloudflare ASX partially solves that by stripping headers known to <a href="https://github.com/google/sxg-rs/blob/06d31585c95cd23c2c6a9f5f5b6fc5eb247bcfdd/sxg_rs/src/headers.rs#L347">change</a> <a href="https://github.com/google/sxg-rs/blob/06d31585c95cd23c2c6a9f5f5b6fc5eb247bcfdd/sxg_rs/src/id_headers.rs">frequently</a>, such as <strong>Date</strong>, <strong>Age,</strong> or <strong>X-Request-Id</strong> from subresources. But if your assets use other unstable headers, you may experience issues, as shown in <a href="https://www.planujemywesele.pl/sxg-tests/no-cache/unstable-subresource">this demo</a>.</p><h4>Doesn&#8217;t cache fix that?</h4><p>As I wrote <a href="https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges">earlier</a>, subresources should be cachable. If you want to see what happens, if the <strong>Cache-Control</strong> header is invalid, here is <a href="https://www.planujemywesele.pl/sxg-tests/subresource-cache-control">a demo</a>.</p><p>Cloudflare has an HTTP cache enabled by default. Even if a given subresource changes on each request, the first response will be stored in the cache (steps <strong>6-9</strong> on the SXG cache population diagram), and later requests will return the cached entry (steps <strong>12-13</strong>). </p><p>HTTP headers are cached too. Therefore, from the Google SXG cache perspective, the mutable subresource doesn&#8217;t change. That&#8217;s good!</p><p>Things become interesting, if the subresource gets modified <em>after</em> being retrieved from the cache or if <em>the cache infrastructure</em> has issues.</p><h2>Compression negotiation</h2><p>When performing an HTTP request, the browser typically uses the <strong>Accept-Encoding</strong> header to tell the server what compression methods it supports. It allows the server to use the best compression the browser understands, for example:</p><ul><li><p>If the browser supports <strong>zstd </strong>and<strong> gzip</strong>, the server will respond with a zstd-compressed response.</p></li><li><p>If the browser supports only <strong>gzip</strong>, the server will respond with it.</p></li></ul><p>The server will unilaterally decide if and how to compress the response if the browser doesn't set the <strong>Accept-Encoding</strong> header.</p><p>The problem arises when caching is involved. The cache stores the response under a <em>cache key</em>, typically created from the URL. However, accessing the same URL may result in a compression mismatch. That&#8217;s why the component performing the compression (the web server or Cloudflare proxy) should inform the cache, the URL is not enough to construct the cache key.</p><h4>Vary HTTP header</h4><p>It does it by setting the <strong>Vary</strong> header to a list of request headers that may impact the response. In the case of compression, it puts <strong>Accept-Encoding</strong> there:</p><pre><code>Vary: Accept-Encoding</code></pre><p>Importantly, the <strong>Vary</strong> header may be omitted if the request doesn&#8217;t contain the <strong>Accept-Encoding</strong> header.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!f-lh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabc490a7-1d6e-45a3-b0e2-83986fd53465_4000x2838.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!f-lh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabc490a7-1d6e-45a3-b0e2-83986fd53465_4000x2838.jpeg 424w, https://substackcdn.com/image/fetch/$s_!f-lh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabc490a7-1d6e-45a3-b0e2-83986fd53465_4000x2838.jpeg 848w, https://substackcdn.com/image/fetch/$s_!f-lh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabc490a7-1d6e-45a3-b0e2-83986fd53465_4000x2838.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!f-lh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabc490a7-1d6e-45a3-b0e2-83986fd53465_4000x2838.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!f-lh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabc490a7-1d6e-45a3-b0e2-83986fd53465_4000x2838.jpeg" width="1456" height="1033" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/abc490a7-1d6e-45a3-b0e2-83986fd53465_4000x2838.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1033,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5889111,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!f-lh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabc490a7-1d6e-45a3-b0e2-83986fd53465_4000x2838.jpeg 424w, https://substackcdn.com/image/fetch/$s_!f-lh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabc490a7-1d6e-45a3-b0e2-83986fd53465_4000x2838.jpeg 848w, https://substackcdn.com/image/fetch/$s_!f-lh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabc490a7-1d6e-45a3-b0e2-83986fd53465_4000x2838.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!f-lh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fabc490a7-1d6e-45a3-b0e2-83986fd53465_4000x2838.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Caching with compression negotiation inspired by <em>The Meeting of Antony and Cleopatra, 41 B.C.</em> by Lawrence Alma-Tadema, oil on panel, 65.4 &#215; 91.4 cm</figcaption></figure></div><p>As HTTP headers are part of the subresource, the presence of the <strong>Vary</strong> header impacts the integrity hash. It could be therefore possible to generate two different versions of the same subresource:</p><ul><li><p>with <strong>Vary</strong> header set, if the request contains an <strong>Accept-Encoding</strong> header,</p></li><li><p>without it otherwise.</p></li></ul><p>Fortunately, Cloudflare ASX doesn&#8217;t allow it by removing the <strong>Vary</strong> header from the response. However, I found one edge case when it&#8217;s not the case.</p><h4>Subresource mutation caused by ASX itself</h4><p>The issue occurs when the subresource to prefetch is behind a worker and uses Cloudflare cache. There are at least 2 reasons for such a setup, both were described <a href="https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges">earlier</a>:</p><ul><li><p>rewriting URLs of CDN-stored assets,</p></li><li><p>adding a <strong>Link</strong> header to all HTML responses by passing all traffic through the worker and letting it skip assets instead of configuring worker routing to avoid assets being processed.</p></li></ul><p>I used reverse engineering again to model what happens. Here is a simplified version of the previous diagram:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!n1as!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83f64627-3fdd-4ac3-8bcb-44cef9be1c3c_2907x3840.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!n1as!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83f64627-3fdd-4ac3-8bcb-44cef9be1c3c_2907x3840.png 424w, https://substackcdn.com/image/fetch/$s_!n1as!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83f64627-3fdd-4ac3-8bcb-44cef9be1c3c_2907x3840.png 848w, https://substackcdn.com/image/fetch/$s_!n1as!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83f64627-3fdd-4ac3-8bcb-44cef9be1c3c_2907x3840.png 1272w, https://substackcdn.com/image/fetch/$s_!n1as!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83f64627-3fdd-4ac3-8bcb-44cef9be1c3c_2907x3840.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!n1as!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83f64627-3fdd-4ac3-8bcb-44cef9be1c3c_2907x3840.png" width="1456" height="1923" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/83f64627-3fdd-4ac3-8bcb-44cef9be1c3c_2907x3840.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1923,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:573660,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!n1as!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83f64627-3fdd-4ac3-8bcb-44cef9be1c3c_2907x3840.png 424w, https://substackcdn.com/image/fetch/$s_!n1as!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83f64627-3fdd-4ac3-8bcb-44cef9be1c3c_2907x3840.png 848w, https://substackcdn.com/image/fetch/$s_!n1as!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83f64627-3fdd-4ac3-8bcb-44cef9be1c3c_2907x3840.png 1272w, https://substackcdn.com/image/fetch/$s_!n1as!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F83f64627-3fdd-4ac3-8bcb-44cef9be1c3c_2907x3840.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">SXG cache ingesting a page with a mutated subresource.</figcaption></figure></div><p>First, ASX asks for a subresource with an <strong>Accept-Encoding</strong> header set to "gzip, br" (step <strong>4</strong>). Therefore, as we discussed, the response includes <strong>Accept-Encoding</strong> in the <strong>Vary</strong> header.</p><p>The integrity header of the subresource is calculated and put in the Link header of the SXG-wrapped page returned to the Google SXG cache in step <strong>6</strong>.</p><p>Next, the request for subresource comes from Google (step <strong>7</strong>) and is proxied by ASX (step <strong>8</strong>). Google doesn't set the <strong>Accept-Encoding</strong> header and ASX honors the original request&#8217;s headers<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. Therefore the response doesn't include the <strong>Vary</strong> header (step <strong>9</strong>).</p><p>When Google receives the subresource (step <strong>10</strong>) it compares its integrity hash with the one from the <strong>Link</strong> header of the SXG-wrapped page it received in step <strong>6</strong>. As they don&#8217;t match, the subresource is discarded and not cached.</p><p>The subresource is not cached, but it&#8217;s still referenced from the cached SXG-wrapped page. <strong>When the browser tries to prefetch it, a fallback page is returned and a CORS error happens.</strong></p><p>It&#8217;s worth noting, this issue doesn&#8217;t happen when:</p><ul><li><p>the Cloudflare cache is disabled&#8212;probably because <strong>Vary</strong> is a cache-related header,</p></li><li><p>not using a worker&#8212;probably because of interactions between a worker and ASX (ASX being a special instance of a worker).</p></li></ul><p>To see it in action, take a look at <a href="https://www.planujemywesele.pl/sxg-tests/subresource-vary-header">the demonstration</a>.</p><h4>Workarounds for ASX bug</h4><p>Until Cloudflare fixes this issue, the simple solution is to set the <strong>Vary</strong> header for all static assets responses, even when requests don&#8217;t specify the <strong>Accept-Encoding</strong> header.</p><h5>Nginx</h5><p>For assets included with the application, you can use the following nginx configuration snippet in locations containing static files:</p><pre><code><code>location ~* \.(?:css|js|gif|png|jpeg|jpg|ico|ttf|woff|woff2|svg)$ {
  # Always set Vary header, because of the issue in Cloudflare ASX
  # when used along with workers
  more_set_headers 'Vary: Accept-Encoding';

  # The rest of assets-related configuration, such as cache expiration
}</code></code></pre><p>Of course, the above solution doesn&#8217;t work for assets stored in the CDN and proxied through a worker.</p><h5>Worker</h5><p>Therefore, the more elegant solution is to implement the workaround in the worker itself. This way the fix doesn&#8217;t pollute the system and is contained closest to the cause of the issue (the worker). The downside is you need to remember about it in every worker if you use many.</p><p>Here is a minimal worker setting the <strong>Vary</strong> header:</p><pre><code>function addVaryHeader(response) {
  const vary = response.headers.get('vary');
  if (!vary || !vary.toLowerCase().includes('accept-encoding')) {
    // Set the Vary header because of the issue in Cloudflare ASX
    // when used along with workers. For more details see:
    // https://www.pawelpokrywka.com/p/fixing-sxg-prefetching-errors-caused-by-mutable-subresources
    const newResponse = new Response(response.body, response);
    newResponse.headers.set('vary', 'Accept-Encoding');
    return newResponse;
  }

  return response;
}

export default {
  async fetch(request) {
    const response = await fetch(request);

    // Add Vary header to all responses.
    // For production, I would suggest doing so only for assets.
    return addVaryHeader(response);
  },
};</code></pre><p>I won&#8217;t rehash <a href="https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges">how to create and deploy Cloudflare workers</a>. I leave it as an exercise for you to merge this code with your workers, but it should be easy. Just wrap the response you are about to return with the <strong>addVaryHeader()</strong> function for static assets.</p><h5>Transform rule</h5><p>Alternatively, you can create a Cloudflare transform rule that adds the <strong>Vary</strong> header to all responses for static assets. The adventage of this approach is that you need to do this once and it benefits all the workers. To do that:</p><ol><li><p>Open your website in the Cloudflare dashboard.</p></li><li><p>Go to <strong>Rules</strong> &#8594; <strong>Transform Rules</strong> &#8594; <strong>Modify Response Header</strong>.</p></li><li><p>Hit the <strong>Create rule</strong> button.</p></li><li><p>Provide a name for your rule, for example: <em>Force Vary: Accept-Encoding for static assets</em>.</p></li><li><p>Click the <strong>Edit expression</strong> link in the Expression Preview section and paste the following expression into the text area:<br><em>(ends_with(http.request.uri.path, "css")) or (ends_with(http.request.uri.path, "js")) or (ends_with(http.request.uri.path, "gif")) or (ends_with(http.request.uri.path, "png")) or (ends_with(http.request.uri.path, "jpeg")) or (ends_with(http.request.uri.path, "jpg")) or (ends_with(http.request.uri.path, "ico")) or (ends_with(http.request.uri.path, "ttf")) or (ends_with(http.request.uri.path, "woff")) or (ends_with(http.request.uri.path, "woff2")) or (ends_with(http.request.uri.path, "svg"))</em></p></li><li><p>In the <strong>Then</strong> section select <em>Set static</em> from the menu and type <em>Vary</em> into the <strong>Header name</strong> and <em>Accept-Encoding</em> into the <strong>Value</strong>.</p></li></ol><p>The form should look similar to the one below:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yuzh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5f42c61-a1c4-4459-a2a2-2b4c63aa0005_2131x2145.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yuzh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5f42c61-a1c4-4459-a2a2-2b4c63aa0005_2131x2145.png 424w, https://substackcdn.com/image/fetch/$s_!yuzh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5f42c61-a1c4-4459-a2a2-2b4c63aa0005_2131x2145.png 848w, https://substackcdn.com/image/fetch/$s_!yuzh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5f42c61-a1c4-4459-a2a2-2b4c63aa0005_2131x2145.png 1272w, https://substackcdn.com/image/fetch/$s_!yuzh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5f42c61-a1c4-4459-a2a2-2b4c63aa0005_2131x2145.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yuzh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5f42c61-a1c4-4459-a2a2-2b4c63aa0005_2131x2145.png" width="1456" height="1466" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a5f42c61-a1c4-4459-a2a2-2b4c63aa0005_2131x2145.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1466,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:530825,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yuzh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5f42c61-a1c4-4459-a2a2-2b4c63aa0005_2131x2145.png 424w, https://substackcdn.com/image/fetch/$s_!yuzh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5f42c61-a1c4-4459-a2a2-2b4c63aa0005_2131x2145.png 848w, https://substackcdn.com/image/fetch/$s_!yuzh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5f42c61-a1c4-4459-a2a2-2b4c63aa0005_2131x2145.png 1272w, https://substackcdn.com/image/fetch/$s_!yuzh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa5f42c61-a1c4-4459-a2a2-2b4c63aa0005_2131x2145.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Adding a transform rule that sets the Vary header. I removed uninteresting parts for readability.</figcaption></figure></div><p>If everything is ok, hit the <strong>Deploy</strong> button. From now on, your assets will always have the <strong>Vary</strong> header set to <strong>Accept-Encoding</strong>.</p><blockquote><h5>Cache invalidation reminder</h5><p>It&#8217;s a good idea to invalidate the cache after performing those changes as described at the beginning of this post. Otherwise, you may need to wait up to 7 days for the results.</p></blockquote><h2><strong>HTTP/2 Prioritization</strong></h2><p>In 2019 Cloudflare <a href="https://blog.cloudflare.com/better-http-2-prioritization-for-a-faster-web/">announced</a> a new feature called <em>Enhanced HTTP/2 Prioritization</em>. It promises improved page load times by optimizing asset delivery so that the browser fetches the most important assets before others.</p><p>The same day, they <a href="https://blog.cloudflare.com/parallel-streaming-of-progressive-images/">introduced</a> a parallel streaming of progressive images. It uses the same mechanism but for data ranges within individual image files. For example, it prioritizes the beginning of a JPEG file containing a low-resolution image. In effect, on pages with more than one image, the browser first renders all of them in low quality, then continues to download them, gradually improving the resolution.</p><p>Those features promise improvements in the UX, so I was eager to turn them on. Both are controlled by one switch:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Vevw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63f617b7-31c6-4682-981b-84c7bad3a7ea_2134x789.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Vevw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63f617b7-31c6-4682-981b-84c7bad3a7ea_2134x789.png 424w, https://substackcdn.com/image/fetch/$s_!Vevw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63f617b7-31c6-4682-981b-84c7bad3a7ea_2134x789.png 848w, https://substackcdn.com/image/fetch/$s_!Vevw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63f617b7-31c6-4682-981b-84c7bad3a7ea_2134x789.png 1272w, https://substackcdn.com/image/fetch/$s_!Vevw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63f617b7-31c6-4682-981b-84c7bad3a7ea_2134x789.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Vevw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63f617b7-31c6-4682-981b-84c7bad3a7ea_2134x789.png" width="1456" height="538" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/63f617b7-31c6-4682-981b-84c7bad3a7ea_2134x789.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:538,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:103420,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Vevw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63f617b7-31c6-4682-981b-84c7bad3a7ea_2134x789.png 424w, https://substackcdn.com/image/fetch/$s_!Vevw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63f617b7-31c6-4682-981b-84c7bad3a7ea_2134x789.png 848w, https://substackcdn.com/image/fetch/$s_!Vevw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63f617b7-31c6-4682-981b-84c7bad3a7ea_2134x789.png 1272w, https://substackcdn.com/image/fetch/$s_!Vevw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F63f617b7-31c6-4682-981b-84c7bad3a7ea_2134x789.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">A switch enabling Cloudflare Enhanced HTTP/2 Prioritization feature.</figcaption></figure></div><h4>SXG doesn&#8217;t like HTTP/2 prioritization</h4><p>I found a lot of prefetching errors were related to JPEG files. After examining the issue, I found that responses for JPEG files contained an HTTP header missing from other responses:</p><pre><code>Cf-Bgj: h2pri</code></pre><p>According to <a href="https://community.cloudflare.com/t/whats-is-this-header-cf-bgj-h2pri/170883/14">a response</a> from a Cloudflare employee, the <strong>h2pri</strong> means it&#8217;s been processed to support HTTP/2 prioritization for Progressive Streaming of JPEGs.</p><p>To make things more interesting, the header is set on the second response (cache HIT), while the first response (cache MISS) doesn&#8217;t include it.</p><blockquote><p>Processing takes some time, therefore I assume it is being put in the background and the first response doesn&#8217;t wait for it to keep the latency low. The processed image lands in the cache and that&#8217;s the reason it includes the <strong>Cf-Bgj</strong> (<strong>C</strong>loud<strong>f</strong>lare <strong>B</strong>ack<strong>g</strong>round <strong>J</strong>ob?) header.</p></blockquote><p>The subresource mutates on the second request. The issue is very similar to the previous one with the <strong>Vary</strong> header, but this time the header name is <strong>Cf-Bgj</strong>. Mutable subresource can&#8217;t be fetched by Google SXG cache, the HTML document depends on it, therefore SXG prefetching breaks.</p><h4>Workarounds for a Cloudflare bug</h4><p>I prepared a page containing <a href="https://www.planujemywesele.pl/sxg-tests/subresource-prioritization">a demonstration</a> of the issue.</p><p>I was tempted to use Cloudflare transform rules to remove the <strong>Cf-Bgj</strong> header. Unfortunately, it&#8217;s a special header that can&#8217;t be removed. Trying to create a transform rule would result in:</p><pre><code>'remove' is not a valid value for operation because it cannot be used on header beginning with 'cf-'</code></pre><p>From my experiments, the issue manifests only if all three conditions apply to a given JPEG subresource:</p><ol><li><p>Enhanced HTTP/2 Prioritization feature is turned on. It is responsible for setting the <strong>Cf-Bgj</strong> header.</p></li><li><p>Cloudflare <strong>cache is in use</strong>. That&#8217;s probably related to the sidenote above about latency. Image processing might introduce latency if done synchronously without caching.</p></li><li><p>Cloudflare <strong>worker is not used</strong>. I suspect it&#8217;s related to workers being <a href="https://blog.cloudflare.com/better-http-2-prioritization-for-a-faster-web/">allowed</a> to manipulate HTTP/2 prioritization by setting the <strong>Cf-Priority</strong> header. Maybe there is a cleanup logic for removing internally used headers, that include <strong>Cf-Bgj</strong>, and this logic gets fired only when using workers.</p></li></ol><p>Turning off asset caching might be a less-than-optimal idea. But, if you don&#8217;t use workers for your assets, you may re-evaluate your decision to make the issue disappear. Any worker will do, the minimal I could think of is the following one-liner:</p><pre><code>export default { fetch: (r) =&gt; fetch(r) };</code></pre><p>If you still want to avoid workers, for example, due to additional financial costs, the remaining solution is to disable the Enhanced HTTP/2 Prioritization feature.</p><blockquote><p>When writing this post, I decided to test, if the feature works as advertised. I utilized <a href="https://github.com/pmeenan/http2priorities/tree/master/stand-alone">the test page</a> by Patrick Meenan and followed his instructions. Unfortunately, I was unable to see any improvements, both in terms of assets prioritization and progressive image streaming. The results look the same, no matter if the Enhanced HTTP/2 Prioritization is enabled or disabled.</p></blockquote><p>Remember to invalidate the cache after implementing the changes to see the results earlier.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Do you find my posts interesting?</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>To be continued&#8230;</h2><p>There are still a lot of things to cover. In the <em>mutable subresources</em> category of errors, I&#8217;ve identified one more. It was particularly tricky and hard to reproduce. I will describe how I found and fixed it in <a href="https://www.pawelpokrywka.com/p/debugging-complex-signed-exchanges-subresource-issue">the next post</a>.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;b142c898-bad8-47e1-8876-52dde174de8c&quot;,&quot;caption&quot;:&quot;Prefetching errors eliminate 99% of the benefits of using Signed Exchanges (SXG). This is why it's critical to resolve these errors. While you learned how to handle&#8230;&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Debugging mutable subresources: a detective story&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:41077879,&quot;name&quot;:&quot;Pawe&#322; Pokrywka&quot;,&quot;bio&quot;:&quot;I write about security, privacy, and complex systems&#8212;exploring them from the messy middle ground between engineering and research.&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1c35edc2-abb7-4761-8eb4-f379f25e519a_800x800.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-02-21T12:36:18.111Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24ea74fc-d89a-499e-9dfd-915527938490_3476x2532.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.pawelpokrywka.com/p/debugging-complex-signed-exchanges-subresource-issue&quot;,&quot;section_name&quot;:&quot;Performance&quot;,&quot;video_upload_id&quot;:null,&quot;id&quot;:153525804,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:0,&quot;comment_count&quot;:0,&quot;publication_id&quot;:null,&quot;publication_name&quot;:&quot;Pawe&#322; Pokrywka's Lab&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe44a7fc6-d5ec-4a99-984a-7a7e0bc80a17_800x800.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>There are at least two Cloudflare caches: the official one and <em>the hidden</em> one. I will describe the latter in the upcoming post.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>Looks like a bug because <a href="https://developers.cloudflare.com/fundamentals/reference/http-request-headers/">Cloudflare should always set the Accept-Encoding header</a> according to the documentation.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Understanding CORS errors in Signed Exchanges]]></title><description><![CDATA[Learn debugging techniques and why the all-or-nothing principle makes these errors critical (part 3 of 10)]]></description><link>https://www.pawelpokrywka.com/p/understanding-cors-sxg-errors</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/understanding-cors-sxg-errors</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Fri, 31 Jan 2025 11:44:56 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!-zzv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79cd0b44-bb8e-4099-8eed-64e2a76f0fc6_1536x1053.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-zzv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79cd0b44-bb8e-4099-8eed-64e2a76f0fc6_1536x1053.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset image2-full-screen"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-zzv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79cd0b44-bb8e-4099-8eed-64e2a76f0fc6_1536x1053.jpeg 424w, https://substackcdn.com/image/fetch/$s_!-zzv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79cd0b44-bb8e-4099-8eed-64e2a76f0fc6_1536x1053.jpeg 848w, https://substackcdn.com/image/fetch/$s_!-zzv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79cd0b44-bb8e-4099-8eed-64e2a76f0fc6_1536x1053.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!-zzv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79cd0b44-bb8e-4099-8eed-64e2a76f0fc6_1536x1053.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-zzv!,w_5760,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79cd0b44-bb8e-4099-8eed-64e2a76f0fc6_1536x1053.jpeg" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/79cd0b44-bb8e-4099-8eed-64e2a76f0fc6_1536x1053.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;full&quot;,&quot;height&quot;:998,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:318298,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-fullscreen" alt="" srcset="https://substackcdn.com/image/fetch/$s_!-zzv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79cd0b44-bb8e-4099-8eed-64e2a76f0fc6_1536x1053.jpeg 424w, https://substackcdn.com/image/fetch/$s_!-zzv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79cd0b44-bb8e-4099-8eed-64e2a76f0fc6_1536x1053.jpeg 848w, https://substackcdn.com/image/fetch/$s_!-zzv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79cd0b44-bb8e-4099-8eed-64e2a76f0fc6_1536x1053.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!-zzv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79cd0b44-bb8e-4099-8eed-64e2a76f0fc6_1536x1053.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Understanding SXG prefetching errors, inspired by <em>The Doctor</em> (1891) by Luke Fildes, oil on canvas, 166 &#215; 242 cm</figcaption></figure></div><blockquote><p>Context (Oct 2025): After deep work with SXG, I suspected changes might be coming&#8212;just not this quickly. Cloudflare <a href="https://community.cloudflare.com/t/amp-and-signed-exchanges-deprecation-october-20th/831238">announced SXG deprecation</a>, and I observed SXG stop working around Sept 19, 2025.</p><p>If you still want SXG, the current path is to self-host&#8212;but the tooling looks abandoned, and setup is non-trivial. Also, Google may follow Cloudflare by shutting down the SXG cache and removing SXG support in Chrome.</p></blockquote><p>In the previous two parts, you learned how to enable Signed Exchanges (SXG) and let Google <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">prefetch your site&#8217;s HTML</a> and <a href="https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges">assets (or SXG subresources)</a> on the search results page.</p><p>The effect: when the user follows the link to your website from Google, the page should load instantaneously from the browser cache. No need to wait for:</p><ul><li><p>the page to become visible because HTML and CSS are already there,</p></li><li><p>the images&#8212;they are already downloaded,</p></li><li><p>the page to become interactive, because javascript starts executing without any delay.</p></li></ul><p>Unfortunately, there are many scenarios in which it doesn&#8217;t work like that. If you met all the requirements I described in <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">the first part</a> of the series, HTML prefetching should work without issues. But prefetching subresources is tricky. It may cause trouble even if you strictly follow all the official recommendations.</p><p>Prefetching subresources is critical from the performance standpoint&#8212;much more important than HTML. Subresources are typically larger, so they take more time to download.</p><p>By reading this post you will learn how to diagnose SXG errors.</p><blockquote><p><strong>Disclaimer: This is not a CORS tutorial!</strong></p><p>Although this post mentions CORS errors a lot, this is not a typical tutorial on configuring your website&#8217;s CORS policy. If you don&#8217;t know what is SXG and/or don&#8217;t use it, then please stop reading because you will waste your time. You will find a lot of valueable resources on the web. This is not the one you are looking for.</p></blockquote><h2>One bad apple spoils the whole barrel</h2><p>When the browser prefetches the SXG page, it does it in two phases:</p><ol><li><p>The browser downloads an SXG-wrapped HTTP response from the Google SXG cache. This response contains the HTML of your page (the main resource).</p></li><li><p>The HTTP response used to deliver the above contains a <strong>Link</strong> header. It includes a list of URLs of SXG-wrapped HTTP responses containing subresources. The browser downloads all of these URLs from the Google SXG cache.</p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OREz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e98d5d1-78e6-453c-8416-dd510a907727_3000x1152.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OREz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e98d5d1-78e6-453c-8416-dd510a907727_3000x1152.png 424w, https://substackcdn.com/image/fetch/$s_!OREz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e98d5d1-78e6-453c-8416-dd510a907727_3000x1152.png 848w, https://substackcdn.com/image/fetch/$s_!OREz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e98d5d1-78e6-453c-8416-dd510a907727_3000x1152.png 1272w, https://substackcdn.com/image/fetch/$s_!OREz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e98d5d1-78e6-453c-8416-dd510a907727_3000x1152.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OREz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e98d5d1-78e6-453c-8416-dd510a907727_3000x1152.png" width="1456" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9e98d5d1-78e6-453c-8416-dd510a907727_3000x1152.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:158807,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!OREz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e98d5d1-78e6-453c-8416-dd510a907727_3000x1152.png 424w, https://substackcdn.com/image/fetch/$s_!OREz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e98d5d1-78e6-453c-8416-dd510a907727_3000x1152.png 848w, https://substackcdn.com/image/fetch/$s_!OREz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e98d5d1-78e6-453c-8416-dd510a907727_3000x1152.png 1272w, https://substackcdn.com/image/fetch/$s_!OREz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9e98d5d1-78e6-453c-8416-dd510a907727_3000x1152.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Encapsulation layers used in SXG prefetching.</figcaption></figure></div><p>When the user decides to visit the page, the browser <em>may use</em> the downloaded subresources in the page rendering process. The important point is that <strong>it will use them only if all of them were successfully prefetched</strong>.</p><p>Let me reiterate this. Even if one of your subresources fails to prefetch for any reason, the browser will discard all of the already prefetched subresources and download them again during page rendering, slowing it down considerably. Most of your assets will be downloaded twice. What a waste of bandwidth!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8Bpn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d014e23-5a7e-4c09-895e-7cbe81119476_974x793.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8Bpn!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d014e23-5a7e-4c09-895e-7cbe81119476_974x793.jpeg 424w, https://substackcdn.com/image/fetch/$s_!8Bpn!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d014e23-5a7e-4c09-895e-7cbe81119476_974x793.jpeg 848w, https://substackcdn.com/image/fetch/$s_!8Bpn!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d014e23-5a7e-4c09-895e-7cbe81119476_974x793.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!8Bpn!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d014e23-5a7e-4c09-895e-7cbe81119476_974x793.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8Bpn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d014e23-5a7e-4c09-895e-7cbe81119476_974x793.jpeg" width="974" height="793" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9d014e23-5a7e-4c09-895e-7cbe81119476_974x793.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:793,&quot;width&quot;:974,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:114019,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8Bpn!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d014e23-5a7e-4c09-895e-7cbe81119476_974x793.jpeg 424w, https://substackcdn.com/image/fetch/$s_!8Bpn!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d014e23-5a7e-4c09-895e-7cbe81119476_974x793.jpeg 848w, https://substackcdn.com/image/fetch/$s_!8Bpn!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d014e23-5a7e-4c09-895e-7cbe81119476_974x793.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!8Bpn!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d014e23-5a7e-4c09-895e-7cbe81119476_974x793.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">SXG's prefetching performance is fragile. A single error can undo all the gains. <em>The house of cards</em> (1869) by Theodore Gerard, oil on panel, 59 &#215; 74 cm</figcaption></figure></div><p>This <em>all-or-nothing</em> principle is unforgiving in case of subresource loading errors. A little icon you tried to prefetch fails to load? Forget about 99% of the SXG speed benefits!<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> That's why you should strive to eliminate all errors.</p><blockquote><p><strong>Why does it work that way?</strong></p><p>SXG <a href="https://wicg.github.io/webpackage/loading.html">documents</a> state it&#8217;s for privacy reasons:</p><p>&#8220;This is intended to prevent the referrer page from encoding a tracking ID into the set of subresources it prefetches.&#8221;</p><p>Let&#8217;s say the site uses 128<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a> subresources. Those subresources could be tiny, to not force the user to download too much data. Google could prefetch them selectively. For example the first subresource is prefetched and the rest is not for user A, the second and third are prefetched for user B, etc. It gives a total of 2<sup>128</sup> combinations, allowing Google to encode 128 bits/16 bytes of data or about 150 ASCII characters.</p><p>Later, when visiting a website, it could use client-side or server-side logic to detect which subresources were prefetched and which were not. Using that knowledge, it could reconstruct (decode) the data passed from Google.</p><p>In effect, Google could pass any data to the website. It may be unique user identifier, search query, etc. <strong>This data could be used by the site for tracking.</strong></p><p>The above method of passing data to the website it not the only one possible. If Google likes to, it could use URL anchor, <strong>Referrer</strong> header, or other more sophisticated ways.</p><p>Currently, Google chooses to protect user&#8217;s privacy, but we don&#8217;t know what the future may bring. What if SXG is used in other search engines that make different privacy choices? Or in entirely different context, where the collusion between reffering and target website is more probable? SXG&#8217;s authors wanted to prevent its misuse and didn&#8217;t want to create another way to track users.</p></blockquote><h2>SXG debugging</h2><h4>Classification of errors</h4><p>There are a few places, where the issues with SXG may occur:</p><ul><li><p>when the SXG is generated by Cloudflare,</p></li><li><p>when SXG is processed and put into Google SXG cache,</p></li><li><p>when the browser tries to prefetch SXG.</p></li></ul><p>Another way of differentiating the errors is by determining what caused them. It might be:</p><ul><li><p>the main document,</p></li><li><p>a subresource.</p></li></ul><h4>SXG Validator</h4><p>You can use <a href="https://chromewebstore.google.com/detail/sxg-validator/hiijcdgcphjeljafieaejfhodfbpmgoe?pli=1">the SXG Validator</a> browser extension described in the <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">first part</a> of the series. It doesn&#8217;t support subresources but will tell you if the main document was correctly generated by Cloudflare, processed, and put into the Google SXG cache. It will report cache ingestion errors as they occur.</p><p>However, if you follow the recommendations, you'll reach a point where the main document is fine and the SXG Validator becomes unnecessary.</p><h4>Swiss-army knife for SXG debug</h4><p>When debugging subresources, my favorite tool is the SXG prefetch page in Chrome Developer Tools, with the <strong>Network</strong> tab open.</p><p>It allows you to diagnose all the errors except those related to SXG generation by Cloudflare. Also, in my experience, it&#8217;s more reliable than SXG Validator which sometimes returns false negative results.</p><h4>Using the SXG prefetch page</h4><p>Upon <a href="https://signed-exchange-testing.dev/prefetch">visiting the page</a>, you will be welcomed with a basic form.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!VkQ8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9901d1b3-19f9-49df-9a36-bbaf3318d968_1221x160.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!VkQ8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9901d1b3-19f9-49df-9a36-bbaf3318d968_1221x160.png 424w, https://substackcdn.com/image/fetch/$s_!VkQ8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9901d1b3-19f9-49df-9a36-bbaf3318d968_1221x160.png 848w, https://substackcdn.com/image/fetch/$s_!VkQ8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9901d1b3-19f9-49df-9a36-bbaf3318d968_1221x160.png 1272w, https://substackcdn.com/image/fetch/$s_!VkQ8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9901d1b3-19f9-49df-9a36-bbaf3318d968_1221x160.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!VkQ8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9901d1b3-19f9-49df-9a36-bbaf3318d968_1221x160.png" width="1221" height="160" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9901d1b3-19f9-49df-9a36-bbaf3318d968_1221x160.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:160,&quot;width&quot;:1221,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:15691,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!VkQ8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9901d1b3-19f9-49df-9a36-bbaf3318d968_1221x160.png 424w, https://substackcdn.com/image/fetch/$s_!VkQ8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9901d1b3-19f9-49df-9a36-bbaf3318d968_1221x160.png 848w, https://substackcdn.com/image/fetch/$s_!VkQ8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9901d1b3-19f9-49df-9a36-bbaf3318d968_1221x160.png 1272w, https://substackcdn.com/image/fetch/$s_!VkQ8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9901d1b3-19f9-49df-9a36-bbaf3318d968_1221x160.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Remember to open Chrome Dev Tools (by pressing the <strong>F12</strong> key), go to the <strong>Network</strong> tab, and clear it by clicking the &#128711; icon or using the <strong>CTRL+L</strong> keyboard shortcut. Now enter the URL of the page you want to test and click the <strong>Submit</strong> button. The page will reload and prefetch the URL you provided.</p><p>Let&#8217;s submit the following URL:</p><pre><code>https://www.planujemywesele.pl/muzyka-na-male-wesele/poznan</code></pre><p>Assuming the page is not yet present in the Google SXG cache, this will result in 3 HTTP requests:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xqD8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd854e2b-5e1b-4d54-ab5a-99158e199125_1750x197.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xqD8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd854e2b-5e1b-4d54-ab5a-99158e199125_1750x197.png 424w, https://substackcdn.com/image/fetch/$s_!xqD8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd854e2b-5e1b-4d54-ab5a-99158e199125_1750x197.png 848w, https://substackcdn.com/image/fetch/$s_!xqD8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd854e2b-5e1b-4d54-ab5a-99158e199125_1750x197.png 1272w, https://substackcdn.com/image/fetch/$s_!xqD8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd854e2b-5e1b-4d54-ab5a-99158e199125_1750x197.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xqD8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd854e2b-5e1b-4d54-ab5a-99158e199125_1750x197.png" width="1456" height="164" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bd854e2b-5e1b-4d54-ab5a-99158e199125_1750x197.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:164,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:45404,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xqD8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd854e2b-5e1b-4d54-ab5a-99158e199125_1750x197.png 424w, https://substackcdn.com/image/fetch/$s_!xqD8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd854e2b-5e1b-4d54-ab5a-99158e199125_1750x197.png 848w, https://substackcdn.com/image/fetch/$s_!xqD8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd854e2b-5e1b-4d54-ab5a-99158e199125_1750x197.png 1272w, https://substackcdn.com/image/fetch/$s_!xqD8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd854e2b-5e1b-4d54-ab5a-99158e199125_1750x197.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The first two are related to the SXG prefetch page and, therefore should be ignored<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>. The third request is the actual prefetching request of your website. Let&#8217;s click on it to see the details:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8psx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b72edd6-4398-4de7-a283-79bb91d2287d_1755x972.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8psx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b72edd6-4398-4de7-a283-79bb91d2287d_1755x972.png 424w, https://substackcdn.com/image/fetch/$s_!8psx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b72edd6-4398-4de7-a283-79bb91d2287d_1755x972.png 848w, https://substackcdn.com/image/fetch/$s_!8psx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b72edd6-4398-4de7-a283-79bb91d2287d_1755x972.png 1272w, https://substackcdn.com/image/fetch/$s_!8psx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b72edd6-4398-4de7-a283-79bb91d2287d_1755x972.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8psx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b72edd6-4398-4de7-a283-79bb91d2287d_1755x972.png" width="1456" height="806" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9b72edd6-4398-4de7-a283-79bb91d2287d_1755x972.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:806,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:215006,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8psx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b72edd6-4398-4de7-a283-79bb91d2287d_1755x972.png 424w, https://substackcdn.com/image/fetch/$s_!8psx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b72edd6-4398-4de7-a283-79bb91d2287d_1755x972.png 848w, https://substackcdn.com/image/fetch/$s_!8psx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b72edd6-4398-4de7-a283-79bb91d2287d_1755x972.png 1272w, https://substackcdn.com/image/fetch/$s_!8psx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9b72edd6-4398-4de7-a283-79bb91d2287d_1755x972.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The important parts are:</p><ol><li><p>The request URL points to a special URL within the webpkgcache.com domain. It is the domain of Google SXG cache. The cache URL is constructed by transforming the original URL<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>.</p></li><li><p>The presence of a <strong>Location</strong> header is one of Google SXG's methods for <a href="https://developers.google.com/search/docs/appearance/signed-exchange#debug-the-google-sxg-cache">signaling that an entry is not yet cached</a>. The value of this header contains the URL of the original page (non-SXG version). It&#8217;s like Google SXG cache is saying: &#8220;I don&#8217;t have the SXG version yet, but in case you need the page right now, please request the original version as a fallback.&#8220;</p></li></ol><h4>Fallback mechanism</h4><p>Unfortunately, Chrome Dev Tools doesn&#8217;t allow you to see the response body if the URL is absent from the SXG cache. The <strong>Preview</strong> and <strong>Response</strong> tabs contain only the following error message (which may be related to <a href="https://issues.chromium.org/issues/40254754">this issue</a>):</p><pre><code>Failed to load response data: No data found for resource with given identifier</code></pre><p>However, you can see it with <strong>curl</strong>. Just point it to the Google SXG cache URL and set the proper <strong>Accept</strong> header:</p><pre><code>curl -H "Accept: application/signed-exchange;v=b3" https://www-planujemywesele-pl.webpkgcache.com/doc/-/s/www.planujemywesele.pl/muzyka-na-male-wesele/poznan</code></pre><p>Assuming the page is not yet present in the Google SXG cache, you will get:</p><pre><code>&lt;HTML&gt;&lt;HEAD&gt; &lt;meta http-equiv="content-type" content="text/html;charset=utf-8"&gt; &lt;TITLE&gt;Redirecting&lt;/TITLE&gt; &lt;META HTTP-EQUIV="refresh" content="0; url=https://www.planujemywesele.pl/muzyka-na-male-wesele/poznan"&gt; &lt;/HEAD&gt; &lt;BODY onLoad="location.replace('https://www.planujemywesele.pl/muzyka-na-male-wesele/poznan'+document.location.hash)"&gt;</code></pre><p>This simple HTML page aims to redirect the user to the original website. This is a fallback mechanism Google uses to ensure users can access the site, even if it's not yet in the SXG cache.</p><blockquote><p>The fallback mechanism has interesting property. It uses client-side redirection that alters referrer.</p><p>Typically when someone comes from Google, the referrer should be set to <strong>www.google.com</strong> or one of Google&#8217;s regional domains. But in case the SXG fallback mechanism, you will see visitors coming from <strong>your-domain-com.webpkgcache.com.</strong></p><p>You should keep that in mind when using your web analytics.</p><p>On top of that, measuring SXG traffic in Google Analytics comes with its own challenges. The official documentation suggests using <strong>window.isSXG</strong> to identify SXG visits, but this method is incomplete. In the 7th part of this series, I explain <a href="https://www.pawelpokrywka.com/p/different-methods-of-prefetching">how to measure SXG traffic</a> correctly.</p></blockquote><h4>Ingestion errors</h4><p>If you looked closely, you might have seen a <strong>Warning</strong> header.</p><p>Google SXG cache uses this header to communicate errors that happen during ingestion. If you wonder how the SXG Validator browser extension knows the cache error message, it gets it from the value of this header.</p><p>In our example, we got the following error message:</p><pre><code>199 - "debug: content has ingestion error: SXG validation failure: Certificate is not valid; bad OCSP status: 2; details: 6; cert trust"</code></pre><p>This is a classic example of transient error, at least if you use Cloudflare. It will vanish when you repeat the request a few seconds later.</p><p>You may observe other errors. Most of them could be eliminated by meeting all the requirements described in the <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">previous</a> <a href="https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges">parts</a>.</p><blockquote><p>Note that fixing the error won&#8217;t be reflected immediately in the <strong>Warning</strong> header, as caching is involved. Try with different URL to get feedback earlier.</p></blockquote><p>If you don&#8217;t see the <strong>Warning</strong> header, it means Google needs more time to fetch the page and store it in the SXG cache.</p><h4>The actual SXG response</h4><p>By submitting the URL on the SXG prefetch page, we requested the SXG version of the document from the Google SXG cache. It automatically activated the cache population mechanism, ultimately retrieving the page and storing it in the cache.</p><p>Now, let&#8217;s clear the requests in Chrome Dev Tools and submit the same URL as previously. This time, the result looks different:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!iQTc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8db9aa7-f038-4482-b0fe-ef03debfef39_1736x518.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!iQTc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8db9aa7-f038-4482-b0fe-ef03debfef39_1736x518.png 424w, https://substackcdn.com/image/fetch/$s_!iQTc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8db9aa7-f038-4482-b0fe-ef03debfef39_1736x518.png 848w, https://substackcdn.com/image/fetch/$s_!iQTc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8db9aa7-f038-4482-b0fe-ef03debfef39_1736x518.png 1272w, https://substackcdn.com/image/fetch/$s_!iQTc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8db9aa7-f038-4482-b0fe-ef03debfef39_1736x518.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!iQTc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8db9aa7-f038-4482-b0fe-ef03debfef39_1736x518.png" width="1456" height="434" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b8db9aa7-f038-4482-b0fe-ef03debfef39_1736x518.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:434,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:193225,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!iQTc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8db9aa7-f038-4482-b0fe-ef03debfef39_1736x518.png 424w, https://substackcdn.com/image/fetch/$s_!iQTc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8db9aa7-f038-4482-b0fe-ef03debfef39_1736x518.png 848w, https://substackcdn.com/image/fetch/$s_!iQTc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8db9aa7-f038-4482-b0fe-ef03debfef39_1736x518.png 1272w, https://substackcdn.com/image/fetch/$s_!iQTc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8db9aa7-f038-4482-b0fe-ef03debfef39_1736x518.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>You may notice there are a lot of HTTP responses. We will get to them, but let&#8217;s move from the top as previously. Ignore the first 2 responses like before and focus on the third one.</p><p>In the <strong>Type</strong> column, you can see &#8220;signed-exchange / Redirect&#8221; instead of the &#8220;text/html&#8221; you saw earlier. When you click on the third row, you will see details:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rdm2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f3bf693-c370-4a55-b12b-500965af015f_1958x1104.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rdm2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f3bf693-c370-4a55-b12b-500965af015f_1958x1104.png 424w, https://substackcdn.com/image/fetch/$s_!rdm2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f3bf693-c370-4a55-b12b-500965af015f_1958x1104.png 848w, https://substackcdn.com/image/fetch/$s_!rdm2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f3bf693-c370-4a55-b12b-500965af015f_1958x1104.png 1272w, https://substackcdn.com/image/fetch/$s_!rdm2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f3bf693-c370-4a55-b12b-500965af015f_1958x1104.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rdm2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f3bf693-c370-4a55-b12b-500965af015f_1958x1104.png" width="1456" height="821" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6f3bf693-c370-4a55-b12b-500965af015f_1958x1104.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:821,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:387302,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!rdm2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f3bf693-c370-4a55-b12b-500965af015f_1958x1104.png 424w, https://substackcdn.com/image/fetch/$s_!rdm2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f3bf693-c370-4a55-b12b-500965af015f_1958x1104.png 848w, https://substackcdn.com/image/fetch/$s_!rdm2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f3bf693-c370-4a55-b12b-500965af015f_1958x1104.png 1272w, https://substackcdn.com/image/fetch/$s_!rdm2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6f3bf693-c370-4a55-b12b-500965af015f_1958x1104.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This time you won&#8217;t see the <strong>Location</strong> header and the <strong>Content-Type</strong> header is set to <strong>application/signed-exchange;v=b3</strong> (previously it was set to <strong>text/html; charset=UTF-8</strong>). Those things tell you the SXG of the main document has been correctly generated and stored in the Google SXG cache.</p><p>If your document uses subresources, the <strong>Link</strong> header will include URLs of copies stored in the Google SXG cache.</p><blockquote><p>If the list doesn&#8217;t include one or more of your subresources, or it there is no <strong>Link</strong> header at all it means Cloudflare couldn&#8217;t include some or all of your subresources. Most probably it is caused by hosting subresources on a different (sub)domain. The other cause is if you have more than 20 subresources&#8212;you will miss anything beyond that limit. See the <a href="https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges">previous part</a> for details.</p></blockquote><p>Notice the structure of URLs in the <strong>Link</strong> header:</p><pre><code>https://<strong>domain-com</strong>.webpkgcache.com/sub/<strong>XXX</strong>/s/<strong>domain.com/path/file.jpg</strong></code></pre><p>Same as with HTML pages, they point to the <strong>webpkgcache.com</strong> domain. However, the path is constructed differently. The most important difference is the presence of the first few characters of the integrity hash of the subresource (marked with <strong>XXX</strong> above). This way, the location of a subresource is closely linked to its content.</p><p>When you open the <strong>Preview</strong> tab you will see the decoded SXG response:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!n1wf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783d02c7-16c3-4cdc-bf09-f75071a7a0f0_1554x1096.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!n1wf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783d02c7-16c3-4cdc-bf09-f75071a7a0f0_1554x1096.png 424w, https://substackcdn.com/image/fetch/$s_!n1wf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783d02c7-16c3-4cdc-bf09-f75071a7a0f0_1554x1096.png 848w, https://substackcdn.com/image/fetch/$s_!n1wf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783d02c7-16c3-4cdc-bf09-f75071a7a0f0_1554x1096.png 1272w, https://substackcdn.com/image/fetch/$s_!n1wf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783d02c7-16c3-4cdc-bf09-f75071a7a0f0_1554x1096.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!n1wf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783d02c7-16c3-4cdc-bf09-f75071a7a0f0_1554x1096.png" width="1456" height="1027" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/783d02c7-16c3-4cdc-bf09-f75071a7a0f0_1554x1096.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1027,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:240683,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!n1wf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783d02c7-16c3-4cdc-bf09-f75071a7a0f0_1554x1096.png 424w, https://substackcdn.com/image/fetch/$s_!n1wf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783d02c7-16c3-4cdc-bf09-f75071a7a0f0_1554x1096.png 848w, https://substackcdn.com/image/fetch/$s_!n1wf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783d02c7-16c3-4cdc-bf09-f75071a7a0f0_1554x1096.png 1272w, https://substackcdn.com/image/fetch/$s_!n1wf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F783d02c7-16c3-4cdc-bf09-f75071a7a0f0_1554x1096.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The most useful fields are:</p><ul><li><p><strong>Signature Date</strong>. It tells you when the SXG was generated. Remember that the time you see there has to be adjusted by adding 1 hour<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>. From the debugging perspective, it will tell you if the SXG is fresh or not.</p></li><li><p><strong>Response headers</strong>. This section lists all the HTTP headers (along with values) included in the SXG when they were generated by Cloudflare Automated Signed Exchanges (ASX). Headers are critical for prefetching subresources; you will understand why after reading <a href="https://www.pawelpokrywka.com/p/fixing-sxg-prefetching-errors-caused-by-mutable-subresources">the next part</a> of the series.</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Subscribe to get more insights on interesting technical topics.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><h4>SXG subresource response</h4><p>The remaining requests of type &#8220;signed-exchange / Redirect&#8221; are related to SXG subresources. When you examine them, they will look similar. The most important differences in the <strong>Preview </strong>tab are:</p><ul><li><p><strong>Content type (</strong>in <strong>Response headers</strong> field<strong>)</strong>. Your main document is likely HTML, while subresources are styles, images, etc. This header will reflect that.</p></li><li><p><strong>Signature Date </strong>and<strong> Signature Expires</strong>. Each subresource has independent signature dates and typically should have higher expiration times than the main document.</p></li></ul><p>Apart from that, you won&#8217;t find a <strong>Link</strong> header in the HTTP response. That&#8217;s because subresource nesting is not allowed: subresources can&#8217;t have sub-subresources.</p><p>Every HTTP response for an existing entry you get from the SXG cache has a <strong>Content-Type</strong> header set to &#8220;application/signed-exchange;v=b3&#8221;. The actual content type of the subresource is encoded in the body of the response. It can be examined in the <strong>Preview</strong> tab.</p><p>I like to think of SXG as <em>a package</em> containing the complete HTTP response. This package must be delivered to the browser through a separate HTTP response.</p><h2>CORS errors</h2><p>Now, that you are equipped with the debug tool and understand how to use it, let&#8217;s examine the most common error you will encounter when you inspect network requests in Chrome Dev Tools:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AZFC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F466a053d-8741-4e29-b828-8d04ac34dc70_1832x183.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AZFC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F466a053d-8741-4e29-b828-8d04ac34dc70_1832x183.png 424w, https://substackcdn.com/image/fetch/$s_!AZFC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F466a053d-8741-4e29-b828-8d04ac34dc70_1832x183.png 848w, https://substackcdn.com/image/fetch/$s_!AZFC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F466a053d-8741-4e29-b828-8d04ac34dc70_1832x183.png 1272w, https://substackcdn.com/image/fetch/$s_!AZFC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F466a053d-8741-4e29-b828-8d04ac34dc70_1832x183.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AZFC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F466a053d-8741-4e29-b828-8d04ac34dc70_1832x183.png" width="1456" height="145" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/466a053d-8741-4e29-b828-8d04ac34dc70_1832x183.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:145,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:82349,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!AZFC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F466a053d-8741-4e29-b828-8d04ac34dc70_1832x183.png 424w, https://substackcdn.com/image/fetch/$s_!AZFC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F466a053d-8741-4e29-b828-8d04ac34dc70_1832x183.png 848w, https://substackcdn.com/image/fetch/$s_!AZFC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F466a053d-8741-4e29-b828-8d04ac34dc70_1832x183.png 1272w, https://substackcdn.com/image/fetch/$s_!AZFC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F466a053d-8741-4e29-b828-8d04ac34dc70_1832x183.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>If you switch to console view, you may see:</p><pre><code>&#10754; Access to link prefetch resource at 'https://www-yourdomain-com.webpkgcache.com/sub/d3_L0zvunVqT/s/www.yourdomain.com/file.jpeg' from origin 'https://www.google.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.  &#10754; GET https://www-yourdomain-com.webpkgcache.com/sub/d3_L0zvunVqT/s/www.yourdomain.com/file.jpeg net::ERR_FAILED 200 (OK)</code></pre><h4>Why do those errors occur?</h4><p>When I first encountered this error I was confused.</p><p>CORS (<strong>C</strong>ross-<strong>O</strong>rigin <strong>R</strong>esource <strong>S</strong>haring) errors typically occur when performing cross-origin requests with invalid or missing CORS policy. However, the subresources are stored in the Google SXG cache and only Google has control over its CORS policy. Is it possible Google set an invalid CORS policy on its own website?</p><p>Let&#8217;s examine the problematic request:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KtQm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff806c268-b5eb-4c43-90b4-9698574b86e4_1482x1182.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KtQm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff806c268-b5eb-4c43-90b4-9698574b86e4_1482x1182.png 424w, https://substackcdn.com/image/fetch/$s_!KtQm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff806c268-b5eb-4c43-90b4-9698574b86e4_1482x1182.png 848w, https://substackcdn.com/image/fetch/$s_!KtQm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff806c268-b5eb-4c43-90b4-9698574b86e4_1482x1182.png 1272w, https://substackcdn.com/image/fetch/$s_!KtQm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff806c268-b5eb-4c43-90b4-9698574b86e4_1482x1182.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KtQm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff806c268-b5eb-4c43-90b4-9698574b86e4_1482x1182.png" width="1456" height="1161" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f806c268-b5eb-4c43-90b4-9698574b86e4_1482x1182.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1161,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:250085,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!KtQm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff806c268-b5eb-4c43-90b4-9698574b86e4_1482x1182.png 424w, https://substackcdn.com/image/fetch/$s_!KtQm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff806c268-b5eb-4c43-90b4-9698574b86e4_1482x1182.png 848w, https://substackcdn.com/image/fetch/$s_!KtQm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff806c268-b5eb-4c43-90b4-9698574b86e4_1482x1182.png 1272w, https://substackcdn.com/image/fetch/$s_!KtQm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff806c268-b5eb-4c43-90b4-9698574b86e4_1482x1182.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>There are no CORS headers in the response. In contrast, when examining valid response, the <strong>Access-Control-Allow-Origin</strong> header is clearly there:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7Qdk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b56565d-9153-4af3-99f4-574624b989b3_1754x1217.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7Qdk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b56565d-9153-4af3-99f4-574624b989b3_1754x1217.png 424w, https://substackcdn.com/image/fetch/$s_!7Qdk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b56565d-9153-4af3-99f4-574624b989b3_1754x1217.png 848w, https://substackcdn.com/image/fetch/$s_!7Qdk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b56565d-9153-4af3-99f4-574624b989b3_1754x1217.png 1272w, https://substackcdn.com/image/fetch/$s_!7Qdk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b56565d-9153-4af3-99f4-574624b989b3_1754x1217.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7Qdk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b56565d-9153-4af3-99f4-574624b989b3_1754x1217.png" width="1456" height="1010" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1b56565d-9153-4af3-99f4-574624b989b3_1754x1217.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1010,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:301532,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!7Qdk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b56565d-9153-4af3-99f4-574624b989b3_1754x1217.png 424w, https://substackcdn.com/image/fetch/$s_!7Qdk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b56565d-9153-4af3-99f4-574624b989b3_1754x1217.png 848w, https://substackcdn.com/image/fetch/$s_!7Qdk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b56565d-9153-4af3-99f4-574624b989b3_1754x1217.png 1272w, https://substackcdn.com/image/fetch/$s_!7Qdk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b56565d-9153-4af3-99f4-574624b989b3_1754x1217.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>So this is the reason for the error. But&#8230;</p><h4>Why is the CORS policy different for this particular file?</h4><p>When you compare the error with a valid response again, you will notice other differences. The most critical HTTP headers that differ in the invalid response include:</p><ul><li><p><strong>Content-Type</strong>. It&#8217;s set to &#8220;text/html; charset=UTF-8&#8221;. But it is supposed to be an SXG-wrapped image&#8230;</p></li><li><p><strong>Location</strong>. It is set to the actual image location on the origin server.</p></li></ul><p>Also, when navigating to the <strong>Preview</strong> tab this error message shows up:</p><pre><code><code>Failed to load response data: No data found for resource with given identifier</code></code></pre><p>Seems familiar&#8230; Wait. It&#8217;s the fallback mechanism Google SXG cache uses when asked for non-existent SXGs of HTML documents! It appears Google reuses it for subresources too. As those responses don&#8217;t contain CORS headers, CORS errors occur.</p><p>So, if you ask Google SXG cache for a non-existing image (or any other file type), instead of this image (or file) you'll receive an HTML response. The HTTP response code is 200, but don't let that fool you. In reality:</p><div class="pullquote"><p>When prefetching, a CORS error means a given (sub)resource is missing from the Google SXG cache.</p></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rCL5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3657bdf3-6538-40f1-b2e0-32c66b15a2ef_2560x1915.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rCL5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3657bdf3-6538-40f1-b2e0-32c66b15a2ef_2560x1915.jpeg 424w, https://substackcdn.com/image/fetch/$s_!rCL5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3657bdf3-6538-40f1-b2e0-32c66b15a2ef_2560x1915.jpeg 848w, https://substackcdn.com/image/fetch/$s_!rCL5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3657bdf3-6538-40f1-b2e0-32c66b15a2ef_2560x1915.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!rCL5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3657bdf3-6538-40f1-b2e0-32c66b15a2ef_2560x1915.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rCL5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3657bdf3-6538-40f1-b2e0-32c66b15a2ef_2560x1915.jpeg" width="1456" height="1089" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3657bdf3-6538-40f1-b2e0-32c66b15a2ef_2560x1915.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1089,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1830803,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!rCL5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3657bdf3-6538-40f1-b2e0-32c66b15a2ef_2560x1915.jpeg 424w, https://substackcdn.com/image/fetch/$s_!rCL5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3657bdf3-6538-40f1-b2e0-32c66b15a2ef_2560x1915.jpeg 848w, https://substackcdn.com/image/fetch/$s_!rCL5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3657bdf3-6538-40f1-b2e0-32c66b15a2ef_2560x1915.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!rCL5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3657bdf3-6538-40f1-b2e0-32c66b15a2ef_2560x1915.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">CORS is not the issue, inspired by <em>The Gleaners</em> (1857) by Jean-Fran&#231;ois Millet, oil on canvas, 83 &#215; 110 cm</figcaption></figure></div><h4>Try it yourself</h4><p>If you want to experience the errors, try a demo page that contains two subresources&#8212;one that successfully prefetches and another that fails to prefetch.</p><p>Go to <a href="https://signed-exchange-testing.dev/prefetch">the SXG prefetching page</a> and paste the following URL. Remember to change the <strong>NUMBER</strong> to a random number between 1 and 10000:</p><pre><code>https://www.planujemywesele.pl/sxg-tests/good-bad-subresource/<strong>NUMBER</strong></code></pre><p>After providing the URL, start repeatedly hitting the <strong>Submit</strong> button.</p><p>Watch the process unfold:</p><ul><li><p>First, you'll need to wait for the Google SXG cache to cache the page.</p></li><li><p>Then wait a few more seconds while the Google SXG cache downloads the subresources. CORS errors will appear for both subresources.</p></li><li><p>After this process completes, you'll observe that one of the subresources successfully prefetches (<strong>good.css</strong>) while the other still fails with a CORS error (<strong>bad.css</strong>).</p></li></ul><h4>Google search behavior</h4><p>The same CORS error occurs when the Google SXG cache lacks a copy of the <em>main document</em> for a specific page. Such errors may occur in actual Google search results, as demonstrated below:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!L5js!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc761f67-e8f2-427b-91eb-535fb7d62fb2_2344x1890.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!L5js!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc761f67-e8f2-427b-91eb-535fb7d62fb2_2344x1890.png 424w, https://substackcdn.com/image/fetch/$s_!L5js!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc761f67-e8f2-427b-91eb-535fb7d62fb2_2344x1890.png 848w, https://substackcdn.com/image/fetch/$s_!L5js!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc761f67-e8f2-427b-91eb-535fb7d62fb2_2344x1890.png 1272w, https://substackcdn.com/image/fetch/$s_!L5js!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc761f67-e8f2-427b-91eb-535fb7d62fb2_2344x1890.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!L5js!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc761f67-e8f2-427b-91eb-535fb7d62fb2_2344x1890.png" width="1456" height="1174" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dc761f67-e8f2-427b-91eb-535fb7d62fb2_2344x1890.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1174,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:814379,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!L5js!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc761f67-e8f2-427b-91eb-535fb7d62fb2_2344x1890.png 424w, https://substackcdn.com/image/fetch/$s_!L5js!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc761f67-e8f2-427b-91eb-535fb7d62fb2_2344x1890.png 848w, https://substackcdn.com/image/fetch/$s_!L5js!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc761f67-e8f2-427b-91eb-535fb7d62fb2_2344x1890.png 1272w, https://substackcdn.com/image/fetch/$s_!L5js!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc761f67-e8f2-427b-91eb-535fb7d62fb2_2344x1890.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The SXG prefetch page used for debugging employs different mechanics to invoke prefetching, so the CORS error message won't be displayed there. However, when using this tool, you can still see the warnings described in the <strong>Ingestion errors</strong> section. Therefore, the SXG prefetch page should suffice in debugging and fixing errors in the main SXG document.</p><h2>Other errors</h2><p>Sometimes you may observe an error similar to the following:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OROP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f971db0-de86-427c-ae53-16d552837ffb_1904x188.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OROP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f971db0-de86-427c-ae53-16d552837ffb_1904x188.png 424w, https://substackcdn.com/image/fetch/$s_!OROP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f971db0-de86-427c-ae53-16d552837ffb_1904x188.png 848w, https://substackcdn.com/image/fetch/$s_!OROP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f971db0-de86-427c-ae53-16d552837ffb_1904x188.png 1272w, https://substackcdn.com/image/fetch/$s_!OROP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f971db0-de86-427c-ae53-16d552837ffb_1904x188.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OROP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f971db0-de86-427c-ae53-16d552837ffb_1904x188.png" width="1456" height="144" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9f971db0-de86-427c-ae53-16d552837ffb_1904x188.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:144,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:73606,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!OROP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f971db0-de86-427c-ae53-16d552837ffb_1904x188.png 424w, https://substackcdn.com/image/fetch/$s_!OROP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f971db0-de86-427c-ae53-16d552837ffb_1904x188.png 848w, https://substackcdn.com/image/fetch/$s_!OROP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f971db0-de86-427c-ae53-16d552837ffb_1904x188.png 1272w, https://substackcdn.com/image/fetch/$s_!OROP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9f971db0-de86-427c-ae53-16d552837ffb_1904x188.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>In the Status column you can see:</p><pre><code>(failed) net::ERR_INVALID_SIGNED_EXCHANGE</code></pre><p>When you inspect the request details and open the <strong>Preview</strong> tab, you may see something like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!g6s9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7daf894a-72b9-415c-ad69-9dfd000406c1_1650x1008.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!g6s9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7daf894a-72b9-415c-ad69-9dfd000406c1_1650x1008.png 424w, https://substackcdn.com/image/fetch/$s_!g6s9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7daf894a-72b9-415c-ad69-9dfd000406c1_1650x1008.png 848w, https://substackcdn.com/image/fetch/$s_!g6s9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7daf894a-72b9-415c-ad69-9dfd000406c1_1650x1008.png 1272w, https://substackcdn.com/image/fetch/$s_!g6s9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7daf894a-72b9-415c-ad69-9dfd000406c1_1650x1008.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!g6s9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7daf894a-72b9-415c-ad69-9dfd000406c1_1650x1008.png" width="1456" height="889" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7daf894a-72b9-415c-ad69-9dfd000406c1_1650x1008.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:889,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:240151,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!g6s9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7daf894a-72b9-415c-ad69-9dfd000406c1_1650x1008.png 424w, https://substackcdn.com/image/fetch/$s_!g6s9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7daf894a-72b9-415c-ad69-9dfd000406c1_1650x1008.png 848w, https://substackcdn.com/image/fetch/$s_!g6s9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7daf894a-72b9-415c-ad69-9dfd000406c1_1650x1008.png 1272w, https://substackcdn.com/image/fetch/$s_!g6s9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7daf894a-72b9-415c-ad69-9dfd000406c1_1650x1008.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The error message says:</p><pre><code>Content type of cert-url must be application/cert-chain+cbor. Actual content type: text/html Failed to fetch the certificate.</code></pre><p>Also, you may notice the <strong>Signature</strong>&#8594;<strong>Certificate URL</strong> field is marked in red.</p><p>In this case, the signed exchange is invalid because the certificate is inaccessible. The browser tried to download it but received an HTML page instead. You probably already know what this means. Yes, this HTML page is the fallback page served by Google SXG cache when the file is missing. It seems Google uses it for missing certificates too<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>.</p><h2>Conclusion</h2><p>Now you can diagnose SXG errors and know that most of them are caused by missing entries in the Google SXG cache.</p><ul><li><p>But why are subresources sometimes not present in the SXG cache?</p></li><li><p>How does Google decide which subresources to cache and which not?</p></li><li><p>Most importantly: how do you convince Google to perform the caching of all subresources used by the page?</p></li></ul><p>I will explain it in <a href="https://www.pawelpokrywka.com/p/fixing-sxg-prefetching-errors-caused-by-mutable-subresources">the next parts</a> of the series:</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;4bd2c0be-f7a9-4556-b11d-7c996a00a018&quot;,&quot;caption&quot;:&quot;In the previous part, you learned CORS errors mean (sub)resources are missing from the Google Signed Exchanges (SXG) cache. This and later posts will expla&#8230;&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;The mystery of mutable subresources in Signed Exchanges&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:41077879,&quot;name&quot;:&quot;Pawe&#322; Pokrywka&quot;,&quot;bio&quot;:&quot;I write about security, privacy, and complex systems&#8212;exploring them from the messy middle ground between engineering and research.&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1c35edc2-abb7-4761-8eb4-f379f25e519a_800x800.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-02-11T13:55:00.285Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda5e2fb8-02fe-4ad1-a83a-a946fec4ae32_7389x4621.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.pawelpokrywka.com/p/fixing-sxg-prefetching-errors-caused-by-mutable-subresources&quot;,&quot;section_name&quot;:&quot;Performance&quot;,&quot;video_upload_id&quot;:null,&quot;id&quot;:152105488,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:1,&quot;comment_count&quot;:0,&quot;publication_id&quot;:null,&quot;publication_name&quot;:&quot;Pawe&#322; Pokrywka's Lab&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe44a7fc6-d5ec-4a99-984a-7a7e0bc80a17_800x800.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><p>Thank you for reading!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Want to stay updated? Leave your email below for new post alerts.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>The remaining 1% of the performance benefits is the main HTML document, which is still prefetched.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>This is a theoretical value selected to better illustrate the point. As far as I know, the SXG spec doesn&#8217;t restrict the number of subresources. However, implementations do: currently Cloudflare ASX and Google SXG-cache limit it to 20, as mentioned in <a href="https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges">the previous post</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>Alternatively, you can filter them out by entering the &#8220;webpkgcache&#8220; string in the filter field. I try to avoid that because unless I remove it, it will stay. Later, I forget about it and wonder why I don&#8217;t see requests I expect. That may be frustrating.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p> The algorithm for computing the subdomain and the URL path suffix is the <a href="https://amp.dev/documentation/guides-and-tutorials/learn/amp-caches-and-cors/amp-cache-urls/">same as for the AMP Cache</a>, while the infix string <strong>/doc/-/</strong> is different.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>I&#8217;m not sure why the signature time is 1 hour before the actual time it was generated. It may be a safeguard against a clock of the server generating signature not being in perfect sync with the clock on the client validating it.</p><p>Without this safeguard, if the server generated a signature at 10:00 and the client believes the current time is 9:59, it may reject the signature as invalid because&#8212;from the client&#8217;s perspective&#8212;the signature will start being valid in 1 minute.</p><p>This won&#8217;t be an issue for most users, but when the client is Googlebot, and the server is Cloudflare it may happen regularly.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p>In my experience, a missing certificate is a temporary error that typically lasts for a few minutes at most.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Prefetching subresources with Signed Exchanges]]></title><description><![CDATA[How to make your website load instantly for Google-referred users (part 2 of 10)]]></description><link>https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Mon, 13 Jan 2025 09:45:55 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!QyI0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f9af7ab-a9b5-4fc4-a0bf-6eb804fb2fb0_2952x2293.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QyI0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f9af7ab-a9b5-4fc4-a0bf-6eb804fb2fb0_2952x2293.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset image2-full-screen"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QyI0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f9af7ab-a9b5-4fc4-a0bf-6eb804fb2fb0_2952x2293.jpeg 424w, https://substackcdn.com/image/fetch/$s_!QyI0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f9af7ab-a9b5-4fc4-a0bf-6eb804fb2fb0_2952x2293.jpeg 848w, https://substackcdn.com/image/fetch/$s_!QyI0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f9af7ab-a9b5-4fc4-a0bf-6eb804fb2fb0_2952x2293.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!QyI0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f9af7ab-a9b5-4fc4-a0bf-6eb804fb2fb0_2952x2293.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QyI0!,w_5760,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f9af7ab-a9b5-4fc4-a0bf-6eb804fb2fb0_2952x2293.jpeg" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7f9af7ab-a9b5-4fc4-a0bf-6eb804fb2fb0_2952x2293.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;full&quot;,&quot;height&quot;:1131,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:7941322,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-fullscreen" alt="" srcset="https://substackcdn.com/image/fetch/$s_!QyI0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f9af7ab-a9b5-4fc4-a0bf-6eb804fb2fb0_2952x2293.jpeg 424w, https://substackcdn.com/image/fetch/$s_!QyI0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f9af7ab-a9b5-4fc4-a0bf-6eb804fb2fb0_2952x2293.jpeg 848w, https://substackcdn.com/image/fetch/$s_!QyI0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f9af7ab-a9b5-4fc4-a0bf-6eb804fb2fb0_2952x2293.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!QyI0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f9af7ab-a9b5-4fc4-a0bf-6eb804fb2fb0_2952x2293.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Prefetched assets contributing to page load speed improvement inspired by T<em>he Forge of Vulcan</em> (1630) by Diego Vel&#225;zquez, oil on canvas 223 &#215; 290 cm, Museo del Prado, Madrid</figcaption></figure></div><blockquote><p>Context (Oct 2025): After deep work with SXG, I suspected changes might be coming&#8212;just not this quickly. Cloudflare <a href="https://community.cloudflare.com/t/amp-and-signed-exchanges-deprecation-october-20th/831238">announced SXG deprecation</a>, and I observed SXG stop working around Sept 19, 2025.</p><p>If you still want SXG, the current path is to self-host&#8212;but the tooling looks abandoned, and setup is non-trivial. Also, Google may follow Cloudflare by shutting down the SXG cache and removing SXG support in Chrome.</p></blockquote><p>I will explain how to drastically improve your website's loading time for Google-referred users using a little-known technology called Signed Exchanges (SXG).</p><p>This is the second post in a series. I assume you read <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">the previous part</a>, which contains the fundamental knowledge required to understand and implement the techniques described here. If you don&#8217;t or need a refresher, I strongly suggest you <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">click here</a>.</p><h2>Things to prefetch</h2><p>You learned how to use SXG to prefetch HTML on Google search results. It was an essential step, but further work is required to fully utilize SXG and make your website load in a fraction of a second. If&#8212;apart from HTML&#8212;you prefetch also:</p><ul><li><p>stylesheets, the user can immediately start reading the text and see the final page layout without images,</p></li><li><p>images, preferably the above-the-fold ones, the actual improvements to LCP will materialize,</p></li><li><p>a custom font (if you use it), the user will experience stable-looking text, potentially improving CLS; on pages with a lot of text above the fold, LCP should improve, too,</p></li><li><p>javascript, you will make your page interactive<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> the moment the user clicks the Google result link.</p></li></ul><h2>Fully prefetched website experience</h2><p>If you implement prefetching of all of the above, your website will load instantaneously and become fully interactive, despite the connection quality, given it had enough time to prefetch on the Google results page.</p><p>Below, I demonstrate how it works while offline. You just can&#8217;t make your connection slower than that!</p><blockquote><p>This is a vertical video, so if you are using mobile device, click <a href="https://www.youtube.com/shorts/BW_Hkthiawg">here</a> to open it in the YouTube app for best experience.</p></blockquote><div id="youtube2-BW_Hkthiawg" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;BW_Hkthiawg&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/BW_Hkthiawg?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p>I began by ensuring the browser cache was empty, simulating a first-time visitor experience.</p><p>Next, I entered a search phrase and received results from Google. I enabled airplane mode to simulate the worst-case scenario for the connection.</p><p>After clicking the link to the website, the browser immediately displayed a fully rendered page. I quickly tested the interactive UI elements, confirming that the JavaScript had loaded correctly.</p><p>When I scrolled below the fold, it became clear that the images there were not prefetched. In real-world conditions, network performance should be better than offline, and images below the fold would load in the background as the user interacts with the page.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Subscribe to my blog and discover more fascinating techniques you never knew were possible.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><h2>SXG subresources</h2><p>In the SXG nomenclature, the assets you choose to prefetch along with the main HTML document are called <strong>subresources</strong>. Most of the time, these are above-the-fold images, styles, fonts, and critical scripts.</p><p>This post focuses on them in contrast to the lower-priority assets that can be fetched later, such as below-the-fold images, non-critical Javascript, etc.</p><h2>Basic requirements and limits</h2><h4>Cacheability</h4><p>The subresources have to be cachable, just like the main HTML document. You must set <strong>max-age/s-maxage</strong> directives in the <strong>Cache-Control</strong> header to a value greater than 120 seconds. Given that subresources are mainly static, immutable files, 1 year is a much better option. It will be truncated to 7 days for SXG, but other caches will use the longer period.</p><p>For assets included with the application, I used the following nginx configuration snippet in locations containing static files:</p><pre><code><code>location ~* \.(?:css|js|gif|png|jpeg|jpg|ico|ttf|woff|woff2|svg)$ {
  expires 1y;
  add_header Cache-Control "public";
  passenger_enabled on; # Passenger is used as an app server
}</code></code></pre><h4>No more than 20</h4><p>As far as I know, SXG spec doesn&#8217;t restrict the number of subresources. However, Google SXG cache limits them and Cloudflare ASX honors this limit.</p><p>If your website depends on more than 20 subresources, the SXG experience won&#8217;t be as good as it could be because <a href="https://github.com/google/webpackager/blob/main/docs/cache_requirements.md#:~:text=There%20may%20be%20no%20more%20than%2020">those over the limit won&#8217;t be prefetched</a>. For example, not all images will be immediately visible or the user won&#8217;t be able to fully interact with the website until the missing scripts are downloaded.</p><p>It&#8217;s therefore a good idea to keep the number of subresources below or equal to 20.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!BiLE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12351a09-61c9-408c-8099-1cf00df3e4f8_1920x1490.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BiLE!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12351a09-61c9-408c-8099-1cf00df3e4f8_1920x1490.jpeg 424w, https://substackcdn.com/image/fetch/$s_!BiLE!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12351a09-61c9-408c-8099-1cf00df3e4f8_1920x1490.jpeg 848w, https://substackcdn.com/image/fetch/$s_!BiLE!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12351a09-61c9-408c-8099-1cf00df3e4f8_1920x1490.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!BiLE!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12351a09-61c9-408c-8099-1cf00df3e4f8_1920x1490.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!BiLE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12351a09-61c9-408c-8099-1cf00df3e4f8_1920x1490.jpeg" width="1456" height="1130" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/12351a09-61c9-408c-8099-1cf00df3e4f8_1920x1490.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1130,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2596073,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!BiLE!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12351a09-61c9-408c-8099-1cf00df3e4f8_1920x1490.jpeg 424w, https://substackcdn.com/image/fetch/$s_!BiLE!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12351a09-61c9-408c-8099-1cf00df3e4f8_1920x1490.jpeg 848w, https://substackcdn.com/image/fetch/$s_!BiLE!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12351a09-61c9-408c-8099-1cf00df3e4f8_1920x1490.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!BiLE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12351a09-61c9-408c-8099-1cf00df3e4f8_1920x1490.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">20 SXG subresources limit inspired by <em>The Animals entering Noah&#8217;s Ark</em> (~1570) by Jacopo Bassano, oil on canvas, 207 &#215; 265 cm</figcaption></figure></div><h5>Scripts and CSS files</h5><blockquote><p>You may remember my website combines Ruby on Rails and Next.js. I will use those frameworks to illustrate how to deal with SXG challenges. Even if you use a different framework, the challenges may be similar.</p></blockquote><p>Rails gives you full control over the HTML and therefore over all the <strong>&lt;link&gt;</strong> tags for preloading subresources, so keeping the number of script and CSS files low is easy.</p><p>In the case of Next.js apps, the chunking of the javascript and CSS is delegated to the framework (and handled in the background by <a href="https://webpack.js.org/">webpack</a>). If the framework or webpack decides to generate too many chunks, you are out of luck.</p><p>In my case, I found the number of generated CSS chunks reasonable. However, the number of javascript chunks was way too high.</p><p>By default, Next.js splits JavaScript into chunks to optimize loading. If page A shares some code with page B, Next.js will create three chunks:</p><ol><li><p>Code used only on page A.</p></li><li><p>Code used only on page B.</p></li><li><p>Code shared between both pages.</p></li></ol><p>When loading the pages, page A will load chunks #1 and #3, while page B will load chunks #2 and #3. This way pages avoid loading code they won&#8217;t use.</p><p>To solve the too-many-chunks problem I disabled this optimization on pages receiving the most of the Google traffic. This means users may download the same code again when accessing other pages on the website. I believe the performance impact is minimal since when the user decides to visit another page, most of the non-javascript parts of the page are already present in the browser cache.</p><p>To prevent Next.js from chunking javascript on specific pages you can extend your <strong>next.config.js</strong> using the following template:</p><pre><code>function skipChunking(config, pages) {
  const originalChunks = config.optimization.splitChunks.chunks;
  config.optimization.splitChunks.chunks = (chunk) =&gt; {
    // Dissallow chunking of specified pages because too many
    // chunks break SXG experience. For the full context see:
    // https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges
    if (pages.includes(chunk.name)) return false;

    return originalChunks(chunk);
  }
}

const nextConfig = {

  // Your current config options

  webpack: (config, options) =&gt; {
    // Replace with your own pages array.
    <strong>if (!options.isServer) skipChunking(config, ['pages/page1', ...]);</strong>

    // Your current webpack customizations go here

    return config;
  }
}
 
module.exports = nextConfig</code></pre><h5>Icons</h5><p>Remember <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_images/Implementing_image_sprites_in_CSS">CSS sprites</a>, popular when the majority of the web&#8217;s HTTP traffic used version 1.1? The idea was to put many images into one and use CSS to <em>extract</em> them on the client side.</p><p>HTTP/2 <em>exorcised</em> us from CSS sprites. That&#8217;s because HTTP/2 multiplexing allows us to perform many requests for small assets efficiently.</p><p>But if you have multiple icons on the page and want all of them to be prefetched with SXG, you have to:</p><ul><li><p>embed them into HTML (increasing the size of your page), or</p></li><li><p>invite sprites back to your life.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ngOP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09052ee8-286b-461e-8119-9248b44f49b1_3511x2106.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ngOP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09052ee8-286b-461e-8119-9248b44f49b1_3511x2106.jpeg 424w, https://substackcdn.com/image/fetch/$s_!ngOP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09052ee8-286b-461e-8119-9248b44f49b1_3511x2106.jpeg 848w, https://substackcdn.com/image/fetch/$s_!ngOP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09052ee8-286b-461e-8119-9248b44f49b1_3511x2106.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!ngOP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09052ee8-286b-461e-8119-9248b44f49b1_3511x2106.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ngOP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09052ee8-286b-461e-8119-9248b44f49b1_3511x2106.jpeg" width="1456" height="873" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/09052ee8-286b-461e-8119-9248b44f49b1_3511x2106.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:873,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:3267711,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ngOP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09052ee8-286b-461e-8119-9248b44f49b1_3511x2106.jpeg 424w, https://substackcdn.com/image/fetch/$s_!ngOP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09052ee8-286b-461e-8119-9248b44f49b1_3511x2106.jpeg 848w, https://substackcdn.com/image/fetch/$s_!ngOP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09052ee8-286b-461e-8119-9248b44f49b1_3511x2106.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!ngOP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09052ee8-286b-461e-8119-9248b44f49b1_3511x2106.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">SXG may require to use of CSS sprites again. Inspired by <em>Dancing Fairies</em> by August Malmstr&#246;m (1866), oil on canvas, 149 &#215; 90 cm</figcaption></figure></div><p>You can also use an icon font, but it&#8217;s just a different implementation of the same idea.</p><p>You will find many <a href="https://medium.com/@hayavuk/complete-guide-to-svg-sprites-7e202e215d34">online</a> <a href="https://css-tricks.com/css-sprites/">resources</a> on how to use sprites, so I won&#8217;t rehash this subject here.</p><h4>Cookies, HSTS &amp; the rest</h4><p>There are other <a href="https://developers.cloudflare.com/speed/optimization/other/signed-exchanges/signed-exchanges-caveats/">requirements</a>, but I think most of them should be already met. You don&#8217;t typically configure your web server so that static files set cookies, do you? And I assume you properly set the HSTS header, as I explained in <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">the previous part</a>.</p><p>The remaining requirements:</p><ul><li><p>adjusting the main document to include subresources properly and</p></li><li><p>respecting the same-origin rule</p></li></ul><p>are not that trivial and will be explained below.</p><h2>Early hints</h2><p>Before I explain how to adjust the HTML to prefetch subresources, let's take a step back and look at a different, more generic prefetching technique. It's called <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103">Early Hints</a>, and it allows the browser to start fetching the assets required for the page before the application generates the HTML, enabling the page to load much faster.</p><p>Let's say your server takes a moment to generate HTML because it needs to fetch data from a slow database and process it using an overloaded CPU. With Early Hints, this time can be used by the browser to download CSS, fonts, and images. When the HTML is finally delivered, the browser has everything it needs to render the page instantly.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HgEH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7177bbf5-8e9d-4fc4-89c3-3bed9996727b_705x374.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HgEH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7177bbf5-8e9d-4fc4-89c3-3bed9996727b_705x374.webp 424w, https://substackcdn.com/image/fetch/$s_!HgEH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7177bbf5-8e9d-4fc4-89c3-3bed9996727b_705x374.webp 848w, https://substackcdn.com/image/fetch/$s_!HgEH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7177bbf5-8e9d-4fc4-89c3-3bed9996727b_705x374.webp 1272w, https://substackcdn.com/image/fetch/$s_!HgEH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7177bbf5-8e9d-4fc4-89c3-3bed9996727b_705x374.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HgEH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7177bbf5-8e9d-4fc4-89c3-3bed9996727b_705x374.webp" width="705" height="374" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7177bbf5-8e9d-4fc4-89c3-3bed9996727b_705x374.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:374,&quot;width&quot;:705,&quot;resizeWidth&quot;:705,&quot;bytes&quot;:12518,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!HgEH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7177bbf5-8e9d-4fc4-89c3-3bed9996727b_705x374.webp 424w, https://substackcdn.com/image/fetch/$s_!HgEH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7177bbf5-8e9d-4fc4-89c3-3bed9996727b_705x374.webp 848w, https://substackcdn.com/image/fetch/$s_!HgEH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7177bbf5-8e9d-4fc4-89c3-3bed9996727b_705x374.webp 1272w, https://substackcdn.com/image/fetch/$s_!HgEH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7177bbf5-8e9d-4fc4-89c3-3bed9996727b_705x374.webp 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Left: Diagram of a normal session. Right: Diagram of a session using Early Hints (HTTP 103) for faster asset loading. / <a href="https://medium.com/@imhamoro/have-you-ever-experienced-issues-with-early-hints-feat-http2-103-5667c6181ae5">source</a></figcaption></figure></div><p></p><p>Technically, the server sends two responses: one with the URLs of the assets and another with the actual HTML content. This challenges the common expectation that a single HTTP request should result in only one response. It likely posed a challenge for developers working on browsers and web servers, as they had to adapt to handling multiple responses for a single request.</p><p>The server supporting Early Hints will check if your HTTP response contains a <strong>Link</strong> header:</p><pre><code>Link: &lt;https://www.example.com/style.css&gt;;rel=preload;as=style</code></pre><p>If found, the server generates a separate HTTP response containing this header, followed by the actual HTTP response generated by the app when it's ready.</p><p>Early Hints is a generic mechanism, not tied to SXG. Therefore, it applies to all page loads, not just SXG prefetches. It's beneficial to have it enabled. In Cloudflare, you can do it in the Speed / Optimization / Content Optimization:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Ady7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35f89a2a-a4b2-46d7-a01f-860436b0e6cc_2132x858.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Ady7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35f89a2a-a4b2-46d7-a01f-860436b0e6cc_2132x858.png 424w, https://substackcdn.com/image/fetch/$s_!Ady7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35f89a2a-a4b2-46d7-a01f-860436b0e6cc_2132x858.png 848w, https://substackcdn.com/image/fetch/$s_!Ady7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35f89a2a-a4b2-46d7-a01f-860436b0e6cc_2132x858.png 1272w, https://substackcdn.com/image/fetch/$s_!Ady7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35f89a2a-a4b2-46d7-a01f-860436b0e6cc_2132x858.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Ady7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35f89a2a-a4b2-46d7-a01f-860436b0e6cc_2132x858.png" width="1456" height="586" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/35f89a2a-a4b2-46d7-a01f-860436b0e6cc_2132x858.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:586,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:114287,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Ady7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35f89a2a-a4b2-46d7-a01f-860436b0e6cc_2132x858.png 424w, https://substackcdn.com/image/fetch/$s_!Ady7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35f89a2a-a4b2-46d7-a01f-860436b0e6cc_2132x858.png 848w, https://substackcdn.com/image/fetch/$s_!Ady7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35f89a2a-a4b2-46d7-a01f-860436b0e6cc_2132x858.png 1272w, https://substackcdn.com/image/fetch/$s_!Ady7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F35f89a2a-a4b2-46d7-a01f-860436b0e6cc_2132x858.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Standard prefetching</h2><p>Even if you don&#8217;t use Early Hints, the <strong>Link</strong> header can be useful for standard prefetching.</p><p>If you observe that 95% of users landing on page A go to page B next, you can prefetch page B on page A. This way, those 95% of users will experience instant loading of page B (the remaining 5% will prefetch the page and won&#8217;t use it, screw them).</p><p>Having a <strong>Link</strong> header in the prefetched page will instruct the browser to prefetch assets mentioned there too, improving the user experience.</p><h2>SXG-compatible HTTP header</h2><p>To prefetch a SXG subresource, the browser needs it to be included in a&nbsp;<strong>Link</strong>&nbsp;header of the HTTP response with your HTML document. The header must point to the subresource and include the file's integrity hash (to ensure the subresource is not altered by malicious SXG cache). Here is an example entry for a stylesheet:</p><pre><code>Link: &lt;https://www.example.com/style.css&gt;;rel=preload;as=style,&lt;https://www.example.com/style.css&gt;;rel=allowed-alt-sxg;header-integrity="sha256-H/W6sQAAk1YIBi/NE86aUkNQjVHcYjo6B7Rg3PQ0vDM="</code></pre><blockquote><p>If you wonder why the subresource URL has to be duplicated, it&#8217;s because of the compatibility with responsive images mentioned later in this post.</p></blockquote><p>To calculate the integrity hash, you must transform subresource content and its final HTTP headers<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. Good luck with doing this in your application, especially given some resources are stored far away on the CDN!</p><h2>Automating the generation of the Link header</h2><h4>HTTP Header</h4><p>Fortunately, both Cloudflare Automated Signed Exchanges (ASX) and <a href="https://github.com/google/nginx-sxg-module">the SXG module for nginx</a> offer a much easier solution. The only thing your app has to do is output the standard <strong>Link</strong> header in the HTTP response (the same one used to implement Early Hints):</p><pre><code><code>Link: &lt;https://www.example.com/style.css&gt;;rel=preload;as=style</code></code></pre><p>As you can see, you can skip the integrity hash. ASX/nginx will download the required assets, calculate integrity hashes, generate the SXG-compatible <strong>Link</strong> header, and use it to replace the original one in the HTTP response.</p><h4>HTML-only</h4><p>This doesn&#8217;t work for the nginx SXG module, but when using Cloudflare, instead of setting the <strong>Link</strong> header as described above, you can put the <strong>&lt;link&gt;</strong> tag inside your HTML. Of course, this tag doesn&#8217;t need to include the integrity hash:</p><pre><code>&lt;link rel="preload" href="/style.css" as="style"&gt;</code></pre><p>Cloudflare ASX will parse the HTML, find those tags, download the assets, calculate integrity hashes, and finally generate the <strong>Link</strong> header and include it in the HTTP response to the SXG request.</p><p>This solution seems more elegant because you keep all asset references in the HTML. However, the drawback of not setting the <strong>Link</strong> header is you don&#8217;t get Early Hints and standard prefetching with subresources, because ASX doesn&#8217;t do it for non-SXG requests.</p><h2>Prefetching of SXG subresources in Rails</h2><p>Ruby on Rails automatically creates the <strong>Link</strong> header when you use helpers such as <strong>stylesheet_link_tag, javascript_include_tag, </strong>and<strong> preload_link_tag</strong>. I believe the original motivation was to activate Early Hints when available, but it helps<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a> with SXG too. Therefore, for most Rails apps, SXG prefetching will work automatically for assets loaded with these helpers.</p><p>You probably already use <strong>stylesheet_link_tag</strong> and <strong>javascript_include_tag</strong> helpers for CSS and scripts. Ensure you also add&nbsp;<strong>preload_link_tag</strong>&nbsp;for above-the-fold images&nbsp;and for fonts.</p><h2>Prefetching of SXG subresources in Next.js</h2><p>First, let&#8217;s clarify the goal. We want to:</p><ul><li><p>prefetch SXG subresources and</p></li><li><p>benefit from Early Hints and standard prefetching with subresources.</p></li></ul><p>Next.js <a href="https://github.com/vercel/next.js/discussions/36089">doesn&#8217;t have a mechanism</a> (<a href="https://github.com/vercel/next.js/issues/71722">yet</a>) to set the <strong>Link</strong> header automatically for styles and javascript chunks. Unfortunately, the framework doesn&#8217;t give the developer access to the URLs of those assets either, so the developer can&#8217;t preload them manually. On the bright side, the framework preloads the styles using the <strong>&lt;link&gt;</strong> tag.</p><p>It means various subresource classes have varying support:</p><ul><li><p><strong>Fonts and images:</strong> SXG prefetching works using <strong>&lt;link&gt;</strong> tags in HTML, Early Hints/standard prefetching requires setting the <strong>Link</strong> header manually,</p></li><li><p><strong>Stylesheets:</strong> SXG prefetching works out of the box, the Early Hints/standard prefetching mechanism is unsupported,</p></li><li><p><strong>Javascript:</strong> neither SXG prefetching nor Early Hints/standard prefetching are supported.</p></li></ul><p>In summary, most of the things don&#8217;t work.</p><h4>How to fix it?</h4><p>Adding support for the required features in Next.js and contributing the changes upstream is likely the best long-term solution. However, my team wasn&#8217;t deeply familiar with the framework&#8217;s internals, so this approach could take some time. I needed a quicker solution to address the issue.</p><p>The next idea was to read the HTTP response of the app, parse the HTML, and add the <strong>Link</strong> header using middleware. I considered:</p><ul><li><p><a href="https://nextjs.org/docs/app/building-your-application/routing/middleware">Next.js middleware</a>: at the current form it&#8217;s very basic and not capable of performing the task needed.</p></li><li><p>One of the nginx modules for transforming HTTP responses using high-level languages such as <a href="https://github.com/openresty/lua-nginx-module">Lua</a> or <a href="https://nginx.org/en/docs/njs/">Javascript</a>. It adds complexity to the server configuration and comes with the maintenance cost of supporting a custom nginx module.</p></li></ul><p>Instead, I&#8217;ve decided to use a Cloudflare worker.</p><h4>What is a Cloudflare worker?</h4><p>A Cloudflare worker is a piece of software that runs on every request at the Cloudflare data center closest to the user. You deploy it at a specific URL prefix, and from that point on, it handles all matching requests.</p><p>The worker can generate responses on its own or make requests to your application, acting as middleware. You can find the documentation <a href="https://developers.cloudflare.com/workers/">here</a>.</p><p>For me, it was important, the solution:</p><ul><li><p>uses Javascript, so it&#8217;s easy to write and extend,</p></li><li><p>has great performance and low latency,</p></li><li><p>doesn&#8217;t come with maintenance costs, because it&#8217;s hosted on Cloudflare,</p></li><li><p>has a negligible financial cost.</p></li></ul><h4>Worker requirements</h4><p>The primary requirement was to enhance Next.js HTTP responses to support SXG prefetching and Early Hints/standard prefetching, without modifying responses generated by Rails.</p><p>To differentiate between responses that required adjustments and those that didn&#8217;t, I chose to check for the presence of the <strong>Link</strong> header. The worker was configured to modify only responses where this header was absent.</p><p>SXG subresources are used in documents, so the worker only had to process HTTP responses with HTML content. No need to alter images, CSS, etc.</p><p>The worker had two responsibilities:</p><ol><li><p>Parse the HTML to find:</p><ol><li><p>All <strong>&lt;link&gt;</strong> tags with the <strong>rel</strong> attribute set to <strong>preload</strong>, enabling Early Hints/standard prefetching for stylesheets (generated by the framework) and developer-specified subresources, such as images and fonts.</p></li><li><p>All <strong>&lt;script&gt;</strong> tags with the <strong>src</strong> attribute to enable Early Hints/standard prefetching and SXG prefetching for scripts, while ignoring inline scripts.</p></li></ol></li><li><p>Use this information to add the appropriate <strong>Link</strong> header to the response.</p></li></ol><h4>Link-adder worker implementation</h4><p>Let&#8217;s create the worker:</p><pre><code>npm create cloudflare@latest -- link-adder</code></pre><p>The wizard will ask a few questions:</p><pre><code>&#9500; In which directory do you want to create your application?
&#9474; dir ./link-adder
&#9474;
&#9500; What would you like to start with?
&#9474; category Hello World example
&#9474;
&#9500; Which template would you like to use?
&#9474; type Hello World Worker
&#9474;
&#9500; Which language do you want to use?
&#9474; lang JavaScript
&#9474;

...

&#9474;
&#9500; Do you want to use git for version control?
&#9474; yes git
&#9474;

...

&#9474;
&#9500; Do you want to deploy your application?
&#9474; no deploy via `npm run deploy`</code></pre><p>Now, let&#8217;s paste the following code into the <strong>src/index.js</strong> file:</p><pre><code>export default {
  async fetch(request) {
    const response = await fetch(request);

    // Skip non-html responses and responses containing Link header
    const headers = response.headers;
    if (!(headers.get('content-type') || '').includes('text/html')) {
      return response;
    }
    if (headers.has('link')) return response;

    // Parse the HTML and find all the URLs to preload.
    // A detailed explanation can be found at:
    // https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges
    const subresources = [];
    const rewriter = new HTMLRewriter()
      .on('link[rel*="preload"]', {
        element: link =&gt; (
          subresources.push({
            href: link.getAttribute('href'),
            as: link.getAttribute('as')
          })
        )
      })
      // No need to preload scripts:
      // - external to the site,
      // - intended for legacy browsers not supporting ES Modules.
      .on('script[src^="/"]:not([nomodule])', {
        element: script =&gt; (
          subresources.push({
            href: script.getAttribute('src'),
            as: 'script'
          })
        )
      });

    // Make sure to wait until parsing is complete
    const responseText = await rewriter.transform(response).text();

    // Prepare Link header value.
    // Escaping is left as an exercise for the reader.
    const linkText = subresources
      .map(({ href, as }) =&gt; `&lt;${href}&gt;; rel=preload; as=${as}`)
      .join(',');

    // Prepare mutable headers object and add the Link header
    const newHeaders = new Headers(headers);
    if (linkText) newHeaders.set('link', linkText);

    // Prepare modified response
    const newResponse = new Response(responseText, {
      headers: newHeaders,
      status: response.status,
      statusText: response.statusText
    });

    return newResponse;
  }
};</code></pre><p>And deploy the worker:</p><pre><code>npm run deploy</code></pre><p>If doing this for the first time, you will be asked to authorize at Cloudflare.</p><p>We have the worker ready, now it&#8217;s time to set up the routing. Go to your website configuration in the Cloudflare admin panel and enter the <strong>Worker Routes</strong> section.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!CTNt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b3c9266-a7b9-482f-8882-195c411a7392_2235x1102.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CTNt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b3c9266-a7b9-482f-8882-195c411a7392_2235x1102.png 424w, https://substackcdn.com/image/fetch/$s_!CTNt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b3c9266-a7b9-482f-8882-195c411a7392_2235x1102.png 848w, https://substackcdn.com/image/fetch/$s_!CTNt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b3c9266-a7b9-482f-8882-195c411a7392_2235x1102.png 1272w, https://substackcdn.com/image/fetch/$s_!CTNt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b3c9266-a7b9-482f-8882-195c411a7392_2235x1102.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!CTNt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b3c9266-a7b9-482f-8882-195c411a7392_2235x1102.png" width="1456" height="718" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4b3c9266-a7b9-482f-8882-195c411a7392_2235x1102.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:718,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:155185,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!CTNt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b3c9266-a7b9-482f-8882-195c411a7392_2235x1102.png 424w, https://substackcdn.com/image/fetch/$s_!CTNt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b3c9266-a7b9-482f-8882-195c411a7392_2235x1102.png 848w, https://substackcdn.com/image/fetch/$s_!CTNt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b3c9266-a7b9-482f-8882-195c411a7392_2235x1102.png 1272w, https://substackcdn.com/image/fetch/$s_!CTNt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b3c9266-a7b9-482f-8882-195c411a7392_2235x1102.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Empty list of Cloudflare worker routes.</figcaption></figure></div><p>Let&#8217;s add the route by clicking the <strong>Add route</strong> button:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!V1GM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb3fb7d7-2d67-4f69-81ed-fdfa42394eb3_955x772.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!V1GM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb3fb7d7-2d67-4f69-81ed-fdfa42394eb3_955x772.png 424w, https://substackcdn.com/image/fetch/$s_!V1GM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb3fb7d7-2d67-4f69-81ed-fdfa42394eb3_955x772.png 848w, https://substackcdn.com/image/fetch/$s_!V1GM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb3fb7d7-2d67-4f69-81ed-fdfa42394eb3_955x772.png 1272w, https://substackcdn.com/image/fetch/$s_!V1GM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb3fb7d7-2d67-4f69-81ed-fdfa42394eb3_955x772.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!V1GM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb3fb7d7-2d67-4f69-81ed-fdfa42394eb3_955x772.png" width="955" height="772" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bb3fb7d7-2d67-4f69-81ed-fdfa42394eb3_955x772.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:772,&quot;width&quot;:955,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:73161,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!V1GM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb3fb7d7-2d67-4f69-81ed-fdfa42394eb3_955x772.png 424w, https://substackcdn.com/image/fetch/$s_!V1GM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb3fb7d7-2d67-4f69-81ed-fdfa42394eb3_955x772.png 848w, https://substackcdn.com/image/fetch/$s_!V1GM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb3fb7d7-2d67-4f69-81ed-fdfa42394eb3_955x772.png 1272w, https://substackcdn.com/image/fetch/$s_!V1GM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbb3fb7d7-2d67-4f69-81ed-fdfa42394eb3_955x772.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Adding a Cloudflare worker route.</figcaption></figure></div><p>In the example above, I provided a test URL and selected a worker from the drop-down menu. In a real deployment, once everything is working as expected, the route should be updated to cover the entire site or at least the sections handled by Next.js.</p><p>After clicking the <strong>Save</strong> button, the worker should be fully functional.</p><p>To check if the worker does what it should visit the URL specified in the route with Chrome DevTools opened on the Network tab. You can compare the <a href="https://www.planujemywesele.pl/sxg-tests/link-adder/off/link-header/off">original app response</a> to <a href="https://www.planujemywesele.pl/sxg-tests/link-adder/on/link-header/off">the one processed</a> by the worker. Here is how the processed HTTP response looks like:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!XeaE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7a117b-24dc-417e-b378-b1db27dd9941_2400x1880.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!XeaE!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7a117b-24dc-417e-b378-b1db27dd9941_2400x1880.png 424w, https://substackcdn.com/image/fetch/$s_!XeaE!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7a117b-24dc-417e-b378-b1db27dd9941_2400x1880.png 848w, https://substackcdn.com/image/fetch/$s_!XeaE!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7a117b-24dc-417e-b378-b1db27dd9941_2400x1880.png 1272w, https://substackcdn.com/image/fetch/$s_!XeaE!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7a117b-24dc-417e-b378-b1db27dd9941_2400x1880.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!XeaE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7a117b-24dc-417e-b378-b1db27dd9941_2400x1880.png" width="1456" height="1141" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cd7a117b-24dc-417e-b378-b1db27dd9941_2400x1880.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1141,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:415046,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!XeaE!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7a117b-24dc-417e-b378-b1db27dd9941_2400x1880.png 424w, https://substackcdn.com/image/fetch/$s_!XeaE!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7a117b-24dc-417e-b378-b1db27dd9941_2400x1880.png 848w, https://substackcdn.com/image/fetch/$s_!XeaE!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7a117b-24dc-417e-b378-b1db27dd9941_2400x1880.png 1272w, https://substackcdn.com/image/fetch/$s_!XeaE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd7a117b-24dc-417e-b378-b1db27dd9941_2400x1880.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>You can see above, the URLs for preload links and scripts were included in the <strong>Link</strong> HTTP header.</p><blockquote><p>When comparing the responses you can also compare timing and see that the worker overhead is negligible.</p></blockquote><p>From now on, the pages generated by Next.js will support Early Hints/standard prefetching and SXG subresource prefetching.</p><blockquote><p>This worker is not specific to Next.js, it could be used to fix responses of any framework with similar issues.</p></blockquote><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">If you found the worker implementation useful, consider subscribing to receive more posts like this.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Same-origin requirement</h2><h4>Issue with CDNs</h4><p>It&#8217;s common practice to use a Content Delivery Network (CDN) for asset hosting, with images being probably the most frequently hosted type of content. The idea is to leverage the CDN&#8217;s high-performance, global network of servers, which are optimized for efficiently hosting static files. This reduces the load on your own infrastructure, allowing your servers to focus on running the application more effectively.</p><p>In these scenarios, your website at <strong>https://www.your-domain.com/</strong> might reference assets from one of the following URL types:</p><ul><li><p><strong>CDN endpoint</strong>: https://s3.eu-west-1.amazonaws.com/your-bucket/path/to/file.jpg</p></li><li><p><strong>Custom subdomain</strong>: https://assets.<strong>your-domain.com</strong>/path/to/file.jpg</p></li></ul><p>However, neither approach will work with SXG. If your page preloads an asset from a different origin than the one the page is loaded from&#8212;even if it's a subdomain&#8212;Cloudflare won&#8217;t include it as a subresource in the SXG version of your page.</p><blockquote><p>One day, Cloudflare may start <a href="https://github.com/google/sxg-rs/issues/82">supporting</a> the prefetching of subresources from subdomains. I&#8217;ve created <a href="https://www.planujemywesele.pl/sxg-tests/subdomain">a test</a> to check its current support status, but as of the date of writing this post, that feature is not yet available.</p></blockquote><p>Offloading your assets to CDN, a method meant to make your site load faster does the opposite in the context of SXG!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tOj2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22c99849-eb69-4671-9952-0809d97696c7_2248x2500.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tOj2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22c99849-eb69-4671-9952-0809d97696c7_2248x2500.jpeg 424w, https://substackcdn.com/image/fetch/$s_!tOj2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22c99849-eb69-4671-9952-0809d97696c7_2248x2500.jpeg 848w, https://substackcdn.com/image/fetch/$s_!tOj2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22c99849-eb69-4671-9952-0809d97696c7_2248x2500.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!tOj2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22c99849-eb69-4671-9952-0809d97696c7_2248x2500.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tOj2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22c99849-eb69-4671-9952-0809d97696c7_2248x2500.jpeg" width="1456" height="1619" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/22c99849-eb69-4671-9952-0809d97696c7_2248x2500.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1619,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1409720,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tOj2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22c99849-eb69-4671-9952-0809d97696c7_2248x2500.jpeg 424w, https://substackcdn.com/image/fetch/$s_!tOj2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22c99849-eb69-4671-9952-0809d97696c7_2248x2500.jpeg 848w, https://substackcdn.com/image/fetch/$s_!tOj2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22c99849-eb69-4671-9952-0809d97696c7_2248x2500.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!tOj2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22c99849-eb69-4671-9952-0809d97696c7_2248x2500.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">CDN SXG incompatibility, inspired by <em>Hippomenes and Atalanta</em> (~1680) by Nicolas Colombel, oil on canvas, 141&nbsp;&#215;&nbsp;127&nbsp;cm, Liechtenstein Museum, Vienna</figcaption></figure></div><h4>Proxy-based URL rewriting</h4><p>No need to move your files back from the CDN to your server just yet!</p><p>What about using good old URL rewriting? This technique allows you to serve assets under a new, SXG-compatible URL.</p><p>If done on your server, it would function similarly to the previous solution, with the proxy gradually copying files into the local cache. But what if someone else could handle that for us? You guessed it&#8212;we'll use Cloudflare Workers again!</p><p>The Worker will leverage Cloudflare's cache, so if you're paying for egress traffic (like with Amazon Web Services), you'll also reduce your costs as an added bonus.</p><h4>URL-rewriter worker implementation</h4><p>Our goal is to send the user the contents of the <strong>https://cdn.com/your-bucket/image.jpg</strong> file to the user requesting the <strong>https://www.your-domain.com/cdn-proxy/image.jpg</strong> URL.</p><p>Create another worker project:</p><pre><code><code>npm create cloudflare@latest -- url-rewriter</code></code></pre><p>Now, paste the following code into the <strong>src/index.js</strong> file:</p><pre><code>const MAP = {
  'https://www.your-domain.com/cdn-proxy/':
  'https://cdn.com/your-bucket/',
  // You may add more mappings such as:
  // 'external-sxg-prefetchable-url': 'cdn-url'
}

export default {
  async fetch(request) {
    const url = request.url;
    // Find a mapping and apply to the url.
    // A detailed explanation can be found at:
    // https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges
    for (let prefix in MAP) {
      if (url.indexOf(prefix) === 0) {
        const newUrl = request.url.replace(prefix, MAP[prefix]);
        request = new Request(newUrl);
        break;
      }
    }
    return fetch(request);
  },
};</code></pre><p>Finally, deploy the worker:</p><pre><code><code>npm run deploy</code></code></pre><p>And add a route in the Cloudflare admin panel, so that the <strong>https://www.your-domain.com/cdn-proxy/*</strong> prefix is handled by the <strong>url-rewriter</strong> worker.</p><p>Try to access <strong>https://www.your-domain.com/cdn-proxy/image.jpg</strong> to see if it works.</p><p>Now, update your app to reference new URLs and you are ready to go! If the previously mentioned requirements are met, SXG prefetching for your assets should work correctly.</p><h2>Interesting subresource types</h2><p>Prefetching scripts or styles is simple. Some subresource classes, however, deserve further discussion.</p><h4>Custom fonts</h4><p>The general advice is to avoid custom fonts to improve performance, but it&#8217;s not always possible. There are at least three ways to implement custom fonts on your website:</p><ul><li><p><strong>Self-hosting:</strong> Treat font files as other assets and reference them from your CSS. In addition to the WOFF2 format, support for legacy browsers requires using the WOFF1 simultaneously. In the past, it was necessary to provide <a href="https://css-tricks.com/snippets/css/using-font-face-in-css/#h-deepest-possible-browser-support">multiple file formats</a> to ensure compatibility with older browsers. Things improved!</p></li><li><p><strong>CSS embedding:</strong> Include reference to an external CSS file in your HTML which downloads the fonts in the format best suited for the user&#8217;s browser from the external font provider CDN. Examples: embedded <a href="https://fonts.google.com/">Google Fonts</a>, Web Fonts by <a href="https://typography.com/">Hoefler&amp;Co</a>.</p></li><li><p><strong>Web font loader:</strong> Include a javascript snippet in your HTML that does the same as above. Example: <a href="https://fonts.adobe.com/">Adobe Fonts</a> when using the dynamic subsetting feature.</p></li></ul><p>Avoid anything other than self-hosted fonts, because loading fonts from an external CDN doesn&#8217;t work with SXG prefetching. Also, CSS embedding and JavaScript loaders are terrible from a performance standpoint:</p><ul><li><p>CSS method blocks font downloading until the CSS file is fetched from the font provider. The CSS file is hosted on an external domain, so it can&#8217;t be prefetched using SXG unless a proxying Cloudflare worker is used as a workaround. But it won&#8217;t work even then, because its content has to be dynamically generated depending on the <strong>User-Agent</strong> header. Caching it in Google SXG cache would interfere with this browser-sniffing mechanism.</p></li></ul><ul><li><p>The web font loader is even worse because it adds JavaScript loading and execution, slowing things down even more.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HuCB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8f77b3-f828-4f8b-8829-2c681d68ee9b_2124x1317.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HuCB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8f77b3-f828-4f8b-8829-2c681d68ee9b_2124x1317.jpeg 424w, https://substackcdn.com/image/fetch/$s_!HuCB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8f77b3-f828-4f8b-8829-2c681d68ee9b_2124x1317.jpeg 848w, https://substackcdn.com/image/fetch/$s_!HuCB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8f77b3-f828-4f8b-8829-2c681d68ee9b_2124x1317.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!HuCB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8f77b3-f828-4f8b-8829-2c681d68ee9b_2124x1317.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HuCB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8f77b3-f828-4f8b-8829-2c681d68ee9b_2124x1317.jpeg" width="1456" height="903" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5d8f77b3-f828-4f8b-8829-2c681d68ee9b_2124x1317.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:903,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1051753,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!HuCB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8f77b3-f828-4f8b-8829-2c681d68ee9b_2124x1317.jpeg 424w, https://substackcdn.com/image/fetch/$s_!HuCB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8f77b3-f828-4f8b-8829-2c681d68ee9b_2124x1317.jpeg 848w, https://substackcdn.com/image/fetch/$s_!HuCB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8f77b3-f828-4f8b-8829-2c681d68ee9b_2124x1317.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!HuCB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d8f77b3-f828-4f8b-8829-2c681d68ee9b_2124x1317.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Choosing not to self-host fonts may hurt page loading speed. Inspired by <em>Hylas and a group of nymphs</em> (1896) by John William Waterhouse, oil on canvas, 98.2 &#215; 163.3 cm</figcaption></figure></div><p>To self-host a font you need a <strong>@font-face</strong> declaration:</p><pre><code>@font-face {
  font-family: "My custom font";
  src: url("/fonts/custom.woff2") format("woff2"),
       url("/fonts/custom.woff") format("woff");

  /* other properties, such as font-weight, etc. */
}</code></pre><p>To enable SXG prefetching, place the following in the HTML head:</p><pre><code>&lt;link rel="preload" href="/fonts/custom.woff2" as="font" type="font/woff2" crossorigin="anonymous" /&gt;</code></pre><p>I added the <strong>type</strong> attribute to ensure that browsers without WOFF2 support won&#8217;t prefetch it.</p><p>The above snippet prefetches only the WOFF2 format for the reasons below:</p><ul><li><p>Prefetching the legacy WOFF1 format could improve performance for a small number of users with older browsers (likely without SXG support). However, this would come at the cost of reduced performance for the majority of users with modern browsers. These browsers support both WOFF1 and WOFF2 formats, and prefetching both would result in downloading two files when only one is needed.</p></li><li><p>Also, adding another file to prefetch would consume one of 20 valuable SXG subresource slots.</p></li></ul><h4>Responsive images</h4><p>The responsive images technique is about serving different images depending on the screen dimensions. A user owning a 32-inch, 8K-grade screen may appreciate <strong>a horizontal, high-resolution</strong> image. On the other hand, a low-end phone user on a metered connection would prefer <strong>a vertical, low-resolution</strong> image.</p><p>Here is an example of a responsive image:</p><pre><code>&lt;img src="wolf_400px.jpg" srcset="wolf_400px.jpg 400w, wolf_800px.jpg 800w, wolf_1600px.jpg 1600w" sizes="50vw"&gt;</code></pre><p>It will load the <strong>wolf_400px.jpg</strong> image on a small screen, <strong>wolf_800px.jpg</strong> on a larger screen, and <strong>wolf_1600px.jpg</strong> on the largest. The image specified in the <strong>href</strong> attribute is used by legacy browsers that do not support responsive images. The <strong>sizes</strong> attribute tells how much screen space is available for an image&#8212;in this case, it&#8217;s 50% of the screen width.</p><p>Let&#8217;s forget about SXG for a minute. If you want to speed up the loading of an image, you can prefetch it by placing a <strong>&lt;link&gt;</strong> tag in the HTML head. This way the browser will download the file very early, before the rest of the HTML is processed.</p><p>But which version of the image should be preloaded? To enable preloading for responsive images, you must specify additional attributes on the <strong>&lt;link&gt;</strong> preload tags that mimic <strong>srcset</strong> and <strong>sizes</strong> attributes on the <strong>&lt;img&gt;</strong> tags: <strong>imagesrcset</strong> and <strong>imagesizes</strong> accordingly (I used examples from <a href="https://web.dev/articles/preload-responsive-images">this guide</a>):</p><pre><code>&lt;link rel="preload" as="image" href="wolf_400px.jpg" imagesrcset="wolf_400px.jpg 400w, wolf_800px.jpg 800w, wolf_1600px.jpg 1600w" imagesizes="50vw"&gt;</code></pre><p>If your website uses responsive images, the good news is they can be optimally prefetched with SXG. The Google SXG cache stores all versions of the image and the user&#8217;s browser chooses which one to prefetch on the Google results page.</p><p>The first step to enable SXG prefetching of responsive images is to put the <strong>&lt;link&gt;</strong> tag in the HTML of your page as described above. This link will be translated into the following <strong>Link</strong> header in the SXG response by Cloudflare ASX:</p><pre><code>Link: &lt;https://example.com/wolf_400px.jpg&gt;;rel=preload;as=image;imagesrcset="wolf_400px.jpg 400w, wolf_800px.jpg 800w, wolf_1600px.jpg 1600w";imagesizes="50vw",&lt;https://example.com/wolf_400px.jpg&gt;;rel=allowed-alt-sxg;header-integrity="sha256-mfpQfImL1YSp8DM3HW0y235K5of+vJAdM0pbIh9MAnI="</code></pre><blockquote><p>I found Cloudflare ASX has a very strict HTML parser. If you include a new line in the attribute (such as <strong>imagesrcset)</strong> value, it won&#8217;t include this attribute in the resulting SXG response.</p><p>The Chrome browser parser is less strict, so in your local tests, everything will be fine. Be sure to minify your HTML before deployment or avoid newlines in <strong>&lt;link&gt;</strong> tags attributes.</p></blockquote><p>However, when you try to prefetch your page you will get the following message in the <strong>Warning</strong> HTTP header of the SXG response from the Google SXG cache:</p><pre><code>199 - "debug: content has ingestion error: SXG ingestion failure: Invalid link preload subresources; validating headers"</code></pre><p>That&#8217;s because Google SXG cache couldn&#8217;t find the integrity hash for all image versions. Cloudflare ASX generated it only for the <strong>wolf_400px.jpg (</strong>the version specified in the <strong>href</strong> attribute).</p><p>The solution is to prepare one <strong>&lt;link&gt;</strong> tag for each image version:</p><pre><code>&lt;link rel="preload" as="image" <strong>href="wolf_400px.jpg"</strong> imagesrcset="wolf_400px.jpg 400w, wolf_800px.jpg 800w, wolf_1600px.jpg 1600w" imagesizes="50vw"&gt;
&lt;link rel="preload" as="image" <strong>href="wolf_800px.jpg"</strong> imagesrcset="wolf_400px.jpg 400w, wolf_800px.jpg 800w, wolf_1600px.jpg 1600w" imagesizes="50vw"&gt;
&lt;link rel="preload" as="image" <strong>href="wolf_1600px.jpg"</strong> imagesrcset="wolf_400px.jpg 400w, wolf_800px.jpg 800w, wolf_1600px.jpg 1600w" imagesizes="50vw"&gt;</code></pre><p>As you can see above, all of the tags look identical except the <strong>href</strong> attribute. As a result, Cloudflare ASX will generate integrity hashes for all versions:</p><pre><code>Link: &lt;https://example.com/wolf_400px.jpg&gt;;rel=preload;as=image;imagesrcset="wolf_400px.jpg 400w, wolf_800px.jpg 800w, wolf_1600px.jpg 1600w";imagesizes="50vw",&lt;https://example.com/wolf_400px.jpg&gt;;rel=allowed-alt-sxg;<strong>header-integrity="sha256-mfpQfImL1YSp8DM3HW0y235K5of+vJAdM0pbIh9MAnI="</strong>,
&lt;https://example.com/wolf_800px.jpg&gt;;rel=preload;as=image;imagesrcset="wolf_400px.jpg 400w, wolf_800px.jpg 800w, wolf_1600px.jpg 1600w";imagesizes="50vw",&lt;https://example.com/wolf_800px.jpg&gt;;rel=allowed-alt-sxg;<strong>header-integrity="sha256-Xtd1ha7j8bRbUE/z7IULqPzPN4ZuUvapKBAEZ5XVvg8="</strong>,
&lt;https://example.com/wolf_1600px.jpg&gt;;rel=preload;as=image;imagesrcset="wolf_400px.jpg 400w, wolf_800px.jpg 800w, wolf_1600px.jpg 1600w";imagesizes="50vw",&lt;https://example.com/wolf_1600px.jpg&gt;;rel=allowed-alt-sxg;<strong>header-integrity="sha256-d0Pusf0qpsEUmIF+dTbvYa8pFmi4BmNt7/HzO0pHDiY="</strong></code></pre><p>As you can see, there is a lot of duplication here. <a href="https://github.com/WICG/webpackage/blob/main/explainers/signed-exchange-subresource-substitution.md#cant-we-merge-allowed-alt-sxg-to-preload-header">It would be enough</a> to have something like below, but I couldn&#8217;t find a way to achieve that with Cloudflare ASX other than preparing the <strong>Link</strong> header all by myself (including integrity hashes):</p><pre><code>Link: &lt;https://example.com/wolf_400px.jpg&gt;;rel=preload;as=image;imagesrcset="wolf_400px.jpg 400w, wolf_800px.jpg 800w, wolf_1600px.jpg 1600w";imagesizes="50vw",&lt;https://example.com/wolf_400px.jpg&gt;;rel=allowed-alt-sxg;<strong>header-integrity="sha256-mfpQfImL1YSp8DM3HW0y235K5of+vJAdM0pbIh9MAnI="</strong>,
&lt;https://example.com/wolf_800px.jpg&gt;;rel=allowed-alt-sxg;<strong>header-integrity="sha256-Xtd1ha7j8bRbUE/z7IULqPzPN4ZuUvapKBAEZ5XVvg8="</strong>,
&lt;https://example.com/wolf_1600px.jpg&gt;;rel=allowed-alt-sxg;<strong>header-integrity="sha256-d0Pusf0qpsEUmIF+dTbvYa8pFmi4BmNt7/HzO0pHDiY="</strong></code></pre><p>Nonetheless, the Google SXG cache happily ingests the document with the verbose <strong>Link</strong> header value, and the warning about the SXG ingestion failure disappears. I prepared a demo page showcasing <a href="https://www.planujemywesele.pl/sxg-tests/responsive-images">the correct and incorrect approaches to prefetching responsive images with SXG</a>.</p><blockquote><p>On the downside, older browsers will prefetch all the versions wasting the bandwidth. At the time of writing, this is 5% of desktop and 6% of mobile users, but <a href="https://caniuse.com/mdn-html_elements_link_imagesrcset">it will decline</a>. There may be ways to improve the experience for users of legacy browsers, but I haven&#8217;t researched them.</p></blockquote><p>If&#8212;apart from SXG prefetching&#8212;you want to use responsive images with Early Hints I have bad news. At the time of writing <a href="https://developer.chrome.com/docs/web-platform/early-hints#current-limitations">responsive images are not supported with Early Hints</a>.</p><p>However, this may change in the future. Additionally, remember that support for responsive images already exists in standard prefetching. Therefore, to fully benefit from responsive images in Next.js, you must adjust the <strong>link-adder</strong> worker (the first one mentioned in this post). Along with the <strong>href</strong> and <strong>as</strong> attributes, the worker must also support the <strong>imagesrcset</strong> and <strong>imagesizes</strong> attributes in cases they are set. The change should be fairly easy to implement, I&#8217;m sure you can handle it!</p><h4>Images combined with a responsive layout</h4><p>Responsive websites use different layouts depending on the screen dimensions. The common practice is to use a 1-column layout for mobile phones and 2+ columns on desktops.</p><p>If the columns contain graphics, the initial viewport on the phone will typically include 1 or 2 images, while on the desktop, the user will see more of them.</p><p>The efficient preloading/prefetching strategy avoids loading images that are not visible in the initial viewport. To save bandwidth, you want to load all the pictures the user sees when the page loads and no more than that.</p><p>The dilemma this time is not which version of the image, but which images to choose.</p><p>The first thing that comes to mind is using the <strong>media</strong> attribute on the <strong>&lt;link&gt;</strong> tag:</p><pre><code>&lt;link rel="preload" as="image" href="1.png"&gt;
&lt;link rel="preload" as="image" href="2.png" <strong>media="(min-width: 800px)"</strong>&gt;</code></pre><p>If the browser loads above HTML it will preload 2 images if the screen width is 800px or more. If it&#8217;s less, then only the first image will be preloaded.</p><p>However, things become different in the context of SXG prefetching. Cloudflare ASX correctly puts the <strong>media</strong> attribute in the <strong>Link</strong> header, but <a href="https://www.planujemywesele.pl/sxg-tests/responsive-layout">my tests show the browser ignores it</a>. As a result, both images are loaded regardless of screen size.</p><p>I solved it by <s>abusing</s> adjusting the <strong>imagesrcset</strong> and <strong>imagesizes</strong> attributes on the <strong>&lt;link&gt;</strong> tags:</p><pre><code>&lt;link rel="preload" as="image" href="1.png"&gt;
&lt;link rel="preload" as="image" href="2.png" <strong>imagesrcset="1.png 2w, 2.png 500w" imagesizes="(max-width: 799px) 1px"</strong>&gt;</code></pre><p>The first <strong>&lt;link&gt;</strong> tag preloads <strong>1.png</strong> unconditionally. However, the second <strong>&lt;link&gt;</strong> tag preloads <strong>2.png</strong> only if the screen is 800px wide or more. Otherwise, it preloads <strong>1.png</strong> again (the browser is smart enough to not perform a second request) and <strong>2.png</strong> is not preloaded which meets the requirements.</p><p>It works because in the second <strong>&lt;link&gt;</strong> tag, the browser is being told:</p><ul><li><p><strong>2.png</strong> is 500 px wide,</p></li><li><p>1<strong>.png</strong> is 2px wide (not true),</p></li><li><p>for screen widths below 800px, there is only 1px room for the image (also not true).</p></li></ul><blockquote><p>The <strong>&lt;link&gt;</strong> tag is responsible for preloading, not rendering, therefore it doesn&#8217;t matter if some things are lies. Our goal is to preload the files. They will be rendered later using a <strong>&lt;img&gt;</strong> tag with a correct <strong>srcset</strong> and <strong>sizes</strong> attributes.</p></blockquote><p>If the screen width is below 800px, the browser recognizes the space is limited (1px), so it checks which image has the closest width. As the browser thinks <strong>1.png</strong> is 2px wide, it preloads it.</p><p>This approach may be combined with multiple image versions from the previous section. It requires introducing another image that is always preloaded and has only 1 version. You can mix different image formats, so an SVG logo may be a good candidate - it&#8217;s used everywhere and is scalable, so one file is enough:</p><pre><code>&lt;link rel="preload" as="image" href=1st-tiny.png" imagesrcset="1st-tiny.png 500w, 1st-big.png 1000w" imagesizes="(max-width: 750px) 100vw, 50vw"&gt;
&lt;link rel="preload" as="image" href="1st-big.png" imagesrcset="1st-tiny.png 500w, 1st-big.png 1000w" imagesizes="(max-width: 750px) 100vw, 50vw"&gt;

&lt;link rel="preload" as="image" href="logo.svg"&gt;

&lt;link rel="preload" as="image" href="2nd-tiny.png" imagesrcset="logo.svg 2w, 2nd-tiny.png 500w, 2nd-big.png 1000w" imagesizes="(max-width: 750px) 1px, 50vw"&gt;
&lt;link rel="preload" as="image" href="2nd-big.png" imagesrcset="logo.svg 2w, 2nd-tiny.png 500w, 2nd-big.png 1000w" imagesizes="(max-width: 750px) 1px, 50vw"&gt;</code></pre><p>This approach works with SXG-prefetching. You can see it in action on <a href="https://www.planujemywesele.pl/sxg-tests/responsive-layout-no-media">the demo page</a>.</p><h4>Be careful about exceeding your subresource limit</h4><p>It&#8217;s worth noting that depending on the number of images you want to prefetch and the number of versions of each image, it may quickly fill up all available subresource slots:</p><pre><code>images &#215; versions + logo + styles + javascripts + fonts + icons &lt;= 20</code></pre><p>Even if your page uses only 1 CSS file, 1 javascript, 4 fonts (normal, bold, italic, bold italic), 1 icon, and 1 logo you are left with 12 slots, which allows you to preload:</p><ul><li><p>12 images (all in 1 version),</p></li><li><p>6 images in 2 versions each,</p></li><li><p>4 images in 3 versions each,</p></li><li><p>3 images in 4 versions each,</p></li><li><p>2 images in 6 versions each (probably doesn&#8217;t make sense),</p></li><li><p>1 image in 12 versions (seems like an overkill).</p></li></ul><p>It&#8217;s up to you to decide which subresources to prefetch for the best user experience.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">If you&#8217;re enjoying this, subscribe to get new posts delivered to your inbox. Every new subscriber brings a unicorn to life. &#129412;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Verifying prefetching of SXG subresources</h2><p>Now, after you&#8217;ve applied all the hints you found here and in <a href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms">the previous part</a> of the series, it&#8217;s time to check if your website is being correctly prefetched with subresources.</p><p>Instead of waiting until Google indexes your site, head over to <a href="https://signed-exchange-testing.dev/prefetch/">the prefetch testing tool</a>, type the URL of the page you want to test, and hit the <strong>Submit</strong> button to make the Google SXG cache aware of your page.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!VYzQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e93225f-889d-45f2-b0c3-5c16e658d0a3_1688x545.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!VYzQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e93225f-889d-45f2-b0c3-5c16e658d0a3_1688x545.png 424w, https://substackcdn.com/image/fetch/$s_!VYzQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e93225f-889d-45f2-b0c3-5c16e658d0a3_1688x545.png 848w, https://substackcdn.com/image/fetch/$s_!VYzQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e93225f-889d-45f2-b0c3-5c16e658d0a3_1688x545.png 1272w, https://substackcdn.com/image/fetch/$s_!VYzQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e93225f-889d-45f2-b0c3-5c16e658d0a3_1688x545.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!VYzQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e93225f-889d-45f2-b0c3-5c16e658d0a3_1688x545.png" width="728" height="235" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2e93225f-889d-45f2-b0c3-5c16e658d0a3_1688x545.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:470,&quot;width&quot;:1456,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:102835,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!VYzQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e93225f-889d-45f2-b0c3-5c16e658d0a3_1688x545.png 424w, https://substackcdn.com/image/fetch/$s_!VYzQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e93225f-889d-45f2-b0c3-5c16e658d0a3_1688x545.png 848w, https://substackcdn.com/image/fetch/$s_!VYzQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e93225f-889d-45f2-b0c3-5c16e658d0a3_1688x545.png 1272w, https://substackcdn.com/image/fetch/$s_!VYzQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e93225f-889d-45f2-b0c3-5c16e658d0a3_1688x545.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><ol><li><p>Give Google SXG cache time to fetch the page and all the subresources. In my experience, it should take no more than a minute, typically about 15 seconds.</p></li><li><p>Then, hit <strong>Submit</strong> again to prefetch everything.</p></li><li><p>Go offline in Chrome Dev Tools or disconnect your Internet connection.</p></li><li><p>Click the&nbsp;<strong>link to target</strong> link.</p></li></ol><p>You should see your page instantly rendered with all the subresources you specified. If this is what you see&#8212;congrats, you did it! But don&#8217;t relax just yet&#8212;there&#8217;s more to come. For now, you can take a moment to admire this beautiful fresco:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!IYLx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4734d8bb-432c-42b9-82c9-a5e442a35ce9_5238x4290.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset image2-full-screen"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IYLx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4734d8bb-432c-42b9-82c9-a5e442a35ce9_5238x4290.jpeg 424w, https://substackcdn.com/image/fetch/$s_!IYLx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4734d8bb-432c-42b9-82c9-a5e442a35ce9_5238x4290.jpeg 848w, https://substackcdn.com/image/fetch/$s_!IYLx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4734d8bb-432c-42b9-82c9-a5e442a35ce9_5238x4290.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!IYLx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4734d8bb-432c-42b9-82c9-a5e442a35ce9_5238x4290.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IYLx!,w_5760,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4734d8bb-432c-42b9-82c9-a5e442a35ce9_5238x4290.jpeg" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4734d8bb-432c-42b9-82c9-a5e442a35ce9_5238x4290.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;full&quot;,&quot;height&quot;:1192,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:22390225,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-fullscreen" alt="" srcset="https://substackcdn.com/image/fetch/$s_!IYLx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4734d8bb-432c-42b9-82c9-a5e442a35ce9_5238x4290.jpeg 424w, https://substackcdn.com/image/fetch/$s_!IYLx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4734d8bb-432c-42b9-82c9-a5e442a35ce9_5238x4290.jpeg 848w, https://substackcdn.com/image/fetch/$s_!IYLx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4734d8bb-432c-42b9-82c9-a5e442a35ce9_5238x4290.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!IYLx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4734d8bb-432c-42b9-82c9-a5e442a35ce9_5238x4290.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The Apotheosis of Hercules (1731&#8211;1736) by Fran&#231;ois Lemoyne, fresco 1850 &#215; 1700 cm</figcaption></figure></div><p>However, if you see a &#8220;no internet&#8221; message or something similar to this instead:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Qdhk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ab7b0de-b806-4da0-9fce-fd8622f789ac_2090x1858.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Qdhk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ab7b0de-b806-4da0-9fce-fd8622f789ac_2090x1858.png 424w, https://substackcdn.com/image/fetch/$s_!Qdhk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ab7b0de-b806-4da0-9fce-fd8622f789ac_2090x1858.png 848w, https://substackcdn.com/image/fetch/$s_!Qdhk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ab7b0de-b806-4da0-9fce-fd8622f789ac_2090x1858.png 1272w, https://substackcdn.com/image/fetch/$s_!Qdhk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ab7b0de-b806-4da0-9fce-fd8622f789ac_2090x1858.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Qdhk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ab7b0de-b806-4da0-9fce-fd8622f789ac_2090x1858.png" width="728" height="647" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9ab7b0de-b806-4da0-9fce-fd8622f789ac_2090x1858.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:1294,&quot;width&quot;:1456,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:211133,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Qdhk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ab7b0de-b806-4da0-9fce-fd8622f789ac_2090x1858.png 424w, https://substackcdn.com/image/fetch/$s_!Qdhk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ab7b0de-b806-4da0-9fce-fd8622f789ac_2090x1858.png 848w, https://substackcdn.com/image/fetch/$s_!Qdhk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ab7b0de-b806-4da0-9fce-fd8622f789ac_2090x1858.png 1272w, https://substackcdn.com/image/fetch/$s_!Qdhk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9ab7b0de-b806-4da0-9fce-fd8622f789ac_2090x1858.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Either you haven't waited long enough for the SXG cache, made an error somewhere, or&#8230; the SXG gods don't like you!</p><h2>Troubleshooting</h2><p>Don&#8217;t worry. Before you hit the <strong>Submit</strong> button in the prefetch testing tool, please open the Network tab in Chrome Dev Tools. You may see red <strong>CORS error</strong> messages after pressing this button.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RMTN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb9e440-514a-4393-a54f-cfd9f080b809_2140x735.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RMTN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb9e440-514a-4393-a54f-cfd9f080b809_2140x735.png 424w, https://substackcdn.com/image/fetch/$s_!RMTN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb9e440-514a-4393-a54f-cfd9f080b809_2140x735.png 848w, https://substackcdn.com/image/fetch/$s_!RMTN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb9e440-514a-4393-a54f-cfd9f080b809_2140x735.png 1272w, https://substackcdn.com/image/fetch/$s_!RMTN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb9e440-514a-4393-a54f-cfd9f080b809_2140x735.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RMTN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb9e440-514a-4393-a54f-cfd9f080b809_2140x735.png" width="1456" height="500" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5fb9e440-514a-4393-a54f-cfd9f080b809_2140x735.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:500,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:134802,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!RMTN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb9e440-514a-4393-a54f-cfd9f080b809_2140x735.png 424w, https://substackcdn.com/image/fetch/$s_!RMTN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb9e440-514a-4393-a54f-cfd9f080b809_2140x735.png 848w, https://substackcdn.com/image/fetch/$s_!RMTN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb9e440-514a-4393-a54f-cfd9f080b809_2140x735.png 1272w, https://substackcdn.com/image/fetch/$s_!RMTN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb9e440-514a-4393-a54f-cfd9f080b809_2140x735.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Those errors like to appear randomly. Some pages may work correctly, some may not, while others only on Tuesdays. So even if your first test was successful, beware.</p><p>I was there, banging my head against the wall. I met every tiny requirement I could find in all the SXG documentation available online. Still, no luck!</p><p>Finally, I began researching this problem on my own. It took some time, but I believe I've identified the causes and the solutions for the most common issues. I&#8217;ll dive deeper into this topic in <a href="https://www.pawelpokrywka.com/p/understanding-cors-sxg-errors">the next part</a>. Spoiler alert: CORS is not to blame.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;f6c05cb4-a064-4fb5-8359-df11e8f70a0f&quot;,&quot;caption&quot;:&quot;In the previous two parts, you learned how to enable Signed Exchanges (SXG) and let Google prefetch your site&#8217;s HTML and assets (or SXG subresources) on the search results page.&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Understanding CORS errors in Signed Exchanges&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:41077879,&quot;name&quot;:&quot;Pawe&#322; Pokrywka&quot;,&quot;bio&quot;:&quot;I write about security, privacy, and complex systems&#8212;exploring them from the messy middle ground between engineering and research.&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1c35edc2-abb7-4761-8eb4-f379f25e519a_800x800.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-01-31T11:44:56.830Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F79cd0b44-bb8e-4099-8eed-64e2a76f0fc6_1536x1053.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.pawelpokrywka.com/p/understanding-cors-sxg-errors&quot;,&quot;section_name&quot;:&quot;Performance&quot;,&quot;video_upload_id&quot;:null,&quot;id&quot;:150454301,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:0,&quot;comment_count&quot;:0,&quot;publication_id&quot;:null,&quot;publication_name&quot;:&quot;Pawe&#322; Pokrywka's Lab&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe44a7fc6-d5ec-4a99-984a-7a7e0bc80a17_800x800.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><h2>Thanks!</h2><p>I hope this post will help you make your website load faster. Thank you for reading&#8212;you&#8217;re the best! :)</p><p>If you enjoyed the post, sharing it with your friends or co-workers is the best way to show your appreciation. I would really be grateful for that!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>Interestingly, the metrics designed to measure page interactivity: <a href="https://web.dev/articles/fid">First Input Delay</a> (FID), and <a href="https://web.dev/articles/inp">Interaction to Next Paint</a> (INP) won&#8217;t improve if the scripts are prefetched.</p><p>In a nutshell, those metrics measure how fast the page reacts to user input (for example a button click). However, until the JavaScript loads, clicking the button doesn&#8217;t invoke any handler and the page doesn&#8217;t change, so that interaction is not measured. The user is frustrated because the button doesn&#8217;t work, but FID and INP are fine with that.</p><p>As a side note, INP measures also CSS transitions and built-in browser behaviors. So technically speaking, the JavaScript doesn&#8217;t need to be loaded for INP measurements of those interactions.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>The data needed to calculate the integrity hash is explained in the definition of the header-integrity parameter from the <a href="https://github.com/WICG/webpackage/blob/main/explainers/signed-exchange-subresource-substitution.md#identifying-exactly-one-version-of-a-signed-exchange">Signed Exchange subresource substitution explainer</a>. I highlighted the important part.</p><p><em>This header-integrity parameter is the SHA256 hash value of the signedHeaders value from the application/signed-exchange format for integrity checking. This signedHeaders is "the canonical serialization of the CBOR representation of the response headers of the exchange represented by the application/signed-exchange resource, excluding the Signature header field". So this value doesn&#8217;t change even if the publisher signs the content again or changes the signing key, but <strong>it does change if any of the headers or body change</strong>. (It catches changes to the body because a valid signed exchange's headers have to include a Digest value that covers the body.)</em></p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>Rails automatically adding the <strong>Link</strong> header actually didn&#8217;t help at first. During my initial SXG experiments, I noticed this conflicted with SXG because the generated <strong>Link</strong> header didn&#8217;t contain the expected integrity hashes. As a result, as far as I remember, the page or the subresources were not prefetched on the Google results page.</p><p>My solution was to use middleware implemented as a Cloudflare worker to strip the<strong> Link</strong>&nbsp;header from HTTP responses to SXG requests. This fixed the issue while preserving Early Hints/standard prefetching for non-SXG requests.</p><p>While working on this post, I wrote <a href="https://www.planujemywesele.pl/sxg-tests/link-header">a test</a> demonstrating the issue. However, it seems no longer there: Cloudflare ASX overwrites the <strong>Link</strong> header set by the app for SXG requests. Either Cloudflare resolved the issue in the meantime, or I was wrong about it from the beginning, and other factors caused my problems.</p></div></div>]]></content:encoded></item><item><title><![CDATA[How I brought LCP down to under 350 ms for Google-referred users on my website]]></title><description><![CDATA[Exploring the techniques I used to optimize performance on a high-traffic website]]></description><link>https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Wed, 08 Jan 2025 17:52:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575b30f4-3dd9-4ba8-8839-84cd2c6054d8_877x640.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dg59!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575b30f4-3dd9-4ba8-8839-84cd2c6054d8_877x640.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset image2-full-screen"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dg59!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575b30f4-3dd9-4ba8-8839-84cd2c6054d8_877x640.jpeg 424w, https://substackcdn.com/image/fetch/$s_!dg59!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575b30f4-3dd9-4ba8-8839-84cd2c6054d8_877x640.jpeg 848w, https://substackcdn.com/image/fetch/$s_!dg59!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575b30f4-3dd9-4ba8-8839-84cd2c6054d8_877x640.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!dg59!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575b30f4-3dd9-4ba8-8839-84cd2c6054d8_877x640.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dg59!,w_5760,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575b30f4-3dd9-4ba8-8839-84cd2c6054d8_877x640.jpeg" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/575b30f4-3dd9-4ba8-8839-84cd2c6054d8_877x640.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;full&quot;,&quot;height&quot;:640,&quot;width&quot;:877,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:255669,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-fullscreen" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dg59!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575b30f4-3dd9-4ba8-8839-84cd2c6054d8_877x640.jpeg 424w, https://substackcdn.com/image/fetch/$s_!dg59!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575b30f4-3dd9-4ba8-8839-84cd2c6054d8_877x640.jpeg 848w, https://substackcdn.com/image/fetch/$s_!dg59!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575b30f4-3dd9-4ba8-8839-84cd2c6054d8_877x640.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!dg59!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F575b30f4-3dd9-4ba8-8839-84cd2c6054d8_877x640.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Improving core web vitals, inspired by <em>The Chariot Race</em>, attributed to Alexander von Wagner, an early version</figcaption></figure></div><p><em>Originally published December 2023, substantially updated January 2025.</em></p><blockquote><p>Context (Oct 2025): After deep work with SXG, I suspected changes might be coming&#8212;just not this quickly. Cloudflare <a href="https://community.cloudflare.com/t/amp-and-signed-exchanges-deprecation-october-20th/831238">announced SXG deprecation</a>, and I observed SXG stop working around Sept 19, 2025.</p><p>If you still want SXG, the current path is to self-host&#8212;but the tooling looks abandoned, and setup is non-trivial. Also, Google may follow Cloudflare by shutting down the SXG cache and removing SXG support in Chrome.</p></blockquote><p>Do most of your users come from Google? You can maximize the performance of your website by using the techniques presented in this and further posts.</p><p>The speed of your website is considered good if the value of the <a href="https://web.dev/articles/lcp">Largest Contentful Paint</a> (LCP) metric is below 2500 ms. I&#8217;m going to show you how I took it below 350 ms on a dynamic, production website with a lot of images, even on a slow connection.</p><p>If you wonder what is the point read <a href="https://web.dev/case-studies/renault">this</a>. Now, take a look at what it feels like (if you prefer, <a href="https://www.youtube.com/watch?v=qnyrWaR-mzk">here</a> is a YouTube version):</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;f63459c4-6ae4-44cb-99d6-8b7e1695d8b1&quot;,&quot;duration&quot;:null}"></div><p>On the recording, you can see I started by clearing Chrome browsing data. This ensures the cache is empty, which resembles a first-time visitor.</p><p>I typed in a search phrase and got results from Google. I downgraded the connection speed and visited the website. Even on slow 3g, it took 140 ms to load the page (LCP). In my observations, the results may vary between 100-350 ms.</p><p>If you need a comparison, perform the above steps for any other website. As of 2025, most will give you 10+ seconds to load.</p><h2>How to achieve LCP below 350ms?</h2><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">This post is just the first part of a series. Subscribe to get notified when I publish the next parts. You won&#8217;t regret it, I promise :)</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>How was it possible to load the entire page including CSS, custom font, and images in a fraction of a second on a slow connection? Have I used the <a href="https://silicon-valley.fandom.com/wiki/Pied_Piper_(algorithm)">Pied Piper compression algorithm</a>?</p><p>If you haven&#8217;t guessed already, I will tell you that the page would behave exactly the same if instead of throttling the connection I cut it entirely off.</p><p>This leaves us with only one possibility. The website HTML and all critical resources such as stylesheets and images have to be <em>prefetched</em> by the browser on the Google results page. After clicking on the link, even when offline, the page can be displayed almost instantly from the browser cache.</p><blockquote><p>Now, you see, the demo wasn't entirely honest. I arranged things so that the preloading hasn&#8217;t been throttled. However, I believe in normal circumstances (when the connection is not extremely slow) the time users spend on Google results is enough for the preload to complete, so the effect on LCP will remain the same as in demonstration video.</p></blockquote><h2>How do I make Google prefetch my website?</h2><p>You need to:</p><ol><li><p>Have a page that frequently appears in Google search results.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a></p></li><li><p>Properly implement Signed Exchanges. This and further posts will focus mainly on that.</p></li></ol><h2>What are Signed Exchanges?</h2><blockquote><p>Signed Exchanges or shortly SXG is being developed by the <a href="https://wicg.io/">Web Incubator Community Group</a> which is a community group of the <a href="https://www.w3.org/">W3C</a> that incubates new web platform features. In the context of IETF standardization, SXG is still a <a href="https://wicg.github.io/webpackage/draft-yasskin-wpack-use-cases.html">constantly updated draft</a>.</p></blockquote><p>Despite it being not 100% finished, <a href="https://caniuse.com/sxg">it works in Chrome-based browsers</a>, there is an <a href="https://github.com/google/nginx-sxg-module">open-source server implementation</a>, and Cloudflare offers it for paying customers.</p><p>Basically, it works as <a href="https://developer.mozilla.org/en-US/docs/Glossary/Prefetch">standard prefetching</a>, but with a twist: the website being prefetched can&#8217;t tell who does it and when. This is important for user privacy and critical on the Google results page because we don&#8217;t want website owners to track our searches.</p><h2>How does SXG work in the context of Google search?</h2><ol><li><p>The Googlebot visits the website. It tells the website it understands the SXG format by setting the appropriate <strong>Accept</strong> HTTP header.</p></li><li><p>The website serves the SXG version of a page instead of a raw HTML. SXG encapsulates HTML and is signed with a website private key.</p></li><li><p>Google saves the SXG to its cache. The cache is located at the webpkgcache.com domain.</p></li><li><p>Later, the user visits the Google website and types the search phrase.</p></li><li><p>Google responds with the results page.</p></li><li><p>The results page instructs the user&#8217;s browser to request the SXG version of the website from Google cache&#8230;</p></li><li><p>&#8230;download it&#8230;</p></li><li><p>&#8230;and store into the browser&#8217;s prefetch cache.</p></li><li><p>The user finally decides to visit the page. There is no need to request the website via the network because the page is prefetched.</p></li><li><p>The browser extracts HTML out of SXG and begins to render the page.</p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Z-IR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90552051-6560-4dc1-a957-9314150e7dee_1920x823.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Z-IR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90552051-6560-4dc1-a957-9314150e7dee_1920x823.png 424w, https://substackcdn.com/image/fetch/$s_!Z-IR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90552051-6560-4dc1-a957-9314150e7dee_1920x823.png 848w, https://substackcdn.com/image/fetch/$s_!Z-IR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90552051-6560-4dc1-a957-9314150e7dee_1920x823.png 1272w, https://substackcdn.com/image/fetch/$s_!Z-IR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90552051-6560-4dc1-a957-9314150e7dee_1920x823.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Z-IR!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90552051-6560-4dc1-a957-9314150e7dee_1920x823.png" width="1200" height="514.2857142857143" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/90552051-6560-4dc1-a957-9314150e7dee_1920x823.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;large&quot;,&quot;height&quot;:624,&quot;width&quot;:1456,&quot;resizeWidth&quot;:1200,&quot;bytes&quot;:87850,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-large" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Z-IR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90552051-6560-4dc1-a957-9314150e7dee_1920x823.png 424w, https://substackcdn.com/image/fetch/$s_!Z-IR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90552051-6560-4dc1-a957-9314150e7dee_1920x823.png 848w, https://substackcdn.com/image/fetch/$s_!Z-IR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90552051-6560-4dc1-a957-9314150e7dee_1920x823.png 1272w, https://substackcdn.com/image/fetch/$s_!Z-IR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F90552051-6560-4dc1-a957-9314150e7dee_1920x823.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Path of the SXG packages from the website to the user&#8217;s browser.</figcaption></figure></div><p>Even if the website is physically loaded from Google, the URL bar in the browser shows the actual website URL. The browser can display it because the SXG is signed, so it can be trusted like the original website.</p><p>There is a downside. Your page has to be cached. If your page changes after being downloaded by Googlebot, the user will see the old version.</p><p>So here&#8217;s the catch, you may think. I agree, that there are use cases that disqualify SXG. But I believe many websites, not only static ones, will work perfectly fine. Mine is the perfect example. And there are ways to minimize the damage of stale content being served.</p><h2>How to generate Signed Exchanges?</h2><p>You can <a href="https://github.com/google/nginx-sxg-module">generate SXG on your own</a> or use a service that does it for you such as Cloudflare<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. I chose the second option because I&#8217;m already a Cloudflare customer.</p><p>Ok, probably the true reason is I&#8217;m lazy and Cloudflare makes turning on SXG a single click (assuming the website is already proxied through Cloudflare).</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OWuW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65a610cd-aaf3-49aa-96ea-9b964a5a9552_2118x1205.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OWuW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65a610cd-aaf3-49aa-96ea-9b964a5a9552_2118x1205.png 424w, https://substackcdn.com/image/fetch/$s_!OWuW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65a610cd-aaf3-49aa-96ea-9b964a5a9552_2118x1205.png 848w, https://substackcdn.com/image/fetch/$s_!OWuW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65a610cd-aaf3-49aa-96ea-9b964a5a9552_2118x1205.png 1272w, https://substackcdn.com/image/fetch/$s_!OWuW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65a610cd-aaf3-49aa-96ea-9b964a5a9552_2118x1205.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OWuW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65a610cd-aaf3-49aa-96ea-9b964a5a9552_2118x1205.png" width="1456" height="828" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/65a610cd-aaf3-49aa-96ea-9b964a5a9552_2118x1205.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:828,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:217254,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!OWuW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65a610cd-aaf3-49aa-96ea-9b964a5a9552_2118x1205.png 424w, https://substackcdn.com/image/fetch/$s_!OWuW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65a610cd-aaf3-49aa-96ea-9b964a5a9552_2118x1205.png 848w, https://substackcdn.com/image/fetch/$s_!OWuW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65a610cd-aaf3-49aa-96ea-9b964a5a9552_2118x1205.png 1272w, https://substackcdn.com/image/fetch/$s_!OWuW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F65a610cd-aaf3-49aa-96ea-9b964a5a9552_2118x1205.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">How to enable SXG in Cloudflare configuration.</figcaption></figure></div><p>As you can see, SXG is a global configuration switch for the entire website. To make it more granular, you can create a <em>Configuration Rule</em> that disables SXG if the URL/subdomain doesn&#8217;t match the part of the website you want SXG to be enabled. This may be handy for testing SXG without impacting the entire site.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xARu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0870227-212d-4c79-a92e-422c583a8782_2168x2586.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xARu!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0870227-212d-4c79-a92e-422c583a8782_2168x2586.png 424w, https://substackcdn.com/image/fetch/$s_!xARu!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0870227-212d-4c79-a92e-422c583a8782_2168x2586.png 848w, https://substackcdn.com/image/fetch/$s_!xARu!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0870227-212d-4c79-a92e-422c583a8782_2168x2586.png 1272w, https://substackcdn.com/image/fetch/$s_!xARu!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0870227-212d-4c79-a92e-422c583a8782_2168x2586.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xARu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0870227-212d-4c79-a92e-422c583a8782_2168x2586.png" width="1456" height="1737" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e0870227-212d-4c79-a92e-422c583a8782_2168x2586.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1737,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:289988,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xARu!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0870227-212d-4c79-a92e-422c583a8782_2168x2586.png 424w, https://substackcdn.com/image/fetch/$s_!xARu!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0870227-212d-4c79-a92e-422c583a8782_2168x2586.png 848w, https://substackcdn.com/image/fetch/$s_!xARu!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0870227-212d-4c79-a92e-422c583a8782_2168x2586.png 1272w, https://substackcdn.com/image/fetch/$s_!xARu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe0870227-212d-4c79-a92e-422c583a8782_2168x2586.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">How to enable SXG only on a specific hostname or on a specific path.</figcaption></figure></div><h2>Is that it?</h2><p>This post wouldn&#8217;t make sense if all I needed to do was enable SXG in the Cloudflare panel. There were certain decisions I needed to make, and requirements my website had to meet.</p><p>This and upcoming posts will show you my path of making the existing website to become fully SXG-enabled. I will include a lot of details and workarounds for various quirks I discovered during hours of debugging and black-box testing.</p><p>I was striving to write in a way that is understandable for a web developer interested in performance optimization. However, this text focuses on practical implementation. Therefore, most discussions and code examples use Ruby on Rails and Next.js, the frameworks used by my website. If you are not interested, you can skip those parts, they are clearly marked.</p><p>Without further ado, let&#8217;s get started!</p><h2>Content generation vs content distribution</h2><p>First, I had to give up the idea that I had 100% control over my website traffic. From now on, the pages generated by my server will be distributed to the users by the infrastructure I can control only partially if at all.</p><p>Google supports <a href="https://github.com/google/webpackager/blob/main/docs/update_cache_api.md">SXG cache purging</a>, but it is asynchronous, from my observations slow and you can&#8217;t purge more than one, specific URL at once. Compare it to Cloudflare where you can <a href="https://developers.cloudflare.com/cache/how-to/purge-cache/">purge cache</a> for example by URL prefix<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a>, or even purge the entire cache in one step. You can achieve similar results with <a href="https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_purge">the nginx cache</a><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a>.</p><p>And nothing prevents someone from downloading the SXG version of my website and hosting it anywhere. When the browser visits it, it will still behave exactly, as it was on my website. And I won&#8217;t see the request coming to my server.</p><p>The HTTP request becomes decoupled from the HTTP response which has a life of its own. Just like a mobile app - after the user installs it, you lose control over it.</p><p>I will be able to see only a subset of requests from users, the rest will be handled by an opaque cache hosted by Google. My logs won&#8217;t reflect all the traffic my website gets. So no server-side page view counting and other request-based analytics.</p><p>The only thing I can control is the amount of time the SXG version of a given URL is valid.</p><h2>No server-side personalization</h2><p>Given the above, the HTML returned for a given URL should be the same for all visitors, no matter what&#8217;s included in the HTTP request<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-5" href="#footnote-5" target="_self">5</a>. I needed to serve the same page regardless of the login status of the user, the cookies<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-6" href="#footnote-6" target="_self">6</a>, device and browser, the country the user came from, and the language he/she had set.</p><p>This becomes a problem because the user expects the page to look different depending on the above variables.</p><p>The obvious solution is to customize the page client-side, in the browser. Javascript has to query API endpoints, maintain state using browser storage, and adjust the page. CSS has to be used for responsiveness, as serving separate versions of the page depending on the <strong>User-Agent</strong> header or redirecting mobile users to a subdomain is not an option.</p><h2>Caching</h2><p>The basic rule of making the app SXG-friendly is to make it cache-friendly because the SXG version of the page will eventually land in Google SXG cache.</p><p>To be cacheable, it is best if the HTTP response includes a <strong>Cache-Control</strong> header.</p><h4>A note on Cloudflare cache</h4><p>By default, Cloudflare caches static assets but <a href="https://developers.cloudflare.com/cache/concepts/default-cache-behavior/#default-cached-file-extensions">doesn&#8217;t cache HTML responses</a>, even if they set the <strong>Cache-Control</strong> header to allow that.</p><p><strong>SXG will work perfectly fine without changing this configuration</strong>. Actually, it&#8217;s easier to implement SXG <strong>without</strong> caching HTML in the Cloudflare cache because you don&#8217;t need to worry about accidentally caching a private user&#8217;s state.</p><p>However, I decided to utilize Cloudflare cache for HTML to maximize the performance benefits. The rest of the steps in this and the following posts assume the HTML will be cached this way.</p><h4>Cache configuration</h4><p>The <strong>Cache-Control</strong> header, among other things, allows you to set the maximum amount of time the page is considered fresh and can be kept in the cache. Particularly <strong>max-age</strong> and <strong>s-maxage</strong> directives can be used for that. Here is an example:</p><pre><code>Cache-Control: public, max-age=600, s-maxage=3600</code></pre><p>It tells the browser to cache the page for 600 seconds, or 10 minutes (<strong>max-age</strong> directive).</p><p>It also tells so-called shared caches (for instance Cloudflare cache or Google cache) to cache the response for 3600 seconds, or 1 hour (<strong>s-maxage</strong> directive). In the absence of <strong>s-maxage</strong>, shared caches would use <strong>max-age</strong>.</p><blockquote><p>Note the difference in spelling: <strong>max-age</strong> vs <strong>s-maxage</strong>. I experienced quite a bit of frustration while debugging why <s>s-max-age</s> doesn&#8217;t work!</p><p><em>Helpful tip: Both directives are written with exactly one hyphen.</em></p></blockquote><p>You will find a detailed explanation of all <strong>Cache-Control</strong> directives <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control">here</a>.</p><p>Google won&#8217;t cache SXG if <strong>max-age/s-maxage</strong> is less than <a href="https://github.com/google/webpackager/blob/main/docs/cache_requirements.md">120 seconds</a>. You can encounter the following warning in <strong>SXG Validator</strong> (debugging tool described later) or directly in the HTTP response generated by SXG cache:</p><pre><code>199 - "debug: content has ingestion error: Error fetching resource: Content is not cache-able or cache-able lifetime is too short"</code></pre><p>The solution is to increase the value of the&nbsp;<strong>max-age/s-max-age</strong>&nbsp;directive within the&nbsp;<strong>Cache-Control</strong>&nbsp;HTTP header to something higher than 120 seconds (in real life,&nbsp;<em>much</em>&nbsp;higher; otherwise, the cached version will hardly be used).</p><p>Apart from that, you <a href="https://developers.cloudflare.com/speed/optimization/other/signed-exchanges/signed-exchanges-caveats/">can&#8217;t use </a><strong><a href="https://developers.cloudflare.com/speed/optimization/other/signed-exchanges/signed-exchanges-caveats/">private</a></strong><a href="https://developers.cloudflare.com/speed/optimization/other/signed-exchanges/signed-exchanges-caveats/">, </a><strong><a href="https://developers.cloudflare.com/speed/optimization/other/signed-exchanges/signed-exchanges-caveats/">no-store</a></strong><a href="https://developers.cloudflare.com/speed/optimization/other/signed-exchanges/signed-exchanges-caveats/">, and </a><strong><a href="https://developers.cloudflare.com/speed/optimization/other/signed-exchanges/signed-exchanges-caveats/">no-cache</a></strong><a href="https://developers.cloudflare.com/speed/optimization/other/signed-exchanges/signed-exchanges-caveats/"> directives</a>, otherwise, SXG won&#8217;t be generated by Cloudflare. Cloudflare SXG generator doesn&#8217;t allow you to set the maximum age to a value higher than 7 days<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-7" href="#footnote-7" target="_self">7</a>.</p><blockquote><p>The default <strong>Cache-Control</strong> header set by Rails, Next.js, and other frameworks typically prevents HTML caching, which consequently disables SXG. This is actually beneficial, as it lets you selectively enable SXG/caching for specific sections of your application while maintaining safety for the rest of your codebase.</p></blockquote><h4>Choosing cache expiration times</h4><p>I had to decide how long to cache HTML pages. It was tough. Naturally, we all want to get the freshest data possible. But we also want to get it immediately. Those two needs are opposite to each other.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1sWV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5266ef9-190f-4ae6-a1b7-5f00371d0be4_2415x2924.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1sWV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5266ef9-190f-4ae6-a1b7-5f00371d0be4_2415x2924.jpeg 424w, https://substackcdn.com/image/fetch/$s_!1sWV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5266ef9-190f-4ae6-a1b7-5f00371d0be4_2415x2924.jpeg 848w, https://substackcdn.com/image/fetch/$s_!1sWV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5266ef9-190f-4ae6-a1b7-5f00371d0be4_2415x2924.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!1sWV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5266ef9-190f-4ae6-a1b7-5f00371d0be4_2415x2924.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1sWV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5266ef9-190f-4ae6-a1b7-5f00371d0be4_2415x2924.jpeg" width="1456" height="1763" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e5266ef9-190f-4ae6-a1b7-5f00371d0be4_2415x2924.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1763,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2216176,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!1sWV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5266ef9-190f-4ae6-a1b7-5f00371d0be4_2415x2924.jpeg 424w, https://substackcdn.com/image/fetch/$s_!1sWV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5266ef9-190f-4ae6-a1b7-5f00371d0be4_2415x2924.jpeg 848w, https://substackcdn.com/image/fetch/$s_!1sWV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5266ef9-190f-4ae6-a1b7-5f00371d0be4_2415x2924.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!1sWV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe5266ef9-190f-4ae6-a1b7-5f00371d0be4_2415x2924.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Freshness vs performance, inspired by <em>The Combat of the Giaour and Hassen</em> (1835) by Eug&#232;ne Delacroix, oil on canvas 73 &#215; 61 cm</figcaption></figure></div><p>The content on my website doesn&#8217;t change very often, and I&#8217;ve observed that Google typically refreshes its cache more frequently than the <strong>Cache-Control</strong> header value suggests.</p><p>After carefully considering all business requirements, I decided to set the cache duration to 24 hours. This value is also recommended in <a href="https://developer.chrome.com/blog/optimizing-lcp-using-signed-exchanges/">a post</a> by Devin Mullins, one of the SXG implementers at Google, and it strikes a good balance between minimizing staleness and optimizing performance.</p><p>As a result, I configured the <strong>s-maxage</strong> to 1 day.</p><blockquote><p>There are advanced ways to further minimize users&#8217; exposure to stale data, especially when utilizing Cloudflare cache. However these methods are highly application-specific. Sharing them publicly would likely benefit only competing websites, so I&#8217;ve chosen to keep them private.</p></blockquote><h4>Caching in Rails</h4><p>I added the following code in every Rails action I wanted to enable for SXG:</p><pre><code>response.headers['Cache-Control'] =
  "s-maxage=#{24.hours.to_i}, public"</code></pre><h4>Caching in Next.js</h4><p>To set the required header in Next.js I used the <a href="https://nextjs.org/docs/pages/api-reference/functions/get-server-side-props#context-parameter">context</a> parameter passed by the framework to the <strong>getServerSideProps()</strong>:</p><pre><code>context.res.setHeader(
  "Cache-Control", `s-maxage=${24 * 60 * 60}, public`
);</code></pre><p>Keep in mind that Next.js will override the <strong>Cache-Control</strong> header value in development. The changes can be observed only in production.</p><h2>HTTP headers to avoid</h2><p>Cloudflare provides a full <a href="https://developers.cloudflare.com/speed/optimization/other/signed-exchanges/signed-exchanges-caveats">list of SXG requirements</a>. The important part is a list of disallowed response HTTP headers, such as <strong>Set-Cookie</strong>. If your app responds with one of those, SXG won&#8217;t be generated for a given page. The intention is to prevent making potentially private information public.</p><blockquote><p>I was unable to provide a real-world example of vulnerability prevented by this safeguard. That&#8217;s because of additional safety measures coming in.</p><p>This is the only thing that came to my mind. Let&#8217;s assume <strong>Set-Cookie</strong> is allowed when generating SXG. Imagine the following scenario:</p><ol><li><p>User submits login information, the page reloads and sets the session cookie.</p></li><li><p>The page gets in Cloudflare cache along with <strong>Set-Cookie</strong> header. From now on, all users visiting this page get immediately logged as the user from previous step. <strong>That&#8217;s already a disaster.</strong></p></li><li><p>Now Googlebot fetches the SXG version of this cached page (including cookies) and publishes it in its SXG cache. Everyone can download it and extract session cookies of the unfortunate user. It&#8217;s possible even after fixing website and purging Cloudflare cache.</p></li></ol><p>Of course, the correct way of mitigating this vulnerability is to invalidate all session cookies after fixing the page. But the point is Cloudflare tries to limit the damage by stopping the private information from leaking further.</p><p>Fortunately, the above scenario won&#8217;t happen in real-life. That&#8217;s because of additional Cloudflare safeguard that <a href="https://developers.cloudflare.com/cache/concepts/default-cache-behavior/">excludes pages with </a><strong><a href="https://developers.cloudflare.com/cache/concepts/default-cache-behavior/">Set-Cookie</a></strong><a href="https://developers.cloudflare.com/cache/concepts/default-cache-behavior/"> header from being cached</a>. I suspect Google also won&#8217;t cache SXG with cookies, but I haven&#8217;t verified that.</p><p>If you have a real-world example of the vulnerability Cloudflare prevents with this measure, leave a comment below.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms/comments&quot;,&quot;text&quot;:&quot;Leave a comment&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.pawelpokrywka.com/p/how-i-took-lcp-down-under-350ms/comments"><span>Leave a comment</span></a></p></blockquote><p>As most websites use cookies, you may think it disqualifies SXG usage. I strongly believe that&#8217;s not the case.</p><p>You can use Google Analytics and other cookies set by the browser. Only cookies returned with server-side generated HTML pages are prohibited. And there are ways to handle those cases too.</p><h2>Session cookie</h2><p>When you generate a page server-side, typically a session cookie is set. This depends on the framework you use, so you need to check by yourself. For example, Next.js doesn&#8217;t use session cookies while they are used extensively in the Rails.</p><p>As mentioned above, SXG won&#8217;t be generated when your response contains cookies. Therefore I needed to adjust my Rails app.</p><p>In every SXG-enabled controller action, I needed to make sure the Rails <strong>session</strong> was not being used. That&#8217;s because loading it automatically sets a cookie in the response. Searching the code for the &#8220;session&#8221; string allowed me to pinpoint all the places I needed to update.</p><h2>CSRF protection</h2><p>In the <a href="https://owasp.org/www-community/attacks/csrf">Cross-Site Request Forgery</a> (CSRF) the attacker tricks the user into performing certain actions in the vulnerable web application.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ldJs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6600d1-eb23-4098-afc0-217a1169cce3_2560x1653.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ldJs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6600d1-eb23-4098-afc0-217a1169cce3_2560x1653.jpeg 424w, https://substackcdn.com/image/fetch/$s_!ldJs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6600d1-eb23-4098-afc0-217a1169cce3_2560x1653.jpeg 848w, https://substackcdn.com/image/fetch/$s_!ldJs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6600d1-eb23-4098-afc0-217a1169cce3_2560x1653.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!ldJs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6600d1-eb23-4098-afc0-217a1169cce3_2560x1653.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ldJs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6600d1-eb23-4098-afc0-217a1169cce3_2560x1653.jpeg" width="1456" height="940" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3b6600d1-eb23-4098-afc0-217a1169cce3_2560x1653.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:940,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2718567,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ldJs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6600d1-eb23-4098-afc0-217a1169cce3_2560x1653.jpeg 424w, https://substackcdn.com/image/fetch/$s_!ldJs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6600d1-eb23-4098-afc0-217a1169cce3_2560x1653.jpeg 848w, https://substackcdn.com/image/fetch/$s_!ldJs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6600d1-eb23-4098-afc0-217a1169cce3_2560x1653.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!ldJs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3b6600d1-eb23-4098-afc0-217a1169cce3_2560x1653.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">CSRF visualization, inspired by <em>The garden of Eden with the fall of man</em> (~1615) by Jan Brueghel the Elder and Peter Paul Rubens, oil on panel, 74.3 &#215; 114.7 cm, Mauritshuis, The Hague</figcaption></figure></div><p>Modern frameworks using cookie-based authentication implement appropriate protections. In some cases, those mechanisms may prevent SXG from being generated. Adjustments may be necessary.</p><blockquote><h4>Warning</h4><p>Interfering with security mechanisms should be done with great care. Developer doing so should be familiar with computer security, and understand CSRF attack and prevention methods.</p><p>Flawed implementation may lead to <a href="https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CSRF">serious vulnerabilities</a>.</p><p>On the other hand, come on, it's not rocket science! Understanding CSRF is fairly easy; you don't need a PhD.</p></blockquote><h2>Rails CSRF approach</h2><blockquote><p>If you use a different framework, you can safely skip this and the following section.</p></blockquote><p>Rails uses unique, session-bound CSRF tokens which are embedded into HTML and later sent in every unsafe request (using methods other than GET or HEAD), such as form submission or AJAX. After being received by the server, tokens are checked against the user session, and requests containing invalid tokens are rejected.</p><p>This mechanism is very effective but doesn&#8217;t work well with caching. The cached page would include CSRF tokens. They were valid for the user visiting the page before it was cached. But for every subsequent user, those tokens will be invalid because their sessions don&#8217;t match those tokens. Users won&#8217;t be able to perform any operations, as all their requests will be rejected.</p><h2>How to make Rails CSRF protection cacheable</h2><p>I fixed it by transmitting the CSRF token as a cookie using a separate API endpoint that&#8217;s not cached. The front end performs an API request and uses a CSRF token from the cookie to <em>patch</em> the HTML of the current page.</p><p>Before proceeding, CSRF tokens must be removed from the HTML. This is necessary for two reasons:</p><ol><li><p>It makes the HTML cleaner.</p></li><li><p>More importantly, CSRF tokens are tied to sessions. Their presence triggers session loading, which in turn sets cookies&#8212;exactly what we're trying to avoid.</p></li></ol><p>All the <a href="https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html">form tags</a> used on SXG-enabled pages have to include the <strong>authenticity_token</strong> option set to an empty string. This way forms will include empty tags, which could be updated later by the front end.</p><pre><code>&lt;%= form_with model: article, <strong>authenticity_token: ''</strong> do |form| %&gt;
  ...
&lt;% end %&gt;</code></pre><p>The other place containing the CSRF token is the <a href="https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/CsrfHelper.html#method-i-csrf_meta_tags">csrf_meta_tags</a> helper in the layout. It is used by Rails javascript to add an <strong>X-Csrf-Token</strong> HTTP header to AJAX requests. It should be replaced with static HTML to be updated later:</p><pre><code>&lt;meta name="csrf-param" content="authenticity_token" /&gt;
&lt;meta name="csrf-token" content /&gt;</code></pre><p>The API controller simply sets the cookie and returns a 200 status code:</p><pre><code># app/controllers/api/cookies_controller.rb
module Api
  class CookiesController &lt; ActionController::API
    include ActionController::Cookies
    include ActionController::RequestForgeryProtection

    def index
      # Calling #form_authenticity_token loads the session
      # automatically. So apart from the csrf_token cookie,
      # the response will also include the session cookie.
      cookies[:csrf_token] = form_authenticity_token
      head :ok
    end
  end
end</code></pre><p>To make it accessible, the following route should be added to <strong>config/routes.rb</strong>:</p><pre><code><code>namespace :api do
  resources :cookies
end</code></code></pre><p>Then some javascript needs to be executed in the front end:</p><pre><code>fetch('/api/cookies').then(() =&gt; {
  const cookie = document.cookie.split('; ')
    .find(row =&gt; row.startsWith('csrf_token=')) || '=';
  const token = decodeURIComponent(cookie.split('=')[1]);
  document.querySelector(
    'meta[name="csrf-token"]'
  ).setAttribute('content', token);
  document.querySelectorAll(
    'input[name="authenticity_token"]'
  ).forEach(input =&gt; { input.value = token; });
})</code></pre><p>The above implementation is simplified for clarity. The production-grade javascript could be optimized to perform the request only once in a while, not on every page load. Also, it should gracefully handle cases where the CSRF token may become obsolete, such as login and logout.</p><h2>Currently logged user</h2><p>The common pattern in server-side rendered web applications is to keep the ID of the currently logged user in the session. This way user information may be fetched from the database and included in the response. In the context of caching, this approach has 2 problems:</p><ol><li><p>The resulting HTML is different for every user.</p></li><li><p>The session is used, therefore the response sets a cookie.</p></li></ol><p>The solution is to use JavaScript to perform an API request to the endpoint that returns current user information and then update the page client-side.</p><h2>First impression optimization</h2><p>One of the decisions I had to make was to optimize for the first-time user. The first impression he/she gets greatly contributes to him/her staying on the page or leaving.</p><p>That&#8217;s why all the pages of my website use server-side rendering. This way user gets the page quicker compared to rendering it client-side.</p><p>When the user is logged in, the section in the screen's upper-right corner on the desktop shows his/her name and profile picture. Otherwise, the user section is replaced with a &#8220;Login / Register&#8221; link.</p><p>Initially, the above space remains empty and only after the user's state is known, it is filled. On a slow connection, it may take a while to perform the API request which slows down the appearance of the user section.</p><p>That&#8217;s why I assume a first-time user is not logged in and the front end doesn&#8217;t need to perform the above API request at all. This way &#8220;Login / Register&#8220; could be displayed immediately.</p><p>After login, browser storage is used to mark the user as logged. This way, the browser can quickly check if performing an API request during the next page load is worth it.</p><blockquote><p>You may think it&#8217;s just a detail. But there is also SEO aspect of this approach.</p><p>Most crawlers behave as first-time users for every request they make. That&#8217;s probably because they don&#8217;t keep state between requests, especially don&#8217;t store cookies.</p><p>In the presented case we saved 1 non-cache&#8217;able request per page, which quickly adds up given number of pages crawlers visits. Performant page saves bot&#8217;s <a href="https://ahrefs.com/blog/crawl-budget/">crawling budget</a>, which could be spent on crawling more pages.</p></blockquote><h2>Other cookies</h2><p>I also had to make sure no other cookies were being set server-side. It was easy to search for all the places in the code setting a cookie. The hard part was to decide how to avoid using it. There were two options: make the front end responsible or remove given cookie usage entirely by solving the problem differently.</p><p>The final check is to use Chrome Developer Tools, preferably with cleared cookies or using incognito mode to simulate Googlebot and examine the app response. If the <strong>Set-Cookie</strong> header shows up, you need to go back to code.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!B9Zq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8327e339-979d-48d9-8401-37f2172c7476_3832x2016.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!B9Zq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8327e339-979d-48d9-8401-37f2172c7476_3832x2016.png 424w, https://substackcdn.com/image/fetch/$s_!B9Zq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8327e339-979d-48d9-8401-37f2172c7476_3832x2016.png 848w, https://substackcdn.com/image/fetch/$s_!B9Zq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8327e339-979d-48d9-8401-37f2172c7476_3832x2016.png 1272w, https://substackcdn.com/image/fetch/$s_!B9Zq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8327e339-979d-48d9-8401-37f2172c7476_3832x2016.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!B9Zq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8327e339-979d-48d9-8401-37f2172c7476_3832x2016.png" width="1456" height="766" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8327e339-979d-48d9-8401-37f2172c7476_3832x2016.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:766,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1778694,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!B9Zq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8327e339-979d-48d9-8401-37f2172c7476_3832x2016.png 424w, https://substackcdn.com/image/fetch/$s_!B9Zq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8327e339-979d-48d9-8401-37f2172c7476_3832x2016.png 848w, https://substackcdn.com/image/fetch/$s_!B9Zq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8327e339-979d-48d9-8401-37f2172c7476_3832x2016.png 1272w, https://substackcdn.com/image/fetch/$s_!B9Zq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8327e339-979d-48d9-8401-37f2172c7476_3832x2016.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Chrome Developer Tools showing cookies being set by the app.</figcaption></figure></div><h2>HSTS</h2><p><a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security">HTTP Strict Transport Security (HSTS)</a> is a security policy ensuring browsers will always use a secure connection when connecting to your website.</p><p>Years ago, I enabled it in the nginx configuration. The web server was setting the <strong>Strict-Transport-Security</strong> HTTP header on all responses. By default, Rails sets this header in production too.</p><p>Unfortunately, the <strong>Strict-Transport-Security</strong> header prevents the SXG from being generated. I had to revert the changes I made to the Nginx configuration. In Rails case, the only change needed was to set <a href="https://guides.rubyonrails.org/configuring.html#config-force-ssl">config.force_ssl</a> to <strong>false</strong>.</p><blockquote><p>If you use a different stack, you may need to check the documentation of your web server and framework to find a way to disable HSTS.</p></blockquote><p>Fortunately, HSTS can still be used by setting a flag in Cloudflare configuration.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!v1_J!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cedcf68-b3b2-4481-8575-aadf25966d99_2144x1016.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!v1_J!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cedcf68-b3b2-4481-8575-aadf25966d99_2144x1016.png 424w, https://substackcdn.com/image/fetch/$s_!v1_J!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cedcf68-b3b2-4481-8575-aadf25966d99_2144x1016.png 848w, https://substackcdn.com/image/fetch/$s_!v1_J!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cedcf68-b3b2-4481-8575-aadf25966d99_2144x1016.png 1272w, https://substackcdn.com/image/fetch/$s_!v1_J!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cedcf68-b3b2-4481-8575-aadf25966d99_2144x1016.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!v1_J!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cedcf68-b3b2-4481-8575-aadf25966d99_2144x1016.png" width="1456" height="690" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7cedcf68-b3b2-4481-8575-aadf25966d99_2144x1016.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:690,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:126027,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!v1_J!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cedcf68-b3b2-4481-8575-aadf25966d99_2144x1016.png 424w, https://substackcdn.com/image/fetch/$s_!v1_J!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cedcf68-b3b2-4481-8575-aadf25966d99_2144x1016.png 848w, https://substackcdn.com/image/fetch/$s_!v1_J!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cedcf68-b3b2-4481-8575-aadf25966d99_2144x1016.png 1272w, https://substackcdn.com/image/fetch/$s_!v1_J!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cedcf68-b3b2-4481-8575-aadf25966d99_2144x1016.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">How to enable HSTS in Cloudflare.</figcaption></figure></div><p>It sets the HSTS header, but in a SXG-compatible way, so that:</p><ul><li><p>the standard response includes the header,</p></li><li><p>the SXG response includes it too,</p></li><li><p>the standard response encapsulated in SXG <strong>does not include it</strong>.</p></li></ul><h2>Writing tests</h2><p>It&#8217;s always good to have tests, and in this particular case, it was especially important. That&#8217;s because it is very easy to make a code change that breaks SXG, and the effects will be invisible in development and hard to spot in production.</p><p>During future development, I expect things like adding new cookies and accessing the session directly or indirectly would be the most common causes of SXG breakage.</p><p>That&#8217;s why I implemented tests checking the responses for required/prohibited headers according to the <a href="https://developers.cloudflare.com/speed/optimization/other/signed-exchanges/signed-exchanges-caveats/">Cloudflare specification</a>. Here is a spec for the Rails app:</p><pre><code>require 'rails_helper'

RSpec.describe ReplaceWithYourController, type: :request do
  before(:context) do
    # Add app-specific initialization logic before making a request
    get "/replace-with-your-sxg-enabled-page"
  end

  let :cache_control_headers do
    %w[
       Cache-Control Cdn-Cache-Control
       Cloudflare-Cdn-Cache-Control Surrogate-Control
      ]
  end

  let :forbidden_headers do
    %w[
        Authentication-Control Authentication-Info Clear-Site-Data
        Optional-WWW-Authenticate Proxy-Authenticate WWW-Authenticate
        Proxy-Authentication-Info Public-Key-Pins Sec-WebSocket-Accept
        Set-Cookie Set-Cookie2 SetProfile Strict-Transport-Security
      ]
  end

  let :forbidden_directives do
    %w[private no-store no-cache max-age=0]
  end

  it 'does not use forbidden HTTP headers' do
    expect(headers).not_to include(*forbidden_headers)
  end

  it 'does not use forbidden cache directives' do
    cache_control_headers.each do |header|
      next unless headers[header]
      expect(headers[header]).not_to include(*forbidden_directives)
    end
  end
end</code></pre><p>In addition, I created tests for application-specific requirements, for example, if <strong>s-maxage</strong> is being set to the correct value.</p><h2>Caching the HTML</h2><p>At this point, the application was ready for Cloudflare to begin caching its HTML content.</p><p>The easiest way to accomplish that is to define the cache rule. To do that, go to <strong>Caching</strong> &#8594; <strong>Cache Rules</strong> and hit the <strong>Create rule</strong> button. Then provide a name for the rule, choose <strong>All incoming request</strong>s, and <strong>Eligible for cache</strong>. The form contains a lot of other settings, they don&#8217;t need to be changed. I removed them for readability in the screenshot below:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!MRQm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F092b54fb-af1e-4fe2-8d51-dac35c754665_2126x1660.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MRQm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F092b54fb-af1e-4fe2-8d51-dac35c754665_2126x1660.png 424w, https://substackcdn.com/image/fetch/$s_!MRQm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F092b54fb-af1e-4fe2-8d51-dac35c754665_2126x1660.png 848w, https://substackcdn.com/image/fetch/$s_!MRQm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F092b54fb-af1e-4fe2-8d51-dac35c754665_2126x1660.png 1272w, https://substackcdn.com/image/fetch/$s_!MRQm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F092b54fb-af1e-4fe2-8d51-dac35c754665_2126x1660.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MRQm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F092b54fb-af1e-4fe2-8d51-dac35c754665_2126x1660.png" width="1456" height="1137" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/092b54fb-af1e-4fe2-8d51-dac35c754665_2126x1660.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1137,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:188107,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!MRQm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F092b54fb-af1e-4fe2-8d51-dac35c754665_2126x1660.png 424w, https://substackcdn.com/image/fetch/$s_!MRQm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F092b54fb-af1e-4fe2-8d51-dac35c754665_2126x1660.png 848w, https://substackcdn.com/image/fetch/$s_!MRQm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F092b54fb-af1e-4fe2-8d51-dac35c754665_2126x1660.png 1272w, https://substackcdn.com/image/fetch/$s_!MRQm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F092b54fb-af1e-4fe2-8d51-dac35c754665_2126x1660.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Adding a cache rule for site-wide HTML caching in the Claudflare panel. Some parts of the form removed for readability.</figcaption></figure></div><p>&#9888;&#65039; <strong>WARNING: This cache rule enables HTML caching site-wide.</strong> To test safely, start by applying it to a limited set of URLs using a <strong>custom filter expression</strong>. This allows you to verify everything functions as expected before broader deployment.</p><p>When you feel comfortable with the rule, hit the <strong>Deploy</strong> button to activate it.</p><h2>Testing on production (!)</h2><p>After implementing the above changes and making sure tests passed, I was ready for the production deployment to validate if Cloudflare generates SXG correctly.</p><p><em>Well, actually it wasn&#8217;t that simple.</em></p><p>This topic was new to me, there were many unknowns and a lot of learning and experimentation in the process. Also, at the same time I was implementing other performance optimization features. It wasn&#8217;t a linear process, like described here. I needed to deploy often and test - on production.</p><p>I decided to do this because the risk of something going wrong was minimal. The worst that could happen was Cloudflare failing to generate SXG. And this was my starting point - not a big deal.</p><p>When testing SXG I used the following tools and techniques:</p><h4>SXG Validator</h4><p><a href="https://chromewebstore.google.com/detail/sxg-validator/hiijcdgcphjeljafieaejfhodfbpmgoe?pli=1">This Chrome extension</a> allows you to quickly check if the current page has an SXG version available. Also, it triggers Googlebot to fetch the page and put it into the Google SXG cache if it&#8217;s not already there. It then tells you the status of the page in the cache and displays errors if Google for some reason rejected the page.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Zm6w!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a7c713-53e3-484b-a86e-9a4a98bbf699_1260x1006.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Zm6w!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a7c713-53e3-484b-a86e-9a4a98bbf699_1260x1006.png 424w, https://substackcdn.com/image/fetch/$s_!Zm6w!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a7c713-53e3-484b-a86e-9a4a98bbf699_1260x1006.png 848w, https://substackcdn.com/image/fetch/$s_!Zm6w!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a7c713-53e3-484b-a86e-9a4a98bbf699_1260x1006.png 1272w, https://substackcdn.com/image/fetch/$s_!Zm6w!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a7c713-53e3-484b-a86e-9a4a98bbf699_1260x1006.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Zm6w!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a7c713-53e3-484b-a86e-9a4a98bbf699_1260x1006.png" width="1260" height="1006" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c8a7c713-53e3-484b-a86e-9a4a98bbf699_1260x1006.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1006,&quot;width&quot;:1260,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:257881,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Zm6w!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a7c713-53e3-484b-a86e-9a4a98bbf699_1260x1006.png 424w, https://substackcdn.com/image/fetch/$s_!Zm6w!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a7c713-53e3-484b-a86e-9a4a98bbf699_1260x1006.png 848w, https://substackcdn.com/image/fetch/$s_!Zm6w!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a7c713-53e3-484b-a86e-9a4a98bbf699_1260x1006.png 1272w, https://substackcdn.com/image/fetch/$s_!Zm6w!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a7c713-53e3-484b-a86e-9a4a98bbf699_1260x1006.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">SXG Validator showing the origin responded with SXG and the response is cached along with a valid certificate.</figcaption></figure></div><blockquote><p>As I update this post, the extension's metrics have grown slightly: from 837 users and 1 review in December 2023 to 1,000 users (though still with just 1 review).</p><p>It tells a lot about SXG's popularity on the web. But fear not! Most websites don&#8217;t use SXG, but it doesn&#8217;t mean the tech is to blame. The tech is solid, and the websites using it outperform others.</p></blockquote><h4>SXG prefetch page</h4><p><a href="https://signed-exchange-testing.dev/prefetch/">A simple page</a> that prefetches a specified URL the same way Google does. Very useful, because you don&#8217;t need to wait until your page is indexed and presented in search results, on high positions.</p><p>When combined with the Network tab in Chrome Developer Tools, you can easily see exactly what HTTP requests are being performed after prefetching is triggered. To see only the SXG traffic, use the <strong>Other</strong> filter.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3HW2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34be4d0f-b864-42b2-89aa-a204a21bed1e_2999x1783.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3HW2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34be4d0f-b864-42b2-89aa-a204a21bed1e_2999x1783.png 424w, https://substackcdn.com/image/fetch/$s_!3HW2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34be4d0f-b864-42b2-89aa-a204a21bed1e_2999x1783.png 848w, https://substackcdn.com/image/fetch/$s_!3HW2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34be4d0f-b864-42b2-89aa-a204a21bed1e_2999x1783.png 1272w, https://substackcdn.com/image/fetch/$s_!3HW2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34be4d0f-b864-42b2-89aa-a204a21bed1e_2999x1783.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3HW2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34be4d0f-b864-42b2-89aa-a204a21bed1e_2999x1783.png" width="1456" height="866" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/34be4d0f-b864-42b2-89aa-a204a21bed1e_2999x1783.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:866,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:536520,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!3HW2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34be4d0f-b864-42b2-89aa-a204a21bed1e_2999x1783.png 424w, https://substackcdn.com/image/fetch/$s_!3HW2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34be4d0f-b864-42b2-89aa-a204a21bed1e_2999x1783.png 848w, https://substackcdn.com/image/fetch/$s_!3HW2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34be4d0f-b864-42b2-89aa-a204a21bed1e_2999x1783.png 1272w, https://substackcdn.com/image/fetch/$s_!3HW2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34be4d0f-b864-42b2-89aa-a204a21bed1e_2999x1783.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">SXG prefetch page with Chrome Developer Tools opened with the <strong>Other</strong> filter. In the Preview tab, you can see the decoded SXG response.</figcaption></figure></div><h4>Curl</h4><p>This well-known <a href="https://curl.se/">tool</a> allows you to see the raw, HTTP-wrapped SXG response. Just specify the appropriate <strong>Accept</strong> HTTP header:</p><pre><code>$ curl -siH "Accept: application/signed-exchange;v=b3" \
    https://www.planujemywesele.pl/zespoly/opole | less

HTTP/2 200
date: Wed, 06 Dec 2023 18:55:40 GMT
content-type: application/signed-exchange;v=b3
content-length: 212278
vary: accept,amp-cache-transform
x-content-type-options: nosniff
report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=BE7oFSLfFhdPe1PYy6NGP%2BVvj9qeh4ZekE30ydqjTrxITTTxbDkzWHPm3LfvackhcPx0jE3XnWOreho%2Bl6cqUAYRl07dB8qLaCtdPHKyulebCrDnmhqFtuCPOcKtyeAVFQq2mq8dfhQ%3D"}],"group":"cf-nel","max_age":604800}
nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
strict-transport-security: max-age=31536000; includeSubDomains; preload
server: cloudflare
cf-ray: 8316be935976632e-LHR
alt-svc: h3=":443"; ma=86400

sxg1-b3^@^@ <em>&#8230; binary data follows</em></code></pre><h4>Dump-signedexchange</h4><p>While <strong>curl</strong> outputs the raw binary SXG format, which humans can decode manually, there's a more convenient option: a command-line tool written in Go that's part of a larger <a href="https://github.com/WICG/webpackage/blob/main/go/signedexchange/README.md">SXG utilities</a> package. This tool simplifies the decoding process significantly.</p><p>Here is an example output. I removed the <strong>Link</strong> header contents and the <strong>signature</strong> for readability:</p><pre><code>$ dump-signedexchange -payload=false -uri https://www.planujemywesele.pl/zespoly/opole

format version: 1b3
request:
  method: GET
  uri: https://www.planujemywesele.pl/zespoly/opole
  headers:
response:
  status: 200
  headers:
    Etag: "d1xui95j634kzc"
    Status: 200 OK
    Content-Type: text/html; charset=utf-8
    Cf-Cache-Status: HIT
    Content-Encoding: mi-sha256-03
    X-Content-Type-Options: nosniff
    Link: ...
    X-App-Id: 2
    Accept-Ranges: bytes
    Cache-Control: s-maxage=86400, max-age=1800, public
    Date: Wed, 08 Jan 2025 17:27:48 GMT
    Cf-Ray: 8fede682c14702a0-WAW
    Digest: mi-sha256-03=4Sh+fNgY++2HoZ4CzZwUTk4TvTfQKfBJvTOn586Ld7g=
    X-Frame-Options: SAMEORIGIN
    Age: 0
    Server: cloudflare
    Content-Length: 215609
signature: ...
header integrity: sha256-R9wSEb/HOpGyeJM9Inzr9Pz6+Zw2CV03sH6GT1ElH3s=</code></pre><p>Please keep in mind both <strong>curl</strong> and <strong>dump-signedexchange</strong> won&#8217;t work when you use <a href="https://developers.cloudflare.com/bots/">Cloudflare bot protection</a> mechanisms, as those are not apps humans use for everyday browsing. They&#8217;ll be recognized as bots and blocked.</p><p>So either disable bot protection when you perform tests or use the next tool.</p><h4>Headers-altering extension</h4><p>There are many browser extensions allowing you to manipulate HTTP request headers. One I used was <a href="https://requestly.io/">Requestly</a>. Similarly to curl, you just need to set the <strong>Accept</strong> header to:</p><pre><code>application/signed-exchange;v=b3</code></pre><p>It runs in the browser, so it can be used with Cloudflare bot protection enabled.</p><p>It works best when combined with Chrome Developer Tools, so you can see SXG response, nicely decoded with headers and stuff.</p><h4>Google Search Console</h4><p>When debugging, it's valuable to view your site as Google sees it. The Google Search Console provides a <strong>URL inspection tool</strong> with a <strong>Live test</strong> feature that lets you fetch any URL using Googlebot and examine the results.</p><p>When testing an SXG-enabled URL, Googlebot will show you the decoded SXG response, similar to what you'd see using the <strong>dump-signedexchange</strong> tool:</p><pre><code>HTTP/1.1 200 OK
accept-ranges: bytes
cache-control: s-maxage=86400, max-age=1800, public
cf-cache-status: HIT
cf-ray: 8fedfc8990c86806-DFW
content-encoding: mi-sha256-03
content-length: 215601
content-type: text/html; charset=utf-8
date: Wed, 08 Jan 2025 17:42:51 GMT
digest: mi-sha256-03=H6SDmuJOXIr/v8Wcfjl++ilQBmaDOpj53i9CCVpdXkI=
etag: "7y1kp1nupp4kz4"
link: ...
server: cloudflare
status: 200 OK
x-app-id: 2
x-content-type-options: nosniff</code></pre><h2>Results</h2><p>Implementing the above changes made it possible to generate the SXG version of the page HTML. Therefore prefetching it in Google results should make the LCP drop by&#8230; the time it takes to download HTML. On my connection, it&#8217;s probably somewhere between 100-600 ms, depending on how quickly the server responds.</p><h4>But how does it compare to that 350 ms I promised and demonstrated earlier?</h4><p>Well, it&#8217;s just the beginning. We have a solid foundation for further optimizations. The next step is to prefetch not only HTML, but the so-called subresources - stylesheets, images, and fonts. And this is where <a href="https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges">the LCP begins to drop significantly</a>!</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;e720132d-6f9c-4e5b-a432-5a87ee806df6&quot;,&quot;caption&quot;:&quot;I will explain how to drastically improve your website's loading time for Google-referred users using a little-known technology called Signed Exchanges (SXG).&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Prefetching subresources with Signed Exchanges&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:41077879,&quot;name&quot;:&quot;Pawe&#322; Pokrywka&quot;,&quot;bio&quot;:&quot;I write about security, privacy, and complex systems&#8212;exploring them from the messy middle ground between engineering and research.&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1c35edc2-abb7-4761-8eb4-f379f25e519a_800x800.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-01-13T09:45:55.633Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f9af7ab-a9b5-4fc4-a0bf-6eb804fb2fb0_2952x2293.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.pawelpokrywka.com/p/subresources-prefetching-with-signed-exchanges&quot;,&quot;section_name&quot;:&quot;Performance&quot;,&quot;video_upload_id&quot;:null,&quot;id&quot;:148076573,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:2,&quot;comment_count&quot;:2,&quot;publication_id&quot;:null,&quot;publication_name&quot;:&quot;Pawe&#322; Pokrywka's Lab&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe44a7fc6-d5ec-4a99-984a-7a7e0bc80a17_800x800.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><h2>Thanks!</h2><p>I would like to express my gratitude to my co-workers, Micha&#322; and Besufekad, for their support and assistance in debugging.</p><p>Thank you for reading this post. I really appreciate it, because it was a long read!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">If you want to stay informed about the upcoming posts and become an expert at improving LCP at the same time, subscribe!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>If you know someone struggling with high LCP, especially if the website in question gets a lot of traffic from Google, then please share this post. I would be grateful :)</p><p>See you in the next part of this series!</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>In the <strong>Help</strong> section, near the toggle for the <strong>Automatic Signed Exchanges</strong> option, Cloudflare states that:</p><p><em>Google only loads Signed Exchanges for the top results.</em></p><p>Contrary to the statement above, the position in Google search results does not appear to matter. I observed that SXG prefetching occurs even for pages ranked well beyond the top 10 results.</p><p>While I am not entirely certain, I suspect that the main factor determining whether a page will be prefetched is page popularity measured by impressions in Google search.</p><p>The other factors remain unknown to me. In my experiments, I observed that only one of the results on a given Google page is prefetched, and it is <strong>not always</strong> the top result.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>I&#8217;m not aware of any other company offering SXG. It might be possible to implement SXG with Fastly using <a href="https://github.com/google/sxg-rs/tree/main/fastly_compute">this repository</a>, but I couldn&#8217;t find any official references to it on the company&#8217;s website.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-3" href="#footnote-anchor-3" class="footnote-number" contenteditable="false" target="_self">3</a><div class="footnote-content"><p>This feature is available only for Enterprise customers (as of 2023).</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-4" href="#footnote-anchor-4" class="footnote-number" contenteditable="false" target="_self">4</a><div class="footnote-content"><p>This feature is available only in the Nginx commercial subscription (as of 2023).</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-5" href="#footnote-anchor-5" class="footnote-number" contenteditable="false" target="_self">5</a><div class="footnote-content"><p>There is a way to serve different versions of the page optimized for different screen sizes using the <strong>Vary</strong> HTTP header paired with the <strong><a href="https://github.com/google/webpackager/blob/main/docs/supported_media.md">supported-media</a></strong><a href="https://github.com/google/webpackager/blob/main/docs/supported_media.md"> meta tag</a>. I haven&#8217;t explored this solution, as my website uses a responsive design and Cloudflare has very limited support for <a href="https://developers.cloudflare.com/cache/concepts/cache-control/">caching responses with </a><strong><a href="https://developers.cloudflare.com/cache/concepts/cache-control/">Vary</a></strong><a href="https://developers.cloudflare.com/cache/concepts/cache-control/"> header</a>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-6" href="#footnote-anchor-6" class="footnote-number" contenteditable="false" target="_self">6</a><div class="footnote-content"><p>It is possible to enable <a href="https://developer.chrome.com/blog/sxg-desktop/">selective prefetching for cookieless users</a>. However, users with cookies set (typically those who have visited your page previously) won&#8217;t benefit from SXG prefetching. Since my goal was to improve page loading speed for as many users as possible, I chose not to explore this approach further.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-7" href="#footnote-anchor-7" class="footnote-number" contenteditable="false" target="_self">7</a><div class="footnote-content"><p>In my experiments, I found not setting the <strong>Cache-Control</strong> header at all makes Cloudflare generate SXG expiring in 7 days, an equivalent of setting <strong>max-age/s-maxage</strong> to this value.</p><p></p></div></div>]]></content:encoded></item></channel></rss>