<?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]]></title><description><![CDATA[I write about security, privacy, and complex systems—exploring them from the messy middle ground between engineering and research.]]></description><link>https://www.pawelpokrywka.com</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</title><link>https://www.pawelpokrywka.com</link></image><generator>Substack</generator><lastBuildDate>Tue, 05 May 2026 11:09:50 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><item><title><![CDATA[Create a Ruby gem with Zeitwerk as a development-only dependency (tutorial)]]></title><description><![CDATA[Forget require statements and make your gem lightweight at the same time]]></description><link>https://www.pawelpokrywka.com/p/gem-with-zeitwerk-as-development-only-dependency</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/gem-with-zeitwerk-as-development-only-dependency</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Sun, 20 Aug 2023 17:56:54 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!MQCJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.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_!MQCJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MQCJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg 424w, https://substackcdn.com/image/fetch/$s_!MQCJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg 848w, https://substackcdn.com/image/fetch/$s_!MQCJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!MQCJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MQCJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg" width="1456" height="942" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:942,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:712764,&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-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!MQCJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg 424w, https://substackcdn.com/image/fetch/$s_!MQCJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg 848w, https://substackcdn.com/image/fetch/$s_!MQCJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!MQCJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c37f787-584b-4f26-86fd-b804872e0534_1920x1242.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">Image by <a href="https://pixabay.com/users/alexas_fotos-686414/?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=3223739">Alexa</a> from <a href="https://pixabay.com//?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=3223739">Pixabay</a></figcaption></figure></div><h2>For the impatient</h2><p>If you know what Zeitwerk is, understand the problem, and just want to do what the title of this post promises, <a href="https://www.pawelpokrywka.com/i/136189156/have-your-cake-and-eat-it-too">jump straight into the tutorial</a>.</p><p>Otherwise, keep on reading.</p><h2>The problem</h2><p>If you work on a non-Rails Ruby project, managing code loading may be challenging. Following the <a href="https://en.wikipedia.org/wiki/Single-responsibility_principle">Single Responsibility Principle</a> means having many specialized classes instead of one doing all the work. And if you keep each class in a separate file you quickly end up with a bunch of files that need to be loaded. That&#8217;s a lot of sad <em>require</em> statements hiding in your files. You can almost hear their mischievous giggles when you rename a class or move it into a different namespace. It may sound similar to:</p><pre><code><code>cannot load such file -- my_perfect_class (LoadError)</code></code></pre><p>Those exceptions make me angry. And Ruby was meant to be optimized<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> for the programmer&#8217;s happiness, right?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qXvr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qXvr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg 424w, https://substackcdn.com/image/fetch/$s_!qXvr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg 848w, https://substackcdn.com/image/fetch/$s_!qXvr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!qXvr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qXvr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg" width="1456" height="1166" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1166,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2254571,&quot;alt&quot;:&quot;Classic oil painting of a sleeping woman haunted by a Ruby LoadError nightmare. A puzzled Ruby programmer in the background.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Classic oil painting of a sleeping woman haunted by a Ruby LoadError nightmare. A puzzled Ruby programmer in the background." title="Classic oil painting of a sleeping woman haunted by a Ruby LoadError nightmare. A puzzled Ruby programmer in the background." srcset="https://substackcdn.com/image/fetch/$s_!qXvr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg 424w, https://substackcdn.com/image/fetch/$s_!qXvr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg 848w, https://substackcdn.com/image/fetch/$s_!qXvr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!qXvr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0987eb1c-3355-4379-9aab-da2276959449_2000x1602.jpeg 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><figcaption class="image-caption">Inspired by <em>The Nightmare</em> (1791) by Henry Fuseli, oil on canvas 101.6 &#215; 126.7 cm, Detroit Institute of Arts</figcaption></figure></div><h2>Enter Zeitwerk</h2><p>Well, there are solutions. One of the best, in my opinion, is <a href="https://github.com/fxn/zeitwerk">Zeitwerk</a>, described as an &#8220;Efficient and thread-safe code loader for Ruby&#8221;.</p><p>You probably heard about it, as it is used by Rails and Hanami. Zeitwerk is the reason you don&#8217;t need to worry about <em>require</em> statements when working with Rails apps. Just follow the intuitive convention of naming files the same as the classes they contain.</p><h2>Using Zeitwerk in your own gem</h2><p>The good news is: Zeitwerk is not limited to Rails. You can <a href="https://www.akshaykhot.com/using-zeitwerk-outside-rails/">use it in any Ruby project</a>, including any gem you create.</p><p>Just put it into Gemspec, and <a href="https://github.com/fxn/zeitwerk#for_gem">add a few lines</a> of code. That&#8217;s it, you are now free to focus on features instead of code loading.</p><p>The bad news is: your gem now depends on Zeitwerk. This means each project using your gem depends on it too. Some people may not like it and decide to not use your awesome gem!</p><p>That&#8217;s because it&#8217;s generally a good practice to minimize dependencies. The benefits include security, maintainability, and stability.</p><p>Does it mean you have to choose between ease of development and dependency minimalization?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mXnM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mXnM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg 424w, https://substackcdn.com/image/fetch/$s_!mXnM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg 848w, https://substackcdn.com/image/fetch/$s_!mXnM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!mXnM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mXnM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg" width="1024" height="684" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:684,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:207730,&quot;alt&quot;:&quot;Classic painting depicting a Ruby developer as Heracles having to choose between two females. The woman on the left symbolizes fewer dependencies. The female on the right symbolizes less frustration.&quot;,&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="Classic painting depicting a Ruby developer as Heracles having to choose between two females. The woman on the left symbolizes fewer dependencies. The female on the right symbolizes less frustration." title="Classic painting depicting a Ruby developer as Heracles having to choose between two females. The woman on the left symbolizes fewer dependencies. The female on the right symbolizes less frustration." srcset="https://substackcdn.com/image/fetch/$s_!mXnM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg 424w, https://substackcdn.com/image/fetch/$s_!mXnM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg 848w, https://substackcdn.com/image/fetch/$s_!mXnM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!mXnM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4ee9127-d575-4854-8717-bb27487268e8_1024x684.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">Inspired by <em>The Choice of Heracles</em> (1596) by Annibale Carracci, oil on canvas 239 cm &#215; 165 cm, National Museum of Capodimonte</figcaption></figure></div><h2>Have your cake and eat it too</h2><div class="pullquote"><p>Can you use Zeitwerk for development, but not include it as a runtime gem dependency?</p></div><p>I asked myself this question while working on <a href="https://www.pawelpokrywka.com/p/cryptreboot">cryptreboot</a>, a gem that allows a machine with an encrypted disk to reboot by <strong>requesting a passphrase beforehand, rather than after the reboot.</strong></p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;ad909c9d-ad12-4c98-ad2d-5021f0b5ae43&quot;,&quot;caption&quot;:&quot;As an Ethereum solo staker, I want my Linux staking box to be as secure as feasible. It applies to physical security too. If the attacker gains access to the validator key stored on disk, it will allow him/her to do malicious things such as intentionally exposing the validator to the slashing penalties.&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Rebooting Linux with encrypted disk&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;2023-07-31T13:53:24.456Z&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%2F60692078-4e29-4484-9988-f262d39b153f_4608x3456.jpeg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.pawelpokrywka.com/p/rebooting-linux-with-encrypted-disk&quot;,&quot;section_name&quot;:&quot;Privacy &amp; Security&quot;,&quot;video_upload_id&quot;:null,&quot;id&quot;:134149533,&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><p>I was not able to find an answer on Google. ChatGPT also failed. Therefore I decided to do it by myself.</p><p>In this tutorial, I will show you how to create a gem from the ground up. It will use Zeitwerk in development, while not depending on it in runtime. And it will still work.</p><h2>Conventions</h2><p>The first character of a line in code blocks has a special meaning:</p><ul><li><p>$ means the remaining part should be executed in the shell,</p></li><li><p>&gt; means the remaining part should be executed in IRB.</p></li></ul><p>In other cases, the block contains the actual code or result of an action you performed.</p><p>I assume you use a Unix-based OS such as Linux distribution (may be run in WSL), *BSD, or macOS. If you run Windows, you may need to adjust some shell commands.</p><h2>Start with an empty gem</h2><p>We will call our gem <em>zeitgeist</em>. Execute in the console:</p><pre><code><code>$ bundle gem zeitgeist
$ cd zeitgeist</code></code></pre><p>Now adjust the Gemspec to make the gem buildable. Open <em>zeitgeist.gemspec</em> file and:</p><ol><li><p>Fill <em>summary</em>, <em>homepage</em>, <em>source_code_uri</em>, and <em>changelog_uri</em>.</p></li><li><p>Delete the line containing the <em>description</em>.</p></li></ol><p>The results should look similar to this (I stripped comments):</p><pre><code><code>require_relative "lib/zeitgeist/version"

Gem::Specification.new do |spec|
  spec.name = "zeitgeist"
  spec.version = Zeitgeist::VERSION
  spec.authors = ["Pawel"]
  spec.email = ["pepawel@users.noreply.github.com"]

  spec.summary = "Awesome gem using Zeitwerk only for development."
  spec.homepage = "https://github.com/pepawel/zeitgeist"
  spec.license = "MIT"
  spec.required_ruby_version = "&gt;= 2.6.0"

  spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"

  spec.metadata["homepage_uri"] = spec.homepage
  spec.metadata["source_code_uri"] = spec.homepage
  spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"

  spec.files = Dir.chdir(__dir__) do
    `git ls-files -z`.split("\x0").reject do |f|
      (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
    end
  end
  spec.bindir = "exe"
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
  spec.require_paths = ["lib"]
end</code></code></pre><p>You should spend some more time on Gemspec if you work on a real gem. The above example minimizes changes needed for building, but it&#8217;s not production-ready.</p><p>Install dependencies:</p><pre><code><code>$ bundle config set --local path .bundle/gems # for easy cleanup
$ bundle install</code></code></pre><p>Let&#8217;s commit the changes and check if you can build the gem:</p><pre><code><code>$ git add .
$ git commit -m 'Initial commit'
$ rake build</code></code></pre><p>It should succeed. However, if there is an error it should be self-explanatory. Most probably you will need to adjust Gemspec.</p><h2>Add some logic</h2><p>Place the following code in <em>lib/zeitgeist/programmer.rb</em>:</p><pre><code><code>module Zeitgeist
  class Programmer
    def happy?
      # Ruby was created in 1993
      Time.now.year &gt;= 1993 ? 'yes' : 'no'
    end
  end
end</code></code></pre><p>To check if it works, run IRB in the context of the gem by executing in the terminal:</p><pre><code><code>$ bin/console</code></code></pre><p>When you try to access <em>Zeitgeist::Programmer</em>, you will see it&#8217;s not loaded:</p><pre><code><code>&gt; Zeitgeist::Programmer
(irb):1:in `&lt;main&gt;':
  uninitialized constant Zeitgeist::Programmer (NameError)
  from bin/console:15:in `&lt;main&gt;'</code></code></pre><p>To make it work, you will need to manually require the correct file:</p><pre><code><code>&gt; require 'zeitgeist/programmer'
=&gt; true
&gt; Zeitgeist::Programmer
=&gt; Zeitgeist::Programmer</code></code></pre><p>But we want it to be required automatically by Zeitwerk. In our example above, it is enough to simply <em>require</em> the correct file. But with your codebase becoming larger, the benefits of using auto-loader will become more and more visible.</p><h2>Enable Zeitwerk</h2><p>Add the Zeitwerk to your Gemfile and install it:</p><pre><code><code>$ bundle add zeitwerk</code></code></pre><p>Gems added to the gem&#8217;s Gemfile are available in development only, so Zeitwerk won&#8217;t become a runtime dependency.</p><p>The next step is to make sure your code uses Zeitwerk. Adjust <em>lib/zeitgeist.rb</em> to include the following lines before the <em>Zeitgeist</em> module definition:</p><pre><code><code>require "zeitwerk"
loader = Zeitwerk::Loader.for_gem
loader.setup</code></code></pre><p>Now run the console again and check if your code is being auto-loaded:</p><pre><code><code>$ bin/console
&gt; Zeitgeist::Programmer.new.happy?
=&gt; "yes"</code></code></pre><p>It works, so we can commit our code changes:</p><pre><code><code>$ git add lib Gemfile*
$ git commit -m 'Add business logic and setup autoloader'</code></code></pre><p>However, it won&#8217;t work when installed on a system without Zeitwerk. The gem will install normally but will raise an exception on first use because it silently depends on Zeitwerk.</p><h2>Basic loader</h2><p>We need a lightweight loader for runtime. Its task would be to load all the Ruby files in gem&#8217;s <em>lib/</em> directory. Why not loop over the files and simply <em>require</em> each one? It&#8217;s because the order of loading files matters. In the loop approach, we may end up loading file <em>B</em> depending on file <em>A</em> before file <em>A</em> is loaded.</p><p>To make sure we use the correct loading order, let&#8217;s create the loader file manually in <em>lib/basic_loader.rb</em>:</p><pre><code><code># Load every project file in one place
require 'zeitgeist/programmer'</code></code></pre><h2>Runtime vs development</h2><p>We want to use a basic loader for runtime and Zeitwerk for development. Therefore we need to distinguish between those two.</p><p>As <em>Gemfile</em> is used only in development, we could use it for this purpose. Add the following code to the beginning of that file:</p><pre><code><code>module ::Zeitgeist # :: is used to escape from Gemfile's scope
  AUTOLOADERS = []
end</code></code></pre><p>It simply sets a constant in the main module of our gem. It will be defined only in development.</p><p>You may notice it duplicates the module definition from the <em>zeitgeist.rb</em>. It&#8217;s perfectly fine as the modules in Ruby can be reopened as many times as you like.</p><p>Now, let&#8217;s adjust <em>lib/zeitgeist.rb</em> to decide which loader to use based on the constant we defined:</p><pre><code><code>require 'zeitgeist/version'

if defined? Zeitgeist::AUTOLOADERS
  require 'zeitwerk'
  Zeitgeist::AUTOLOADERS &lt;&lt; Zeitwerk::Loader.for_gem.tap do |loader|
    loader.ignore("#{__dir__}/basic_loader.rb")
    loader.setup
  end
else
  require 'basic_loader'
end

module Zeitgeist
  class Error &lt; StandardError; end
  # Your code goes here...
end</code></code></pre><p>The <em>basic_loaded.rb</em> file doesn&#8217;t match the Zeitwerk convention - it doesn&#8217;t define the <em>BasicLoader</em> constant. Therefore, to avoid warnings we add it to the ignored files as you see above.</p><p>The other change was to save the Zeitwerk instance into <em>AUTOLOADERS</em> constant. We will use it later.</p><p>Let&#8217;s commit our changes, build the gem, and install it locally:</p><pre><code><code>$ git add lib Gemfile
$ git commit -m 'Fix code loading'
$ rake build
$ gem install --user pkg/zeitgeist-0.1.0.gem</code></code></pre><p>As our gem doesn&#8217;t contain any executables, you can safely ignore the following warning if it appears:</p><pre><code><code>WARNING:  You don't have /home/user/.gem/ruby/3.0.0/bin in your PATH,
          gem executables will not run.</code></code></pre><p>Now we can test if the gem works:</p><pre><code><code>$ irb
&gt; require 'zeitgeist'
&gt; Zeitgeist::Programmer.new.happy?
=&gt; "yes"
&gt; defined? Zeitwerk
=&gt; nil</code></code></pre><p>It works, and the last line tells us Zeitwerk is not used.</p><p>Now let&#8217;s verify if development works too:</p><pre><code><code>$ bin/console
&gt; Zeitgeist::Programmer.new.happy?
=&gt; "yes"
&gt; defined? Zeitwerk
=&gt; "constant"</code></code></pre><p>It works too, but uses Zeitwerk just as we like.</p><p>However, we need to manually update <em>lib/basic_loader.rb</em> each time we add, remove, rename, or move the files. Why not automate it?</p><h2>Autogenerate basic loader</h2><p>Zeitwerk does <a href="https://medium.com/cedarcode/understanding-zeitwerk-in-rails-6-f168a9f09a1f">its magic</a> using <a href="https://www.rubydoc.info/stdlib/core/Module#autoload-instance_method">Module#autoload</a>. It guarantees the loading order is valid. We can use Zeitwerk to produce <em>lib/basic_loader.rb</em> file containing all the <em>require</em> statements in the correct order.</p><p>To generate that file, we will use Zeitwerk&#8217;s <em>on_load</em> callback which gets called every time file is loaded. We need to get a list of all files, therefore we will force Zeitwerk to load the entire codebase by using <em>#eager_load</em> method.</p><p>Put the following into <em>bin/loader</em>:</p><pre><code><code>#!/usr/bin/env ruby
# frozen_string_literal: true

require 'pathname'
require 'stringio'

class LoaderGenerator
  def call
    writer do |out|
      out.puts &lt;&lt;~HEADER
        # frozen_string_literal: true

        # File generated automatically, do not edit

      HEADER
      yield.each do |auto_loader|
        auto_loader.on_load do |_cpath, _value, abspath|
          next if abspath !~ /.rb$/i # skip directories

          out.puts "require '#{path_to_requirement(abspath)}'"
        end
        auto_loader.eager_load
      end
    end
    true
  end

  private

  def writer(&amp;block)
    output ? block.call(output) : File.open(loader_path, 'w', &amp;block)
  end

  def path_to_requirement(abspath)
    relative_path_from(loader_dir, abspath).sub(/.rb$/i, '')
  end

  def relative_path_from(base_dir, target_path)
    target = Pathname.new(target_path)
    base = Pathname.new(base_dir)
    target.relative_path_from(base).to_s
  end

  attr_reader :loader_path, :loader_dir, :output

  def initialize(loader_path, output = nil)
    @loader_path = loader_path
    @loader_dir = File.dirname(loader_path)
    @output = output
  end
end

class LoaderValidator
  def call(&amp;block)
    StringIO.new.tap do |buffer|
      LoaderGenerator.new(loader_path, buffer).call(&amp;block)
      buffer.rewind
    end.read == current
  end

  private

  def current
    File.read(loader_path)
  end

  attr_reader :loader_path

  def initialize(loader_path)
    @loader_path = loader_path
  end
end

require 'bundler/setup'

loader_path = File.join(__dir__, '..', 'lib', 'basic_loader.rb')

klass = ARGV[0] == 'generate' ? LoaderGenerator : LoaderValidator
action = klass.new(loader_path)
result = action.call do
  require 'zeitgeist'
  Zeitgeist::AUTOLOADERS
end
exit 1 unless result</code></code></pre><p>Now make the file executable and run it:</p><pre><code><code>$ chmod +x bin/loader
$ bin/loader generate</code></code></pre><p>It will:</p><ol><li><p>Initialize Zeitwerk and the main module of our gem.</p></li><li><p>Configure Zeitwerk to add <em>require</em> line to the basic loader each time the Ruby file is loaded.</p></li><li><p>Tell Zeitwerk to eagerly load all the code. As a result, <em>lib/basic_loader.rb</em> becomes populated.</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_!nx-p!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nx-p!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg 424w, https://substackcdn.com/image/fetch/$s_!nx-p!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg 848w, https://substackcdn.com/image/fetch/$s_!nx-p!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!nx-p!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nx-p!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg" width="1456" height="661" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:661,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1294531,&quot;alt&quot;:&quot;A classic fresco depicting almighty Zeitwerk (God) creating a basic loader (Adam) which says: \&quot;Thx for creating me Z. Got runtime. See you in dev.\&quot;&quot;,&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="A classic fresco depicting almighty Zeitwerk (God) creating a basic loader (Adam) which says: &quot;Thx for creating me Z. Got runtime. See you in dev.&quot;" title="A classic fresco depicting almighty Zeitwerk (God) creating a basic loader (Adam) which says: &quot;Thx for creating me Z. Got runtime. See you in dev.&quot;" srcset="https://substackcdn.com/image/fetch/$s_!nx-p!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg 424w, https://substackcdn.com/image/fetch/$s_!nx-p!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg 848w, https://substackcdn.com/image/fetch/$s_!nx-p!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!nx-p!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1395286f-1ddd-4e06-822c-4facb0444cb9_2880x1307.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">Inspired by <em>The Creation of Adam</em> (~1511) by Michelangelo, fresco painting, 230.1 cm &#215; 480.1 cm, Sistine Chapel</figcaption></figure></div><p>The file <em>lib/basic_loader.rb</em> should look like this:</p><pre><code><code># frozen_string_literal: true

# File generated automatically, do not edit

require 'zeitgeist/programmer'</code></code></pre><h2>Ensure the built gem contains current files</h2><p>If you read the <em>bin/loader</em> code above carefully, you probably noticed <em>LoaderValidator</em> class. It gets activated if <em>generate</em> argument was not specified. It generates a basic loader in memory and compares it with the current. If they don&#8217;t match, the script exits with an error.</p><p>We will use it to protect us from forgetting to regenerate the basic loader before building the gem.</p><p>Let&#8217;s add those lines to the end of <em>Rakefile</em>:</p><pre><code><code>Rake::Task.define_task :validate_loader do
  abort "Basic loader is stale, run `bin/loader generate` to fix" unless system("bin/loader validate")
end

Rake::Task[:build].enhance [:validate_loader]</code></code></pre><p>We create a new task called <em>validate_loader</em> which raises an exception if the basic loader is stale. Afterward, we add it as a dependency to the <em>build</em> task, so it gets called before.</p><p>From now on running <em>rake build</em> with stale basic loader will fail with the message:</p><pre><code><code>Basic loader is stale, run `bin/loader generate` to fix</code></code></pre><h2>Tests: which loader to use?</h2><p>It makes more sense to use the basic loader instead of Zeitwerk for tests. What if we generate the basic loader incorrectly due to some error? If tests use Zeitwerk, the basic loader is bypassed and we won&#8217;t see any problems.</p><p>As we use RSpec for our gem, let&#8217;s add this line to the beginning of <em>spec/spec_helper.rb</em>:</p><pre><code><code>raise "Failed to regenerate basic loader" unless system "bin/loader generate"</code></code></pre><p>This way before each test, the basic loader will be regenerated automatically.</p><p>If you prefer to decide when the basic loader file changes, you can use this instead:</p><pre><code><code>raise "Basic loader is stale, run `bin/loader generate` to fix" unless system "bin/loader validate"</code></code></pre><p>You will be notified when the basic loader needs to be updated. This approach allows you to have full control over updating this file.</p><h2>Run the tests</h2><p>We should have a working infrastructure, let&#8217;s run the tests:</p><pre><code><code>$ rake
... 
Zeitgeist
  has a version number
  does something useful (FAILED - 1)
...</code></code></pre><p>The second test failed. It&#8217;s an example test that is meant to fail. Let&#8217;s change it to something related to our logic by replacing <em>spec/zeitgeist_spec.rb</em> with:</p><pre><code><code>RSpec.describe Zeitgeist do
  it "has a version number" do
    expect(Zeitgeist::VERSION).not_to be nil
  end

  it "checks if programmer is happy" do
    expect(Zeitgeist::Programmer.new.happy?).to eq("yes")
  end
end</code></code></pre><p>Run the tests again:</p><pre><code><code>$ rake
...
Zeitgeist
  has a version number
  checks if programmer is happy
...</code></code></pre><p>All green! Let&#8217;s commit our changes, build the gem and install it:</p><pre><code><code>$ git add lib spec bin/loader Rakefile
$ git commit -m 'Make sure basic loader is autogenerated'
$ rake build
$ gem install --user pkg/zeitgeist-0.1.0.gem</code></code></pre><h2>Final run</h2><p>We can check if the gem works after installation:</p><pre><code><code>$ irb
&gt; require 'zeitgeist'
&gt; Zeitgeist::Programmer.new.happy?
=&gt; "yes"
&gt; defined? Zeitwerk
=&gt; nil</code></code></pre><p>It works, and it still doesn&#8217;t require Zeitwerk.</p><p>Now let&#8217;s test if Zeitwerk works in development. First, let&#8217;s create a random constant:</p><pre><code><code>$ echo "Zeitgeist::Pi = 3.1337" &gt; lib/zeitgeist/pi.rb</code></code></pre><p>This file is not required anywhere. Let&#8217;s check if it autoloads:</p><pre><code><code>$ bin/console
&gt; Zeitgeist::Pi
=&gt; 3.1337</code></code></pre><p>It works. From now on, you can easily add new files to your code base and do not worry about <em>require</em> statements. Congratulations :)</p><p></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 that was interesting, subscribe! You will receive my upcoming posts in your mailbox. 100% spam free, unsubscribe anytime.</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>Cleanup</h2><p>To restore your environment to its previous state, you will need to uninstall the gem:</p><pre><code><code>$ gem uninstall zeitgeist</code></code></pre><p>You can also remove the directory containing the repository you were working on. Apart from the code, all the gems you installed including Zeitwerk are located there.</p><h2>Limitations</h2><p>Compared to using Zeitwerk, this approach has some limitations. I believe in most cases, minor code adjustments would do. As with every refactoring, good test coverage will help. However, if your project is large and/or you use advanced code-loading features, implementation of this approach may be challenging.</p><p>Those limitations were identified by the author of Zeitwerk, Xavier Noria. He gave me <a href="https://github.com/fxn/zeitwerk/issues/271">awesome feedback</a> which resulted in a major update to the article (the old version could be found <a href="https://web.archive.org/web/20230821123926/https://blog.pawelpokrywka.com/p/gem-with-zeitwerk-as-development-only-dependency">here</a>). Thank you, Xavier!</p><p>Here are the limitations I&#8217;m currently aware of:</p><h4>Conditional code loading</h4><p>An example would be to use one module on Linux, and another on macOS:</p><pre><code><code>module Foo
  include Mac if mac?
  include Linux if linux?
end</code></code></pre><p>If OS-specific modules are defined in separate files, the basic loader will be generated differently depending on the developer&#8217;s OS.</p><p>Effectively gem developed on Linux will crash on macOS and vice-versa.</p><h4>Delayed code loading</h4><p>The developer may choose to delay the loading of some code parts. As the method presented in this post depends on eager loading, it won't include those parts. This will lead to crashes in runtime.</p><h4>Circular references</h4><p>Let&#8217;s assume we have <em>foo.rb</em>:</p><pre><code><code>module Foo
  include Bar
end</code></code></pre><p>And <em>foo/bar.rb</em>:</p><pre><code><code>module Foo::Bar
end</code></code></pre><p>Zeitwerk can handle this, while the generated basic loader will crash.</p><h4>Implicit namespace definitions</h4><p>Zeitwerk allows you to define <em>Zeitgeist::Foo::Bar</em> class without defining <em>Zeitgeist::Foo</em> module first. If you want to use the approach presented in this post, you need to define it explicitly.</p><h2>Final notes</h2><p>You learned how to use Zeitwerk in gem development while not adding it as a runtime dependency. <a href="https://github.com/pepawel/zeitgeist">Here you will find the repository</a> with the gem we just built together.</p><p>I encourage you to apply this knowledge to your own project. The code I published here uses a permissive MIT license, so you can use it even in your commercial work.</p><p>Also, there are <a href="https://rubygems.org/gems/zeitwerk/reverse_dependencies">nearly 450 gems depending on Zeitwerk</a>. I think many of them would accept pull requests minimizing runtime dependencies. If you feel brave, go contribute to your favorite gem to make the Open Source world a better place!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PnZR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PnZR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png 424w, https://substackcdn.com/image/fetch/$s_!PnZR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png 848w, https://substackcdn.com/image/fetch/$s_!PnZR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png 1272w, https://substackcdn.com/image/fetch/$s_!PnZR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PnZR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png" width="385" height="489" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:489,&quot;width&quot;:385,&quot;resizeWidth&quot;:385,&quot;bytes&quot;:23987,&quot;alt&quot;:&quot;A complicated stack of blocks symbolizing all the modern infrastructure. One small block at the bottom symbolizes a project some random person in Nebraska has been thanklessly maintaining since 2003&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="A complicated stack of blocks symbolizing all the modern infrastructure. One small block at the bottom symbolizes a project some random person in Nebraska has been thanklessly maintaining since 2003" title="A complicated stack of blocks symbolizing all the modern infrastructure. One small block at the bottom symbolizes a project some random person in Nebraska has been thanklessly maintaining since 2003" srcset="https://substackcdn.com/image/fetch/$s_!PnZR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png 424w, https://substackcdn.com/image/fetch/$s_!PnZR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png 848w, https://substackcdn.com/image/fetch/$s_!PnZR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.png 1272w, https://substackcdn.com/image/fetch/$s_!PnZR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8078e843-b32f-4b04-b9ad-f24222c374a0_385x489.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">Please don&#8217;t get me wrong. Zeitwerk is a rock-solid, actively developed gem. I highly respect the developers behind it and I am grateful to them. But I still believe limiting your dependencies is beneficial. Source: <a href="https://xkcd.com/2347/">xkcd</a></figcaption></figure></div><p>The code used in this tutorial was extracted from the <a href="https://www.pawelpokrywka.com/p/cryptreboot">cryptreboot</a> gem. This little project is very close to my heart, so forgive me I mentioned it for a second time in this post. If you use a Linux system with an encrypted disk, give cryptreboot a try :)</p><p>And thank you for reading this post :) You are the best!</p><p>If you have any feedback, please leave it in a comment below.</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>At <a href="https://www.youtube.com/watch?v=oEkJvvGEtB4">this</a> Google Tech Talk in 2008, Matz said, &#8220;I hope to see Ruby help every programmer in the world to be productive, and to enjoy programming, and to be happy. That is the primary purpose of Ruby language.&#8221;</p><p></p></div></div>]]></content:encoded></item><item><title><![CDATA[Rebooting Linux with encrypted disk]]></title><description><![CDATA[How to make it easier to keep the kernel updated]]></description><link>https://www.pawelpokrywka.com/p/rebooting-linux-with-encrypted-disk</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/rebooting-linux-with-encrypted-disk</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Mon, 31 Jul 2023 13:53:24 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!y6S8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60692078-4e29-4484-9988-f262d39b153f_4608x3456.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_!y6S8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60692078-4e29-4484-9988-f262d39b153f_4608x3456.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!y6S8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60692078-4e29-4484-9988-f262d39b153f_4608x3456.jpeg 424w, https://substackcdn.com/image/fetch/$s_!y6S8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60692078-4e29-4484-9988-f262d39b153f_4608x3456.jpeg 848w, https://substackcdn.com/image/fetch/$s_!y6S8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60692078-4e29-4484-9988-f262d39b153f_4608x3456.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!y6S8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60692078-4e29-4484-9988-f262d39b153f_4608x3456.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!y6S8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60692078-4e29-4484-9988-f262d39b153f_4608x3456.jpeg" width="728" height="546" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/60692078-4e29-4484-9988-f262d39b153f_4608x3456.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:1092,&quot;width&quot;:1456,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:6956375,&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-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!y6S8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60692078-4e29-4484-9988-f262d39b153f_4608x3456.jpeg 424w, https://substackcdn.com/image/fetch/$s_!y6S8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60692078-4e29-4484-9988-f262d39b153f_4608x3456.jpeg 848w, https://substackcdn.com/image/fetch/$s_!y6S8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60692078-4e29-4484-9988-f262d39b153f_4608x3456.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!y6S8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60692078-4e29-4484-9988-f262d39b153f_4608x3456.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></figure></div><p>As an <a href="https://ethstaker.cc/">Ethereum solo staker</a>, I want my Linux staking box to be as secure as feasible. It applies to physical security too. If the attacker gains access to the validator key stored on disk, it will allow him/her to do malicious things such as intentionally exposing the validator to the slashing penalties.</p><p>My machine isn&#8217;t placed in a data center guarded 24/7, therefore to protect it from the physical attacker I used cryptography.</p><h2>Encryption inconvenience</h2><p>I decided to use <a href="https://en.wikipedia.org/wiki/Linux_Unified_Key_Setup">LUKS disk encryption</a> which can be enabled easily during the Linux installation. I set a long passphrase and I rest assured my data is well protected.</p><p>I need to provide the passphrase each time I turn the machine on. This way the volume key used to unlock the disk, can be derived from the text I enter. It requires a keyboard and physical presence during startup, therefore it may be inconvenient for headless systems.</p><p>There are many methods to make it easier. Though I use one of them, I find them either complex, costly, unreliable, or compromising security. I don&#8217;t want to discuss them here, because this topic deserves a separate post.</p><p>The important thing is, <strong>once started, the node should stay online</strong>. And with UPS protection, turning it off doesn&#8217;t happen almost at all.</p><h2>Maintenance</h2><p>To ensure there are no security bugs I perform regular software updates. Modern Linux distributions make it really easy to automate.</p><p>However, kernel updates are not that simple. To use a new kernel, the system needs to be rebooted. It&#8217;s just a short downtime which is often acceptable.</p><p>But when using disk encryption, the disk needs to be unlocked. How to achieve that when using ssh connection from a remote location? Given the fact kernel updates can be frequent, it becomes an issue.</p><p>One way to approach it is to try to avoid reboots.</p><h2>Live patching&#8230;</h2><p>&#8230;is a technique to apply changes to the running kernel, without a reboot. There are <a href="https://ubuntu.com/security/livepatch">many</a> <a href="https://www.suse.com/products/live-patching/">proprietary</a> <a href="https://www.redhat.com/sysadmin/kernel-live-patching-linux">implementations</a> <a href="https://ksplice.oracle.com/">provided</a> <a href="https://www.ninjaone.com/patch-management/linux/">as</a> <a href="https://tuxcare.com/enterprise-live-patching-services/kernelcare-enterprise/">services</a>. Preparing a binary patch ready to be applied to a running kernel seems to me a complex task that requires a lot of testing. Probably that&#8217;s why most suppliers charge for this service. In the case of <a href="https://ubuntu.com/security/livepatch">Ubuntu Livepatch</a>, before the patch is pushed to the customers, it is tested on the production systems of non-paying users. That&#8217;s the cost of using it for free!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-hmy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9afe46f8-e620-41c0-b425-84f240a99e4a_1481x850.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-hmy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9afe46f8-e620-41c0-b425-84f240a99e4a_1481x850.jpeg 424w, https://substackcdn.com/image/fetch/$s_!-hmy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9afe46f8-e620-41c0-b425-84f240a99e4a_1481x850.jpeg 848w, https://substackcdn.com/image/fetch/$s_!-hmy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9afe46f8-e620-41c0-b425-84f240a99e4a_1481x850.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!-hmy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9afe46f8-e620-41c0-b425-84f240a99e4a_1481x850.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-hmy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9afe46f8-e620-41c0-b425-84f240a99e4a_1481x850.jpeg" width="728" height="418" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9afe46f8-e620-41c0-b425-84f240a99e4a_1481x850.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:836,&quot;width&quot;:1456,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:1108687,&quot;alt&quot;:&quot;A classic image of a Linux distro vendor testing patches on non-paying users.&quot;,&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="A classic image of a Linux distro vendor testing patches on non-paying users." title="A classic image of a Linux distro vendor testing patches on non-paying users." srcset="https://substackcdn.com/image/fetch/$s_!-hmy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9afe46f8-e620-41c0-b425-84f240a99e4a_1481x850.jpeg 424w, https://substackcdn.com/image/fetch/$s_!-hmy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9afe46f8-e620-41c0-b425-84f240a99e4a_1481x850.jpeg 848w, https://substackcdn.com/image/fetch/$s_!-hmy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9afe46f8-e620-41c0-b425-84f240a99e4a_1481x850.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!-hmy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9afe46f8-e620-41c0-b425-84f240a99e4a_1481x850.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">Inspired by <em>Cleopatra Testing Poisons on Condemned Prisoners</em> (1887) by Alexandre Cabanel, oil on canvas, 162.6 cm x 287.6 cm, Royal Museum of Fine Arts Antwerp</figcaption></figure></div><p>But still, when your kernel gets a live patch it is recommended to reboot in the nearest maintenance window.</p><p>I like to use open-source solutions instead of proprietary ones. Therefore I needed something else.</p><h2>No rebooting at all?</h2><p>In some environments and threat models, most vulnerabilities in the kernel are not a big deal. If you trust your local users, expose one or a few regularly updated services, and firewall the rest of the ports, then it&#8217;s just unlikely for the attacker to succeed. He/she will need a remote exploit in the tried and tested TCP/IP stack. I believe those types of bugs are extremely rare.</p><p>In the above scenario, one may choose to keep running with the same kernel, avoiding reboots. For the record: I don&#8217;t recommend this approach.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!wna4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f48be77-e37b-4082-81db-879c4e507643_3213x2176.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wna4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f48be77-e37b-4082-81db-879c4e507643_3213x2176.jpeg 424w, https://substackcdn.com/image/fetch/$s_!wna4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f48be77-e37b-4082-81db-879c4e507643_3213x2176.jpeg 848w, https://substackcdn.com/image/fetch/$s_!wna4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f48be77-e37b-4082-81db-879c4e507643_3213x2176.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!wna4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f48be77-e37b-4082-81db-879c4e507643_3213x2176.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wna4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f48be77-e37b-4082-81db-879c4e507643_3213x2176.jpeg" width="1456" height="986" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5f48be77-e37b-4082-81db-879c4e507643_3213x2176.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:986,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2391945,&quot;alt&quot;:&quot;A classic image of a person in the lions' den. The person clearly ignores good security practices.&quot;,&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="A classic image of a person in the lions' den. The person clearly ignores good security practices." title="A classic image of a person in the lions' den. The person clearly ignores good security practices." srcset="https://substackcdn.com/image/fetch/$s_!wna4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f48be77-e37b-4082-81db-879c4e507643_3213x2176.jpeg 424w, https://substackcdn.com/image/fetch/$s_!wna4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f48be77-e37b-4082-81db-879c4e507643_3213x2176.jpeg 848w, https://substackcdn.com/image/fetch/$s_!wna4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f48be77-e37b-4082-81db-879c4e507643_3213x2176.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!wna4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f48be77-e37b-4082-81db-879c4e507643_3213x2176.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">Inspired by <em>Daniel in the Lions' Den</em> (1614-1616) by Peter Paul Rubens, oil on canvas, 224.2 cm &#215; 330.5 cm, National Gallery of Art in Washington</figcaption></figure></div><p>That said, if the only reason for ignoring established good security practices is to prevent reboots because disk encryption makes it inconvenient, then I think we should do better.</p><p>We should make reboots convenient.</p><h2>How I wish it would work</h2><p>I would like to reboot to the new kernel without entering the passphrase again. It was entered once: can&#8217;t it be reused for a new kernel?</p><p>Well, we have <a href="https://wiki.archlinux.org/title/kexec">kexec</a>. It will be <a href="https://kernelnewbies.org/Linux_2_6_13">18 this year</a>, but it is not popular amongst people. If you haven&#8217;t heard about it, it allows you to replace the currently running kernel with a new one. And it does it without involving hardware reset and executing the boot loader:</p><pre><code>$ sudo kexec -l new_kernel.img   # load kernel into memory
$ sudo kexec -e                  # boot the loaded kernel</code></pre><p>While being cool, it&#8217;s not the same as live patching. The new kernel has to boot the entire system, filesystems need to be mounted and services have to start. The disk has to be unlocked, so the passphrase is required again.</p><h4>What if it was possible to preserve the volume encryption key?</h4><p>Linux keeps the keys for disk encryption in the kernel keyring. It is a special, secure place in memory. Even the root can&#8217;t retrieve volume encryption keys from there!<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a></p><pre><code>$ sudo dmsetup table
dm_crypt-0: 0 36284416 crypt aes-xts-plain64 :64:logon:cryptsetup:edd435cc-b51a-4530-aef3-5010caeee165-d0 0 252:3 32768</code></pre><p>As you can see above, only key reference is provided. The actual key is kept securely in kernel memory.</p><p>If the user could tell kexec to keep the given key intact, then the new kernel could reuse it. The newly booted system could unlock the disk without asking for a passphrase. I imagine kexec adds a new option allowing to specify a key to preserve:</p><pre><code><code>$ sudo kexec -l new_kernel.img --keep-key=my-key-reference</code></code></pre><h4>We are not there yet</h4><p>Most of the work related to the volume key preservation feature would need to be done on the kernel side, but kexec and cryptsetup would also need to implement support. From my limited perspective, it seems doable, but I cannot estimate the amount of work required.</p><p>In the meantime, let&#8217;s try to resolve this issue using the tools we already have.</p><h2>Initramfs</h2><p>Kexec allows to pass 2 things to the new kernel:</p><ul><li><p><em>kernel command line</em> containing various options for kernel and userspace,</p></li><li><p><em>initramfs</em> which is run by a new kernel to initialize various things, including disk encryption.</p></li></ul><p>We could include a volume key in the kernel command line and later use it to set up disk encryption. It would work, but the kernel command line is visible to the users, which disqualifies this concept.</p><p>So we are left with initramfs. This is a file located typically in an unencrypted /boot partition. We definitely don&#8217;t want to store the volume key on an unencrypted disk, because it defeats the purpose of encryption.</p><p>But we don&#8217;t need to modify the original initramfs file permanently. It&#8217;s enough to modify it temporarily and pass it to kexec.</p><h2>Introducing cryptreboot</h2><p>This led me to create a tool that:</p><ol><li><p>Asks the user for a passphrase to derive the volume key.</p></li><li><p>Copies original initramfs into memory and patches it to include the volume key.</p></li><li><p>Uses kexec to load patched initramfs and kernel into memory.</p></li><li><p>Initiates standard system shutdown to stop services and unmount filesystems.</p></li><li><p>On shutdown completion, a kernel with patched initramfs is executed. This step is done by systemd.</p></li></ol><p>Here is what it looks like compared to the standard reboot. If you prefer, you can check <a href="https://www.youtube.com/watch?v=C5anmOjOhBI">the identical YouTube version</a>.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;ca422f20-1829-453c-851e-22635f5fa785&quot;,&quot;duration&quot;:null}"></div><p></p><p>As you can see, the user is still asked for a passphrase, but it&#8217;s done before reboot, not after. This way the user could execute a reboot remotely. Physical presence during startup is not required anymore.</p><p>From a security standpoint, it should be almost as secure as the standard setup:</p><ul><li><p>secrets are not persisted anywhere, everything is done in memory,</p></li><li><p>volume key touches userspace for just a brief moment; the rest of the time it stays safely in the kernel keyring.</p></li></ul><p>If you have a different perspective, please leave a comment below.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/p/rebooting-linux-with-encrypted-disk/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/rebooting-linux-with-encrypted-disk/comments"><span>Leave a comment</span></a></p><p>Cryptreboot is an easy-to-use, drop-in solution written in Ruby and released under the MIT license. You can find <a href="https://github.com/phantom-node/cryptreboot">it on GitHub</a>.</p><h2>What&#8217;s next</h2><p>Here are the features I plan to implement:</p><ul><li><p>boot loader configuration parsing; currently, cryptreboot relies on symlinks or the user to pick the kernel and initramfs,</p></li><li><p>optionally persist the volume key on an encrypted disk in a file accessible to root only; this way user accepting a slight security trade-off won&#8217;t be prompted for the passphrase at all,</p></li><li><p>integration with systemd, to make cryptreboot the default reboot handler,</p></li><li><p>deb package to make installation easier,</p></li><li><p>support for non-Debian Linux distributions.</p></li></ul><p>If you use disk encryption in Linux, please give cryptreboot a try and share your feedback!</p><p>Thank you for reading this post. If you want to stay in the loop, subscribe and <a href="https://github.com/phantom-node/cryptreboot">star the project</a> on Github, so you won&#8217;t miss new cryptreboot releases.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.pawelpokrywka.com/subscribe?"><span>Subscribe now</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>It is actually possible to get the volume key from user space, but the disk has to be unlocked with a non-default &#8212;disable-keyring cryptsetup flag.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Ledger card: was there a data leak?]]></title><description><![CDATA[How I traced the source of the potential data leak]]></description><link>https://www.pawelpokrywka.com/p/ledger-card-was-there-a-data-leak</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/ledger-card-was-there-a-data-leak</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Tue, 04 Jul 2023 12:47:03 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!RSwL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe04a86b2-6341-4912-bd42-6b36d941af8d_1154x1679.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>2023-10-23 update</h2><p>3 months ago I received an email that got my attention. I subsequently wrote a blog post about it. To make a long story short I suspected the disposable email address I used exclusively for Ledger Card leaked somehow because I got a suspicious email on that address. Later on, I found out the source of the problem lay in Baanx, Ledger's business partner.</p><p>Baanx informed the email I got had been sent by a legitimate project, which is a partner of Baanx. I neglected to check the project carefully &#8212; that&#8217;s on me!</p><p>However, it&#8217;s only one part of the issue.</p><p>The email message came unsolicited to the address which shouldn&#8217;t be used for this type of communication. There is no doubt Baanx failed to handle my email address properly. While Baanx might see it differently, I believe this specific mailing campaign reached others as well. However&#8230;</p><h4>The breach probably did not happen</h4><p>We exchanged a few messages and had video calls with Baanx. I learned a lot of details, which I won&#8217;t provide here. The common part of our conclusions is that there was a human error during the handling of marketing email campaigns.</p><p>Most importantly, Baanx said they investigated the case, fixed a software bug, and cleaned up internal processes. Here is part of the email I got from Baanx:</p><blockquote><p>There definitely were some "human error" learnings here on our internal processes that were cleaned up, so I thank you for reporting the issue. No other customers or waiting list participants contacted us, so it appears to be a single edge case - at least from reports.</p></blockquote><p>While I think they could do a bit better, I believe them.</p><p>The end of this story is positive. Something that looked like a data breach turned out to be a relatively harmless human error. That&#8217;s a relief!</p><p>A special thanks to Scott Carlson from Baanx for his diligent management of the situation.</p><h2>2023-07-05 update</h2><p>I got an <a href="https://www.reddit.com/r/ledgerwallet/comments/14qeh9w/comment/jqqo4od">official response from CL Card support</a> on Reddit. I also spoke to a person from Baanx. It seems that Anrk is Baanx&#8217;s partner. They're conducting an internal investigation. They say there was no data breach. I'm still not 100% clear why I got this email message. I will update this post when more data becomes available.</p><h2>Here comes the original text</h2><p>Do you remember how <a href="https://www.ledger.com/message-ledgers-ceo-data-leak">1 million records leaked from Ledger</a>?</p><p>Today I received this funny-looking email message:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RSwL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe04a86b2-6341-4912-bd42-6b36d941af8d_1154x1679.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RSwL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe04a86b2-6341-4912-bd42-6b36d941af8d_1154x1679.png 424w, https://substackcdn.com/image/fetch/$s_!RSwL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe04a86b2-6341-4912-bd42-6b36d941af8d_1154x1679.png 848w, https://substackcdn.com/image/fetch/$s_!RSwL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe04a86b2-6341-4912-bd42-6b36d941af8d_1154x1679.png 1272w, https://substackcdn.com/image/fetch/$s_!RSwL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe04a86b2-6341-4912-bd42-6b36d941af8d_1154x1679.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RSwL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe04a86b2-6341-4912-bd42-6b36d941af8d_1154x1679.png" width="1154" height="1679" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e04a86b2-6341-4912-bd42-6b36d941af8d_1154x1679.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1679,&quot;width&quot;:1154,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:871941,&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_!RSwL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe04a86b2-6341-4912-bd42-6b36d941af8d_1154x1679.png 424w, https://substackcdn.com/image/fetch/$s_!RSwL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe04a86b2-6341-4912-bd42-6b36d941af8d_1154x1679.png 848w, https://substackcdn.com/image/fetch/$s_!RSwL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe04a86b2-6341-4912-bd42-6b36d941af8d_1154x1679.png 1272w, https://substackcdn.com/image/fetch/$s_!RSwL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe04a86b2-6341-4912-bd42-6b36d941af8d_1154x1679.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>Seems similar to <a href="https://www.ankr.com/">Ankr</a>. But if you check it closely you will notice a typo.</p><h2>Recipient address</h2><p>&#8220;Oh, just another scam attempt&#8221; - I thought<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. However, when I checked the recipient address of this message, I found this address looks similar to the one, I use exclusively for my Ledger <a href="https://www.ledger.com/cl-card">CL card</a>.</p><p>By <em>exclusively</em> I mean, I don&#8217;t provide this address in any other place. Every time I register somewhere, I use a different, unique email address. This is my method of localizing sources of data leaks (if it was my data that leaked).</p><p>However, in this particular case, I used the same email address twice. I used it to contact CL Card support regarding the issues with my card. After closer inspection, I found that I made a typo in my email address when contacting CL Card support. To sum things up, I used:</p><ul><li><p>address #1 as a login for the CL Card system,</p></li><li><p>address #2 for contacting CL Card support<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>.</p></li></ul><p>The scam message was delivered to email address #2.</p><h2>Source of the leak</h2><p>So it seems, my address leaked from the support department and <strong>not the CL card system itself</strong>. That&#8217;s a big relief!</p><p>But still, the data we have now leads to many questions. Here are some of them:</p><ul><li><p>When the leak happened?</p></li><li><p>How many other addresses leaked?</p></li><li><p>Apart from email addresses, what other data leaked?</p></li><li><p>Was it a corrupt or careless employee?</p></li><li><p>Was it a CRM database leak?</p></li><li><p>Is CRM self-hosted by Ledger or outsourced? Maybe they outsource support entirely?</p></li><li><p>Was CRM data transferred into another system (for example for <a href="https://blog.trezor.io/ongoing-phishing-attacks-on-trezor-users-edd840b17304">marketing purposes</a>), and the leak happened there?</p></li></ul><p>I can answer the first question. I sent the first message using address #2 on 8th December 2022. <strong>So it seems the leak happened between 2022-12-08 and 2023-07-04.</strong></p><p>Also, I know <a href="https://haveibeenpwned.com/">Have I Been Pwned</a> doesn&#8217;t report my email as leaked. I checked the same day I wrote this post.</p><p>I don&#8217;t know other answers. But I wonder how many people noticed this too. If you were affected, please leave a comment below.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/p/ledger-card-was-there-a-data-leak/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/ledger-card-was-there-a-data-leak/comments"><span>Leave a comment</span></a></p><h2>My data leaked - what now?</h2><p>Unfortunately, if your data leaks, you can&#8217;t <em>leak it back</em>. It helps if you provide disposable addresses, but your main address will leak someday too. As always you need to be careful with incoming mail.</p><h2>That&#8217;s it</h2><p>Thank you for reading this short post.</p><p>For interested parties, I pasted the headers of the message and the first part of the text below. I obfuscated my email address and some other data which may potentially lead to revealing private information.</p><pre><code>Delivered-To: address-2@obfuscated.com
Received: by 2002:ab3:1c15:0:b0:238:9402:e3c6 with SMTP id u21csp4866874lth;
        Tue, 4 Jul 2023 03:00:09 -0700 (PDT)
X-Google-Smtp-Source: APBJJlGIpdLNd8AIX7qcHMNUTlkl5SwX0AIeWFPH3NbG0NmWonNOfJbO6NP308UQD2NXNBzLQOO5
X-Received: by 2002:a17:902:d501:b0:1b8:3936:7b64 with SMTP id b1-20020a170902d50100b001b839367b64mr20459348plg.1.1688464809144;
        Tue, 04 Jul 2023 03:00:09 -0700 (PDT)
ARC-Seal: i=1; a=rsa-sha256; t=1688464809; cv=none;
        d=google.com; s=arc-20160816;
        b=vppot55BTC0Nh7GIdbYXbKkW+isDHpv7D0N4QQeYwg6qUShM6SncDiiDxRpZGylRNB
         vYll7SyfV7nMe1RkojIwcsGocbl6o8FGgQVgj/sMqZ7bIJNWu0wx+mkQRYOZ5VD/j5pC
         eHPFaD4AadeMlQTtWBrqeZx6lRovpHPBrXJnFQ7BriNOfbINnuxgQgHuAnt9vP8As/Rm
         oij0SvOVoOsXW0wRWOFiWebZ+jmGRUMNWRmkLSsAGnSSAcC7x3+VStBjpg9KMDH37iLr
         WL5MJ7IvbnzmooomKMDeVwrUEKUFVEULpELb7LCF9lM6qAas9bz4XNn8rtgQ0doIfuxh
         mH8A==
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816;
        h=feedback-id:from:list-unsubscribe:subject:reply-to:mime-version
         :date:message-id:to:dkim-signature;
        bh=8yLsdori4r6GtMEEET+tAfTrVGhEDhyYgbLw94d8CEQ=;
        fh=tDNHxWpIqxviIVEMkK8CTTH5nJteJnWyRzljgxDe6kw=;
        b=Oh3dOJKFdCq5hF0TPRn4pTSwnLdpqhkQbu8ePsrpsa2942ygYm8oqKn8vo1FTIHAqC
         OGWcB6odmBIZxTnE2UktQ13Cxhn7sIWvoxJh4OpGDiF2VmQrvYXvyjrJvAb/xQnVNxYj
         b8YZIK4Q9JTKIhLllhAA2P0Tvo9jY9maEBpz4bs/oq1lFenOrDcuVFwKsJM0AFVtNsBC
         7zjsqMzyPEnVwAeTG1XDr7SgQeq84TESIG/M5j5icR2s40qktBUGIjYnkpCnH5XtW84C
         68W4+/h6Ph2g3gD+t2ze/Fbeg7jdVJfyczvXSUR4/P5YvB4dxeJPvvbq/vZsqPYFnYx6
         qKeg==
ARC-Authentication-Results: i=1; mx.google.com;
       dkim=pass header.i=@anrkprotocol.com header.s=scph0623 header.b=qQ7u47oI;
       spf=pass (google.com: domain of msprvs1=1954918sqetfe=bounces-280172@sparkpostmail.com designates 192.174.87.92 as permitted sender) smtp.mailfrom="msprvs1=1954918SQEtfE=bounces-280172@sparkpostmail.com"
Return-Path: &lt;msprvs1=1954918SQEtfE=bounces-280172@sparkpostmail.com&gt;
Received: from mta-174-87-92.smtp-out.sparkpostmail.com (mta-174-87-92.smtp-out.sparkpostmail.com. [192.174.87.92])
        by mx.google.com with ESMTPS id kb14-20020a170903338e00b001b6ae9f8bb1si12371885plb.75.2023.07.04.03.00.08
        for &lt;address-2@obfuscated.com&gt;
        (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128);
        Tue, 04 Jul 2023 03:00:09 -0700 (PDT)
Received-SPF: pass (google.com: domain of msprvs1=1954918sqetfe=bounces-280172@sparkpostmail.com designates 192.174.87.92 as permitted sender) client-ip=192.174.87.92;
Authentication-Results: mx.google.com;
       dkim=pass header.i=@anrkprotocol.com header.s=scph0623 header.b=qQ7u47oI;
       spf=pass (google.com: domain of msprvs1=1954918sqetfe=bounces-280172@sparkpostmail.com designates 192.174.87.92 as permitted sender) smtp.mailfrom="msprvs1=1954918SQEtfE=bounces-280172@sparkpostmail.com"
X-MSFBL: OWV7igvEEXPJb+7ZJrrlYsThoPLaO1ot/hRxyPVy+gU=|eyJtZXNzYWdlX2lkIjo iNjQ5Y2E1ZWRhMzY0YTExMDVjOWQiLCJzdWJhY2NvdW50X2lkIjoiMCIsImN1c3R vbWVyX2lkIjoiMjgwMTcyIiwidGVuYW50X2lkIjoic3BjIiwiciI6ImNsLWNhcmR zLmNvbS4xMi4xMi4yMDIxLnN5c3RlbUBjcnlwdG9uaXgub3JnIn0=
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=anrkprotocol.com; s=scph0623; t=1688464805; i=@anrkprotocol.com; bh=8yLsdori4r6GtMEEET+tAfTrVGhEDhyYgbLw94d8CEQ=; h=To:Message-ID:Date:Content-Type:Subject:List-Unsubscribe:From:
&#9; From:To:Cc:Subject; b=obfuscated
To: address-2@obfuscated.com
Message-ID: &lt;obfuscated@jp.mta1vrest.cc.prd.sparkpost&gt;
Date: Tue, 04 Jul 2023 10:00:05 +0000
Content-Type: multipart/alternative; boundary="_----MvvyzH+M+eka7ub4N8/3Kw===_61/D9-38980-5ADE3A46"
MIME-Version: 1.0
Reply-To: anrk@anrkprotocol.com
Subject: On-chain card spending, self-custody and beyond! &#128640;
X-Campaign-ID: 7184983
List-Unsubscribe: &lt;https://links.iterable.com/e/encryptedUnsubscribe?obfuscated&gt;,&lt;mailto:unsubscribe+obfuscated@unsubscribe.iterable.com&gt;
From: anrkprotocol &lt;anrk@anrkprotocol.com&gt;
X-Message-ID: obfuscated
X-Feedback-ID: obfuscated:iterable
Feedback-ID: obfuscated:iterable

--_----MvvyzH+M+eka7ub4N8/3Kw===_61/D9-38980-5ADE3A46
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset="UTF-8"

Connect to your Web3 wallet to an open source wallet network



 &lt;https://anrkprotocol.com/on-chain-transactions/&gt;=E2=80=8A

Connect Your Metamask, Ledger, Phamtom or Web3 Wallet To The X Card And Spe=
nd=20
On-Chain!


We are building anrkprotocol - an open-source wallet network that allows yo=
u=20
to connect your Web3 wallet to our Mastercard, enabling on-chain spending a=
nd=20
giving you complete custody over your assets. With anrkprotocol you keep=20
control of your funds at all times and you eliminate the need for trust in=
=20
custodians or financial institutions. Join Waitlist!=20
&lt;https://anrkprotocol.com/on-chain-transactions/&gt;=20</code></pre><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>Maybe Anrk is legitimate, I haven&#8217;t checked. However, sending marketing messages to email addresses obtained without user consent doesn&#8217;t look fair.</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>To be 100% accurate, I used address #2 by mistake, after realizing it I continued to use address #1 for contacting support. But the most important is I sent a few messages from address #2 and got replies to this address.</p></div></div>]]></content:encoded></item><item><title><![CDATA[How to deanonymize smart contract author]]></title><description><![CDATA[Doxxing Solidity developer for fun and profit]]></description><link>https://www.pawelpokrywka.com/p/how-to-deanonymize-smart-contract-author</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/how-to-deanonymize-smart-contract-author</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Sat, 07 Aug 2021 22:19:35 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!TOcW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fed6797fe-4c48-45a5-adb2-015eeb64659e_2000x1335.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_!TOcW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fed6797fe-4c48-45a5-adb2-015eeb64659e_2000x1335.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TOcW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fed6797fe-4c48-45a5-adb2-015eeb64659e_2000x1335.jpeg 424w, https://substackcdn.com/image/fetch/$s_!TOcW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fed6797fe-4c48-45a5-adb2-015eeb64659e_2000x1335.jpeg 848w, https://substackcdn.com/image/fetch/$s_!TOcW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fed6797fe-4c48-45a5-adb2-015eeb64659e_2000x1335.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!TOcW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fed6797fe-4c48-45a5-adb2-015eeb64659e_2000x1335.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TOcW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fed6797fe-4c48-45a5-adb2-015eeb64659e_2000x1335.jpeg" width="1456" height="972" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/ed6797fe-4c48-45a5-adb2-015eeb64659e_2000x1335.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:972,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2728027,&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-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!TOcW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fed6797fe-4c48-45a5-adb2-015eeb64659e_2000x1335.jpeg 424w, https://substackcdn.com/image/fetch/$s_!TOcW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fed6797fe-4c48-45a5-adb2-015eeb64659e_2000x1335.jpeg 848w, https://substackcdn.com/image/fetch/$s_!TOcW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fed6797fe-4c48-45a5-adb2-015eeb64659e_2000x1335.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!TOcW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fed6797fe-4c48-45a5-adb2-015eeb64659e_2000x1335.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">Attribution: Freepik</figcaption></figure></div><p>I came across an article whose author described the process of creating a smart contract and deploying it to the Ethereum mainnet. He also published the source code.</p><p>I didn&#8217;t have experience with smart contract development and blockchain analysis, but after reading the article I began to wonder:</p><blockquote><p>Is it possible to use this public information in order to find Ethereum address of this person?</p></blockquote><p>It would be fun to see how much money he holds ;-)</p><h2>TL;DR</h2><p>Given the smart contract&#8217;s source code it is possible to find it using blockchain analysis. I&#8217;ve used Google Big Query to search for specific function signatures. In the end, I give some hints on how to avoid deanonymization.</p><h2>Disclaimer</h2><p>I don&#8217;t want to disclose the personal details of the smart contract&#8217;s author. Let&#8217;s just call him John. I contacted John and he approved this article before publication.</p><p>Don&#8217;t use the knowledge you got here (or anywhere else) to hurt others.</p><h2>Challenge</h2><p>I can define a challenge as entering unknown territory with a clear goal in mind. In the area of computer security and privacy, this feeling boosts my creativity in problem-solving, forces me to learn new things fast, perform reverse engineering and try to get into the state of mind of a given system creator.</p><p>It gives me the satisfaction of a deep understanding of the system and a thrill, both at the same time. With that motivation, I started researching the problem.</p><h2>Examine the code</h2><p>My first attempt was to check for any hardcoded Ethereum addresses in John&#8217;s code. The common practice is to set contract owner address dynamically during deployment instead of hardcoding it, and it was also the case here. But it costs nothing to check.</p><pre><code>constructor() {
  <strong>owner = msg.sender</strong>; // set owner to address of caller
}</code></pre><h2>Compare bytecode</h2><p>Every smart contract&#8217;s bytecode is available publicly on the blockchain. Therefore my second attempt was to compile John&#8217;s code and then use some tool to compare it with every smart contract on Ethereum.</p><p>I started by finding a way to query the blockchain for the information I wanted. It appears you can&#8217;t search by bytecode in popular blockchain explorers such as <a href="https://etherscan.io/">Etherscan</a> or <a href="https://etherchain.org/">Etherchain</a>. Googling bytecode is not a good idea, because Google doesn&#8217;t allow such large queries.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!sCef!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Feb7da7a0-3fc4-447c-9004-4ca65a282a0f_1190x426.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sCef!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Feb7da7a0-3fc4-447c-9004-4ca65a282a0f_1190x426.png 424w, https://substackcdn.com/image/fetch/$s_!sCef!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Feb7da7a0-3fc4-447c-9004-4ca65a282a0f_1190x426.png 848w, https://substackcdn.com/image/fetch/$s_!sCef!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Feb7da7a0-3fc4-447c-9004-4ca65a282a0f_1190x426.png 1272w, https://substackcdn.com/image/fetch/$s_!sCef!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Feb7da7a0-3fc4-447c-9004-4ca65a282a0f_1190x426.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!sCef!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Feb7da7a0-3fc4-447c-9004-4ca65a282a0f_1190x426.png" width="1190" height="426" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/eb7da7a0-3fc4-447c-9004-4ca65a282a0f_1190x426.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:426,&quot;width&quot;:1190,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:128278,&quot;alt&quot;:&quot;Google error message saying: The requested URL /... is too large to process. That's all we know.&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="Google error message saying: The requested URL /... is too large to process. That's all we know." title="Google error message saying: The requested URL /... is too large to process. That's all we know." srcset="https://substackcdn.com/image/fetch/$s_!sCef!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Feb7da7a0-3fc4-447c-9004-4ca65a282a0f_1190x426.png 424w, https://substackcdn.com/image/fetch/$s_!sCef!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Feb7da7a0-3fc4-447c-9004-4ca65a282a0f_1190x426.png 848w, https://substackcdn.com/image/fetch/$s_!sCef!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Feb7da7a0-3fc4-447c-9004-4ca65a282a0f_1190x426.png 1272w, https://substackcdn.com/image/fetch/$s_!sCef!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Feb7da7a0-3fc4-447c-9004-4ca65a282a0f_1190x426.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">If your query is too long, you can <a href="https://www.youtube.com/watch?v=v2FMqtC1x9Y">break Google</a>.</figcaption></figure></div><h2>BigQuery</h2><p>However, I&#8217;ve found Google created a BigQuery public dataset for Ethereum and updates it daily. From the user&#8217;s perspective BigQuery is just a large SQL database similar to Postgresql or Mysql used routinely by developers. Its usage is free (within limits) and there is a web-based query tool called <a href="https://console.cloud.google.com/bigquery">BigQuery Console</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_!LM1M!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd24af2a4-00f5-4155-b532-21d6ca8d6884_2962x1390.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LM1M!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd24af2a4-00f5-4155-b532-21d6ca8d6884_2962x1390.png 424w, https://substackcdn.com/image/fetch/$s_!LM1M!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd24af2a4-00f5-4155-b532-21d6ca8d6884_2962x1390.png 848w, https://substackcdn.com/image/fetch/$s_!LM1M!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd24af2a4-00f5-4155-b532-21d6ca8d6884_2962x1390.png 1272w, https://substackcdn.com/image/fetch/$s_!LM1M!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd24af2a4-00f5-4155-b532-21d6ca8d6884_2962x1390.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LM1M!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd24af2a4-00f5-4155-b532-21d6ca8d6884_2962x1390.png" width="1456" height="683" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/d24af2a4-00f5-4155-b532-21d6ca8d6884_2962x1390.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:683,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:394468,&quot;alt&quot;:&quot;Google Cloud Platform screenshot&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="Google Cloud Platform screenshot" title="Google Cloud Platform screenshot" srcset="https://substackcdn.com/image/fetch/$s_!LM1M!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd24af2a4-00f5-4155-b532-21d6ca8d6884_2962x1390.png 424w, https://substackcdn.com/image/fetch/$s_!LM1M!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd24af2a4-00f5-4155-b532-21d6ca8d6884_2962x1390.png 848w, https://substackcdn.com/image/fetch/$s_!LM1M!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd24af2a4-00f5-4155-b532-21d6ca8d6884_2962x1390.png 1272w, https://substackcdn.com/image/fetch/$s_!LM1M!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd24af2a4-00f5-4155-b532-21d6ca8d6884_2962x1390.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">BigQuery console showing schema of the Ethereum public dataset.</figcaption></figure></div><p>I found it perfect for the job. Looking at schema, there is a <code>contracts</code> table and it contains a <code>bytecode</code> column. To see bytecode related to each one of random 10 transactions made on the last day, I could execute the following SQL statement:</p><pre><code>SELECT bytecode
FROM
  bigquery-public-data.crypto_ethereum.contracts
WHERE
  block_timestamp &gt;
  CAST(DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY) AS TIMESTAMP)
LIMIT 10</code></pre><blockquote><p>Timestamp constrain is optional but recommended. This table is huge. Restricting time interval prevents Google from processing a lot of non-relevant data. I skipped this constrain on my first trials which quickly ate all of my quota.</p></blockquote><p>Knowing the exact bytecode and blog post&#8217;s publication date I could find the contract address by executing the following statement:</p><pre><code>SELECT address
FROM
  bigquery-public-data.crypto_ethereum.contracts
WHERE
  block_timestamp &gt; <strong>{month before post publication date}</strong>
AND
  bytecode = <strong>{compiled bytecode of smart contract}</strong>
LIMIT 10</code></pre><blockquote><p><code>address</code> is a column containing contract address.</p></blockquote><p>Now I only needed the bytecode of this smart contract.</p><h2>Compilation</h2><p>I decided to use a free web-based IDE called <a href="https://remix.ethereum.org/">Remix</a> because it doesn&#8217;t require setting up the development environment.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cbT7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1c4276d-02f4-40a8-866e-ecb45068c9a8_1876x858.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cbT7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1c4276d-02f4-40a8-866e-ecb45068c9a8_1876x858.png 424w, https://substackcdn.com/image/fetch/$s_!cbT7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1c4276d-02f4-40a8-866e-ecb45068c9a8_1876x858.png 848w, https://substackcdn.com/image/fetch/$s_!cbT7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1c4276d-02f4-40a8-866e-ecb45068c9a8_1876x858.png 1272w, https://substackcdn.com/image/fetch/$s_!cbT7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1c4276d-02f4-40a8-866e-ecb45068c9a8_1876x858.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cbT7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1c4276d-02f4-40a8-866e-ecb45068c9a8_1876x858.png" width="1456" height="666" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/d1c4276d-02f4-40a8-866e-ecb45068c9a8_1876x858.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:666,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:200555,&quot;alt&quot;:&quot;Remix IDE screenshot&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="Remix IDE screenshot" title="Remix IDE screenshot" srcset="https://substackcdn.com/image/fetch/$s_!cbT7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1c4276d-02f4-40a8-866e-ecb45068c9a8_1876x858.png 424w, https://substackcdn.com/image/fetch/$s_!cbT7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1c4276d-02f4-40a8-866e-ecb45068c9a8_1876x858.png 848w, https://substackcdn.com/image/fetch/$s_!cbT7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1c4276d-02f4-40a8-866e-ecb45068c9a8_1876x858.png 1272w, https://substackcdn.com/image/fetch/$s_!cbT7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1c4276d-02f4-40a8-866e-ecb45068c9a8_1876x858.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">Remix IDE with Solidity editor opened.</figcaption></figure></div><p>I&#8217;ve created a <code>.sol</code> file in Remix and pasted the code of the contract. Then I opened <strong>Solidity compiler</strong> tab and set Solidity version to match the version specified in the code. Each file has the following line at the top:</p><pre><code>pragma solidity SPEC</code></pre><blockquote><p>In place of <code>SPEC</code> there is a version specification.</p></blockquote><p>Using the same compiler is important because every version may produce different bytecode and I was trying to generate an exact copy of the bytecode deployed by John.</p><p>The code was compiled successfully, and I was able to get bytecode.</p><h2>Blockchain search</h2><p>To my surprise, SQL query mentioned above didn&#8217;t return any values. I modified <code>WHERE</code> statement to use SQL <code>LIKE</code>, which allows finding partial matches:</p><pre><code><code>bytecode LIKE '%</code><strong><code>{part of compiled bytecode}</code></strong><code>%'</code></code></pre><p>Then I experimented with passing different parts of bytecode. However, there were no meaningful results neither.</p><h2>Optimization</h2><p>Then it hit me. Solidity compiler <a href="https://docs.soliditylang.org/en/latest/internals/optimizer.html">optimizes</a> code, to reduce code size (contract deployment is cheaper) and execution cost (users have to pay less for gas). </p><p>Optimization level is controller by a parameter specified during compilation.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AsYB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F665e1856-4a64-47a8-bd5a-062910497129_1627x958.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AsYB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F665e1856-4a64-47a8-bd5a-062910497129_1627x958.png 424w, https://substackcdn.com/image/fetch/$s_!AsYB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F665e1856-4a64-47a8-bd5a-062910497129_1627x958.png 848w, https://substackcdn.com/image/fetch/$s_!AsYB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F665e1856-4a64-47a8-bd5a-062910497129_1627x958.png 1272w, https://substackcdn.com/image/fetch/$s_!AsYB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F665e1856-4a64-47a8-bd5a-062910497129_1627x958.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AsYB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F665e1856-4a64-47a8-bd5a-062910497129_1627x958.png" width="1456" height="857" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/665e1856-4a64-47a8-bd5a-062910497129_1627x958.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:857,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:126239,&quot;alt&quot;:&quot;Screenshot showing Remix compiler configuration&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="Screenshot showing Remix compiler configuration" title="Screenshot showing Remix compiler configuration" srcset="https://substackcdn.com/image/fetch/$s_!AsYB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F665e1856-4a64-47a8-bd5a-062910497129_1627x958.png 424w, https://substackcdn.com/image/fetch/$s_!AsYB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F665e1856-4a64-47a8-bd5a-062910497129_1627x958.png 848w, https://substackcdn.com/image/fetch/$s_!AsYB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F665e1856-4a64-47a8-bd5a-062910497129_1627x958.png 1272w, https://substackcdn.com/image/fetch/$s_!AsYB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F665e1856-4a64-47a8-bd5a-062910497129_1627x958.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">Remix compiler configuration showing optimization parameter.</figcaption></figure></div><p>This parameter is a number from 0 to 4294967295. I suspect each number produces a slightly different bytecode.</p><p>I didn&#8217;t know what optimization parameter had been used. I didn&#8217;t want to compile the code and execute SQL queries against the blockchain 4294967296 times. After trying to guess common values few times, I gave up. <strong>There has to be another way.</strong></p><h2>Function signatures</h2><p>I dug into Ethereum dataset. One of the columns in SQL schema got my attention: <code>function_sighashes</code>.</p><p>After some research, I found that, in order for the smart contract&#8217;s functions to be called by users or other smart contracts, there needs to be some kind of mapping between the function name and an address in bytecode where the function logic resides.</p><p>But what if someone likes to use <code>veryLongFunctionNames</code>? Does it mean the name of the function has to be embedded in smart contract bytecode, increasing blockchain size and eating precious gas?</p><p>This may be one of the reasons Ethereum uses the concept called <em>function signature</em>. The function signature is created by hashing the prototype string and discarding anything after the first 4 bytes. The hash algorithm used is Keccak256 which you may know from <a href="https://en.wikipedia.org/wiki/SHA-3">SHA-3</a>. However SHA-3 uses a version with slightly different parameters, therefore the output is different.</p><blockquote><p>For example, to get function signature for a function <code>myFunction</code> which receives 1 argument of type <code>uint256</code>:</p><ol><li><p>Prepare function prototype, let&#8217;s say <code>myFunction(uint256)</code>.</p></li><li><p>Find an online Keccak256 generator (for example <a href="https://hashtools.org/keccak_256.html">this one</a>) and paste the function prototype there.</p></li><li><p>You will get <code>50628c969c386d878aac8a993492e42110c19ba346d377fec055d2d56124b695</code>.</p></li><li><p>Remove anything after the first 4 bytes and add <code>0x</code> prefix to make it clear we are dealing with a hexadecimal number.</p></li><li><p>The result is <code>0x50628c96</code>.</p></li></ol></blockquote><p>Those four bytes along with the mapping method will show you where the function resides in a compiled contract (it is a simplification to make you grasp the general idea, check limitations below).</p><h2>Back to BigQuery</h2><p>I&#8217;ve calculated function signatures for every function of John&#8217;s smart contract. Now I was armed with the information needed to find the address of this contract:</p><pre><code>SELECT address
FROM
  bigquery-public-data.crypto_ethereum.contracts
WHERE
  '<strong>0xf68deb93</strong>' IN UNNEST(function_sighashes)
AND
  block_timestamp &gt; <strong>{month before post publication date}</strong>
LIMIT 10</code></pre><blockquote><p>This simplified example contains only one function signature (<code>0xf68deb93</code>). To find John&#8217;s contract I had to add more of them to the <code>WHERE</code> condition.</p></blockquote><p>Yay! The query gave exactly one result. <strong>Is it the contract address I was looking for?</strong></p><h2>Verification</h2><p>I used <a href="https://etherscan.io/">Etherscan</a> to get more information about this address. Etherscan confirmed this is a contract address and allowed me to decompile it using a build-in online tool. The resulting code looked similar to the original code published by John. <strong>I found it!</strong></p><h2>Limitations</h2><p>Function signatures are extracted from contract bytecode using heuristics. In case the contract&#8217;s bytecode doesn&#8217;t follow conventions, it may be hard or even impossible to obtain function signatures. Therefore some function signatures may not be available in the BigQuery dataset.</p><blockquote><p>For more details, check out <a href="https://cloud.google.com/blog/products/data-analytics/ethereum-bigquery-how-we-built-dataset">how the Ethereum dataset was built</a> and <a href="https://ethereum.stackexchange.com/a/60062">how those heuristics work</a>.</p></blockquote><p>This is how I understand why some function signatures are not available in the dataset. If you have a more accurate explanation, please share it below.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/p/how-to-deanonymize-smart-contract-author/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-to-deanonymize-smart-contract-author/comments"><span>Leave a comment</span></a></p><h2>Extracting information</h2><p>Let&#8217;s see what information I can get knowing the contract address.</p><p>The most important data is the Ethereum account address which interacted with the contract. I was able to found it easily in the list of the contract&#8217;s transactions on Etherscan. There was only one, therefore I could safely assume it was the address of John.</p><p>The rest is simple: when you have someone&#8217;s address you can get his entire transactions history and balance. <strong>It&#8217;s like a bank statement, but public.</strong></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pLEU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F1639a295-3b02-4ab0-a8dd-77869848d878_2408x1904.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pLEU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F1639a295-3b02-4ab0-a8dd-77869848d878_2408x1904.png 424w, https://substackcdn.com/image/fetch/$s_!pLEU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F1639a295-3b02-4ab0-a8dd-77869848d878_2408x1904.png 848w, https://substackcdn.com/image/fetch/$s_!pLEU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F1639a295-3b02-4ab0-a8dd-77869848d878_2408x1904.png 1272w, https://substackcdn.com/image/fetch/$s_!pLEU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F1639a295-3b02-4ab0-a8dd-77869848d878_2408x1904.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pLEU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F1639a295-3b02-4ab0-a8dd-77869848d878_2408x1904.png" width="1456" height="1151" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/1639a295-3b02-4ab0-a8dd-77869848d878_2408x1904.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1151,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:605660,&quot;alt&quot;:&quot;Screenshot of Etherscan website showing details of Ethereum address&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="Screenshot of Etherscan website showing details of Ethereum address" title="Screenshot of Etherscan website showing details of Ethereum address" srcset="https://substackcdn.com/image/fetch/$s_!pLEU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F1639a295-3b02-4ab0-a8dd-77869848d878_2408x1904.png 424w, https://substackcdn.com/image/fetch/$s_!pLEU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F1639a295-3b02-4ab0-a8dd-77869848d878_2408x1904.png 848w, https://substackcdn.com/image/fetch/$s_!pLEU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F1639a295-3b02-4ab0-a8dd-77869848d878_2408x1904.png 1272w, https://substackcdn.com/image/fetch/$s_!pLEU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F1639a295-3b02-4ab0-a8dd-77869848d878_2408x1904.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">Etherscan showing details of Ethereum address. This particular one belongs to Vitalik Butterin who <a href="https://twitter.com/VitalikButerin/status/1050126908589887488">made it publicly available</a>.</figcaption></figure></div><p>Now I could contact John and tell him the exact amount of Ether he holds. His reaction was worth the time I&#8217;ve put into this task :)</p><blockquote><p>If you like this story then I can send you my future articles right after publication. This is a privacy-oriented blog - there will be <strong>no spam</strong> and you can <strong>unsubscribe anytime</strong>.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.pawelpokrywka.com/subscribe?"><span>Subscribe now</span></a></p></blockquote><h2>How to avoid being doxxed on Ethereum?</h2><p>Ethereum transactions are pseudonymous, which means the user performing them is private as long as his address (pseudonym) can&#8217;t be linked to his real identity. John runs a blog, therefore his identity is public. Smart contract source code is also public and linked to the blog. <strong>Therefore anyone who is able to perform the steps I described above, could deanonymize him.</strong></p><p>If you want to deploy a contract which source code is public and associated with you, here are some ideas to make connecting your real identity with your main Ethereum address harder:</p><ul><li><p><strong>obfuscate source code before deployment</strong> (<a href="https://github.com/xf97/BiAn">example obfuscator</a>) - this makes finding the smart contract more difficult,</p></li><li><p><strong>separate contract deployment from contract usage</strong>: deploy contract from address specially generated for this purpose and don&#8217;t use it for anything else - others could see who created the contract, but won&#8217;t be able to easily connect this information with your transactions (which in most cases reveal your financial situation), especially if you follow next advice,</p></li><li><p>if the contract is meant to be used by other people - don&#8217;t be first to use it, wait for others to transact - <strong>hide in a crowd</strong>; break up a transaction into many smaller ones and execute them at irregular intervals from many different, unrelated accounts, preferable with different transaction history; this method is similar to techniques employed by anonymity mixers such as <a href="https://tornado.cash/">Tornado.cash</a>,</p></li><li><p><strong>make sure all addresses you use (for deployment and transactions) are anonymously funded and not linked to your main account.</strong></p><blockquote><p>I plan to write dedicated article about maintaining anonymity on Ethereum blockchain. I will link it here. Subscribe to get it right after publication.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.pawelpokrywka.com/subscribe?"><span>Subscribe now</span></a></p></blockquote></li></ul><h2>Final word</h2><p><strong>Thank you</strong> for reading this little deanonymization story, <strong>I hope you enjoyed it :)</strong></p><p>In case you would like to add something, have a question, or found an error, please comment down below.</p><p>Privacy is important. If you think this article will be useful to others, please spread the word.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.pawelpokrywka.com/p/how-to-deanonymize-smart-contract-author?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/how-to-deanonymize-smart-contract-author?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p>]]></content:encoded></item><item><title><![CDATA[Freeconet: intercepting VoIP calls]]></title><description><![CDATA[Translated by AI to English, originally published on 11 January 2011 in Polish.]]></description><link>https://www.pawelpokrywka.com/p/freeconet-intercepting-voip-calls</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/freeconet-intercepting-voip-calls</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Tue, 11 Jan 2011 11:00:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!rjrC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f422f3c-b8eb-47ea-a7b5-04a000ddf5a7_2041x1134.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Translated by AI to English, originally published on 11 January 2011 in Polish.</em></p><h2><strong>2011-01-18 update</strong></h2><p>I received information that the vulnerability had already been known earlier, in July 2008. The issue was discovered by J&#243;zef and described in <a href="https://www.freeconet.pl/forum/viewtopic.php?t=2888">this forum thread</a>.</p><h2><strong>2011-01-12 update</strong></h2><p>On January 12, 2011, Freeconet's management issued an official statement regarding the vulnerability described in this article.</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">Freeconet statement</div><div class="file-embed-details-h2">64.3KB &#8729; PDF file</div></div><a class="file-embed-button wide" href="https://www.pawelpokrywka.com/api/v1/file/22da4a2a-4f5e-46c1-860c-9e4378c84065.pdf"><span class="file-embed-button-text">Download</span></a></div><a class="file-embed-button narrow" href="https://www.pawelpokrywka.com/api/v1/file/22da4a2a-4f5e-46c1-860c-9e4378c84065.pdf"><span class="file-embed-button-text">Download</span></a></div></div><p>The platform owners reacted immediately&#8212;on the day the article was published, the &#8220;Add external operator&#8221; feature was disabled.</p><h2><strong>Introduction</strong></h2><p><a href="https://www.freeconet.pl">Freeconet</a> is a VoIP platform that offers a range of internet telephony services to its users.<br>At the end of September 2010, I discovered a vulnerability that allows an attacker to intercept calls from Freeconet users to numbers chosen by the attacker.<br>The attacker has full control over the calls: they can terminate, record, join, and manipulate them in any way.<br>To demonstrate some of the possible attacks, I created the <a href="https://github.com/pepawel/voiprox">VOIPROX</a> tool (see below).</p><p>Immediately after discovering the vulnerability, I passed detailed information to the platform&#8217;s owner.<br>Unfortunately, despite assurances from Freeconet, the vulnerability was not removed.<br>That&#8217;s why I&#8217;ve decided to publish full details about the issue.<br>I believe in the magic of Full Disclosure&#8212;the vulnerability will be eliminated, and Freeconet users will soon be safe.</p><p>However, after the fix, the platform&#8217;s owner should be urged to investigate whether such abuse has already occurred.<br>If so, the affected individuals&#8212;as well as law enforcement agencies, if necessary&#8212;should be notified.<br>To ensure the credibility of such an investigation, it should be conducted by an external, trusted body, e.g., the <a href="https://www.uke.gov.pl">Urz&#261;d Komunikacji Elektronicznej</a>.</p><p>This matter is also important to me personally, as I use this otherwise innovative and probably the most technologically advanced VoIP platform in Poland.</p><h2><strong>The vulnerability</strong></h2><p>The platform allows you to assign additional phone numbers to a VoIP account&#8212;numbers from external, non-Freeconet operators.<br>The idea is to increase user convenience while reducing call costs.</p><p>Let&#8217;s assume users A and B are registered on Freeconet.<br>User A also has a number from a traditional operator and can associate it with their account.<br>If user B dials that number, the platform sets up an internal network connection, so the call is free.<br>If the number wasn&#8217;t registered, the platform would use traditional telephony, which would result in charges for user B.</p><p>Note: It&#8217;s user A&#8217;s VoIP terminal that rings, not the phone connected to the traditional landline.<br>This issue is typically solved by using a hybrid gateway or a VoIP phone that can handle both IP and traditional line connections.<br>That way, a single device handles both types of calls.</p><p>The external operator definition feature is available in the Freeconet control panel under Configuration &#187; Operators &#187; External.<br>Clicking &#8220;Add external operator&#8221; opens a form where the number and area code are entered in separate fields.<br>Clicking &#8220;Add&#8221; links the number to the account&#8212;provided the number format is correct and it&#8217;s not already linked to another account.</p><p>There might be other validation checks, but it&#8217;s clear a crucial one is missing: verifying the user&#8217;s right to the entered number.<br>An attacker can enter any number&#8212;not owned by them.<br>From then on, <strong>all calls to that number</strong> are no longer routed to the actual owner&#8212;they go directly to the attacker!</p><h2><strong>Scope of the threat</strong></h2><p>Freeconet has between <a href="http://www.freeconet.pl/img/stuff/napisali_o_nas/artykul_263_top_produkt_pomorza.pdf">several thousand</a> and <a href="https://biznesmax.pl/swiat-telekomunikacji-wyroznil-call-ex/">several hundred thousand subscribers</a>, depending on the source.</p><p>I believe the vulnerability has existed since Freeconet launched in <a href="http://www.freeconet.pl/pl/o-firmie">September 2006</a>.<br>So, for over four years, users have been vulnerable to attacks like those described below.</p><h2><strong>Attack scenarios</strong></h2><h4><strong>Denial of Service (DoS)</strong></h4><p>The simplest attack type.<br>The attacker doesn&#8217;t answer calls, hangs up, or simulates a busy line.<br>This effectively blocks access to the targeted number.</p><h4><strong>Eavesdropping</strong></h4><p>As a call is initiated, the attacker starts a parallel call to the same number using traditional telephony (via or outside of Freeconet).<br>Once the recipient answers, the attacker connects the audio streams of both calls and records them.</p><p>The attack is harder to detect if the attacker hides their number or (more difficultly) spoofs the caller&#8217;s number.<br>This way, the callee won&#8217;t notice that the call came from a different number.</p><p>The attacker can then use fragments of the recorded call to construct misleading or self-serving statements&#8212;for example, to carry out the next attack.</p><h4><strong>Call modification</strong></h4><p>This builds upon the eavesdropping attack.<br>The attacker can manipulate the call in real-time&#8212;altering pitch, adding echo, or even injecting their own speech.<br>More advanced versions may target interactive phone systems (IVRs), such as banking lines.</p><p>For example, after the victim passes authentication via phone, the attacker disconnects them and continues the call impersonating the user.</p><h2><strong>How to protect yourself?</strong></h2><p>Until the vulnerability is fixed, users can protect themselves by changing call routing rules to <strong>avoid internal Freeconet calls</strong>.<br>This will increase call costs for calls to other Freeconet users but ensures your calls aren't intercepted.</p><p>To do this, go to Configuration &#187; Calling &#187; Routing and set external routing rules for all calls.</p><h2><strong>VOIPROX tool</strong></h2><p>To demonstrate the vulnerability, I created a tool called VOIPROX (from VoIP proxy&#8212;or, for fun, VoIP rocks!).</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rjrC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f422f3c-b8eb-47ea-a7b5-04a000ddf5a7_2041x1134.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rjrC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f422f3c-b8eb-47ea-a7b5-04a000ddf5a7_2041x1134.png 424w, https://substackcdn.com/image/fetch/$s_!rjrC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f422f3c-b8eb-47ea-a7b5-04a000ddf5a7_2041x1134.png 848w, https://substackcdn.com/image/fetch/$s_!rjrC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f422f3c-b8eb-47ea-a7b5-04a000ddf5a7_2041x1134.png 1272w, https://substackcdn.com/image/fetch/$s_!rjrC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f422f3c-b8eb-47ea-a7b5-04a000ddf5a7_2041x1134.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rjrC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f422f3c-b8eb-47ea-a7b5-04a000ddf5a7_2041x1134.png" width="1456" height="809" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4f422f3c-b8eb-47ea-a7b5-04a000ddf5a7_2041x1134.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:809,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:93830,&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://blog.pawelpokrywka.com/i/160436037?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f422f3c-b8eb-47ea-a7b5-04a000ddf5a7_2041x1134.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_!rjrC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f422f3c-b8eb-47ea-a7b5-04a000ddf5a7_2041x1134.png 424w, https://substackcdn.com/image/fetch/$s_!rjrC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f422f3c-b8eb-47ea-a7b5-04a000ddf5a7_2041x1134.png 848w, https://substackcdn.com/image/fetch/$s_!rjrC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f422f3c-b8eb-47ea-a7b5-04a000ddf5a7_2041x1134.png 1272w, https://substackcdn.com/image/fetch/$s_!rjrC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4f422f3c-b8eb-47ea-a7b5-04a000ddf5a7_2041x1134.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 tool logs into two SIP accounts&#8212;an input and an output one. It works similarly to a proxy. Incoming calls to the number(s) assigned to the input account (hereafter referred to as intercepted numbers) are received and forwarded to the real recipient via the output account. The incoming call is only answered once the person being called picks up. This prevents a suspicious pause that could alert the caller.</p><p>Each intercepted call is automatically saved to a separate WAV file. While the call is ongoing, the user can interact with it in real time via a simple console.</p><p>VOIPROX was written in Python. I chose this language because the VoIP library I selected is only available for C and Python.</p><p>VOIPROX was written and tested on Linux. However, I see no reason it wouldn't work on other operating systems supported by Python and PJSIP.</p><p>To test the tool conveniently, you'll need a separate VoIP terminal, gateway, or softphone (e.g., the web softphone provided by Freeconet). Note: the softphone must run on a different machine than VOIPROX. VOIPROX requires access to a sound card (even if it doesn&#8217;t use it) and cannot share it with other software.</p><h4><strong>Dependencies</strong></h4><p>Install all dependencies needed to compile PJSIP. You can find detailed instructions <a href="http://trac.pjsip.org/repos/wiki/Getting-Started/Autoconf#Requirements">here</a>. Pay special attention to the ALSA libraries. If they are not detected, VOIPROX will not support local audio.</p><p>Download the PJSIP library (I tested version 1.8.10):</p><pre><code><code>$ wget http://www.pjsip.org/release/1.8.10/pjproject-1.8.10.tar.bz2</code></code></pre><p>Unpack the archive and compile the library without installing it:</p><pre><code><code>$ tar jxf pjproject-1.8.10.tar.bz2
$ cd pjproject-1.8.10
$ ./configure
$ make dep
$ make</code></code></pre><p>Install Python and the necessary packages to compile extensions. You can find all the required information <a href="http://trac.pjsip.org/repos/wiki/Python_SIP/Build_Install#Requirements">here</a>.</p><p>Compile and install the PJSIP extension for Python:</p><pre><code><code>$ cd pjsip-apps/src/python
$ sudo make</code></code></pre><p>Check installation:</p><pre><code><code>$ python -c 'import pjsua' &amp;&amp; echo 'OK'</code></code></pre><h4>VoIP accounts</h4><p>You will need three SIP accounts:</p><ul><li><p><strong>Input account:</strong> Receives calls to one or more intercepted numbers.</p></li><li><p><strong>Output account:</strong> Used by VOIPROX to make calls to actual recipients.</p></li><li><p><strong>Test account:</strong> Used for testing. If you already have a Freeconet account, you can skip creating this one.</p></li></ul><p>Although in theory, the output account can be registered with any VoIP provider, VOIPROX currently supports only Freeconet accounts. You can create all accounts using the Freeconet <a href="https://wizard.freeconet.pl/">registration form</a>.</p><p>Assign a phone number, provide your details, email, and login for each account. You can use the same email for all. To distinguish accounts, include the type in the login (e.g., -in, -out, -test).</p><h4>Account configuration</h4><p>Log in using the credentials sent to your email and configure:</p><ul><li><p><strong>Output account:</strong></p><ul><li><p>Top up your balance: Payments &#187; Top Up.</p></li><li><p>Deactivate Freeconet internal calls (see "How to Protect Yourself").</p></li><li><p>Optionally, hide caller ID: Configuration &#187; Users &#187; Your user &#187; Presentation &#187; Hide.</p></li></ul></li><li><p><strong>Input account:</strong></p><ul><li><p>Configuration &#187; Operators &#187; External &#187; Add external operator.</p></li><li><p>Enter the number to intercept and click Add. For mobile numbers, provide the first three digits (area code) without the leading zero in the first field and the rest in the second.</p></li><li><p>Note: The number must not be from Freeconet.</p></li><li><p><strong>Do not use popular numbers (e.g., company hotlines), as intercepting random calls can have legal consequences. Use your own landline or mobile number.</strong></p></li></ul></li><li><p><strong>Test account:</strong></p><ul><li><p>If you already have a Freeconet VoIP phone, use it for testing and skip ahead.</p></li><li><p>Otherwise, after logging in, use the web client under "Call from Website". Try dialing 901 (for free account status info) or Freeconet&#8217;s support at 801 009 500.</p></li><li><p>If calls don&#8217;t work, try another SIP client. Configuration info is in the email from Freeconet.</p></li><li><p>Do not make test calls from the same machine running VOIPROX. Use a separate VoIP device or run a softphone on another computer.</p></li></ul></li></ul><h4>Launching the program</h4><p>Download VOIPROX:</p><pre><code><code>$ git clone https://github.com/pepawel/voiprox.git
$ cd voiprox</code></code></pre><p>Start VOIPROX with SIP credentials:</p><pre><code><code>$ ./voiprox input_login:password1 output_login:password2</code></code></pre><p>A short sound confirms correct sound card communication.</p><p>If issues occur, run with <strong>-v</strong> for verbose mode.</p><h4>First test</h4><p>Make a call from the test account (on a different device) to the intercepted number set in the input account. The test call is free for the test account but will charge the output account.</p><p>After the call, a <strong>.wav</strong> file will appear in the VOIPROX directory. Congrats&#8212;you&#8217;ve just intercepted your first phone call. ;-)</p><h4>Further capabilities</h4><p>VOIPROX has an interactive console. Type <strong>help</strong> for commands:</p><pre><code><code>&gt;&gt; help
Possible commands:
  list, show       - show all active connections
  disconnect [[caller|callee] [from]] connection
    - disconnect caller or calee from connection given by
      connection id; if caller/callee not given entire
      connection is terminated
  attach [mic|speaker] [to] connection
    - attach local microphone or speaker to given connection;
      if mic/speaker keyword not given both will be attached
  detach [mic|speaker] [from] connection - analogical to attach
  play file.wav [to] connection
                   - play wav file (no spaces allowed in file name)
  help             - show this help
  quit, exit or ^D - terminate all active connections and quit</code></code></pre><p>Call and don&#8217;t hang up. Use the <strong>list</strong> command to see active calls:</p><pre><code><code>&gt;&gt; list
1: 123456789 &gt; 0987654321</code></code></pre><p>Attach mic/speaker:</p><pre><code><code>&gt;&gt; attach to 1</code></code></pre><p>Detach mic to listen only:</p><pre><code><code>&gt;&gt; detach mic from 1</code></code></pre><p>Disconnect the caller to impersonate:</p><pre><code><code>&gt;&gt; attach mic to 1
&gt;&gt; disconnect caller from 1</code></code></pre><p>Play a WAV file:</p><pre><code><code>&gt;&gt; play test.wav to 1</code></code></pre><p>Enjoy&#8212;and play nice!</p><h4>Disclaimer</h4><p>VOIPROX is a proof-of-concept tool. It may contain bugs. I&#8217;ve tested intercepting two calls simultaneously, but not more. Tested on Ubuntu 9.10.</p><p>All calls during development were initiated by me and used only for testing.</p><p>Use VOIPROX at your own risk. Do not break the law. Freeconet logs all connections&#8212;logs may be used as evidence in court.</p><p>I hope bad actors won&#8217;t use this tool. Public release should pressure Freeconet to patch the flaw quickly, denying criminals time to abuse it. Anyone already secretly exploiting it will lose that opportunity.</p>]]></content:encoded></item><item><title><![CDATA[A security flaw in the modem provisioning system of Telekomunikacja Polska]]></title><description><![CDATA[Sent confidentially to Telekomunikacja Polska in October 2004.]]></description><link>https://www.pawelpokrywka.com/p/security-flaw-in-the-modem-provisioning-of-telekomunikacja-polska</link><guid isPermaLink="false">https://www.pawelpokrywka.com/p/security-flaw-in-the-modem-provisioning-of-telekomunikacja-polska</guid><dc:creator><![CDATA[Paweł Pokrywka]]></dc:creator><pubDate>Sun, 23 Apr 2006 10:00:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Va83!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc23ce18-3511-4f55-a5ec-7468c9eefaf7_1369x778.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Sent confidentially to Telekomunikacja Polska in October 2004. Disclosed to the public at the <a href="https://confidence-conference.org/">CONFidence</a> 2006 conference in slide form. Translated from Polish using AI.</em></p><h2>Summary</h2><p>TP Internet DSL, based on ADSL technology, is a service from Telekomunikacja Polska (TP) mainly targeted at businesses. The DSL modem management system contains a serious security flaw. In this post, I will explain how I discovered the flaw, what risks it involves, and propose countermeasures.</p><h2>Background</h2><p>It all started when my DSL modem broke. I had been using TP&#8217;s DSL Internet Access service for a while and was mostly satisfied. The connection was provided by a Siemens Speedstream 5660 ADSL modem. The modem worked well, though it occasionally suffered from stability issues<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>. However, one thing always annoyed me: TP didn&#8217;t allow users to configure the modem themselves&#8212;it was password-protected, and neither the customer nor even the technician knew the password.</p><p>TP claimed this restriction was for security reasons<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>. Ironically, it actually compromised security.</p><p>Since the modem broke on a weekend, I knew I&#8217;d be waiting a while for a replacement. A friend lent me a Planet ADSL modem, and I tried configuring it manually. The DSL link connected fine, but I couldn&#8217;t get past the PPP login&#8212;I didn&#8217;t have the username or password, and TP didn&#8217;t provide this information to customers. After some persistence, a helpful technician gave me the PPP credentials, which I greatly appreciated.</p><p>During that call, I learned something interesting: by entering a special service login and password, a script would automatically configure the modem. Unfortunately, this only worked with Speedstream modems, so I had to configure everything manually.</p><h2>How it works</h2><p>The service login and password are a clever trick for installers. The installer resets the modem to factory settings, connects it to the DSL line, and enters the login <em>konfiguracja@konfiguracja</em> with the password <em>konfiguracja</em>. A script then completes the configuration remotely.</p><p>This setup prevents rogue installers from keeping access to modems or sharing it with customers. TP controls a centralized database of logins and passwords, allowing them to enforce strong password policies.</p><p>But as I realized, this special pair of login and password shouldn't be public&#8212;because it can be exploited. And anyone can learn them&#8212;all it takes is carefully watching the technician&#8217;s hands during modem configuration.</p><h2>The experiment begins</h2><p>I began to wonder whether this automation could be used to gain access to the modem.</p><p>I downloaded the <a href="https://www.manualslib.com/manual/846990/Efficient-Networks-Speedstream-5600-Series.html">modem manual</a> from the manufacturer&#8217;s website. After studying it, I concluded that the modem is most likely configured remotely via Telnet, since it has a built-in Telnet server. If I could intercept the Telnet session, I might be able to extract some interesting information. But how? The transmission takes place over the DSL line. A DSL sniffer? It was beyond my reach.</p><p>Eventually, I came up with an idea. An attacker equipped with two DSL modems and some additional hardware can build a network like the one shown 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_!Va83!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc23ce18-3511-4f55-a5ec-7468c9eefaf7_1369x778.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Va83!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc23ce18-3511-4f55-a5ec-7468c9eefaf7_1369x778.png 424w, https://substackcdn.com/image/fetch/$s_!Va83!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc23ce18-3511-4f55-a5ec-7468c9eefaf7_1369x778.png 848w, https://substackcdn.com/image/fetch/$s_!Va83!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc23ce18-3511-4f55-a5ec-7468c9eefaf7_1369x778.png 1272w, https://substackcdn.com/image/fetch/$s_!Va83!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc23ce18-3511-4f55-a5ec-7468c9eefaf7_1369x778.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Va83!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc23ce18-3511-4f55-a5ec-7468c9eefaf7_1369x778.png" width="1369" height="778" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cc23ce18-3511-4f55-a5ec-7468c9eefaf7_1369x778.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:778,&quot;width&quot;:1369,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:79642,&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://blog.pawelpokrywka.com/i/160447249?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc23ce18-3511-4f55-a5ec-7468c9eefaf7_1369x778.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_!Va83!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc23ce18-3511-4f55-a5ec-7468c9eefaf7_1369x778.png 424w, https://substackcdn.com/image/fetch/$s_!Va83!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc23ce18-3511-4f55-a5ec-7468c9eefaf7_1369x778.png 848w, https://substackcdn.com/image/fetch/$s_!Va83!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc23ce18-3511-4f55-a5ec-7468c9eefaf7_1369x778.png 1272w, https://substackcdn.com/image/fetch/$s_!Va83!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc23ce18-3511-4f55-a5ec-7468c9eefaf7_1369x778.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 attacker configures the Planet modem to forward (NAPT) traffic coming from the Internet on administrative ports (Telnet, WWW, SNMP, FTP) to an internal host&#8212;the Speedstream modem. The Speedstream is reset to factory settings&#8212;this enables Telnet access to its configuration. The last step is to set the default route on the Speedstream so that it goes through the Planet modem.</p><p>I entered the service login and password into the Planet modem's PPP configuration, then started a sniffer on my PC...</p><h2>First success</h2><p>After a moment, the Planet modem establishes a connection. It receives an address from the pool of service addresses. After a short while, the attacker notices activity on the Ethernet network. The traffic comes from a provisioning server that initiates a Telnet connection to the Planet modem, which forwards it to the Speedstream modem, allowing interception of the session. After recording and analyzing the transmission, I learned, among other things, confidential information about the modem!</p><pre><code>Type "?" at the command prompt for a list of commands.
Type "help" at the command prompt for general help.
For detailed help on a specific command, type command name
followed by a "?",  for instance, "show ?".

Command-&gt; show

--- General Router Information
 System Mode          - Router
 System Type          - SpeedStream 5660-R:ENI
 System HW Version    - 0
 System Up Time       - 0 Days 0 Hours 22 Minutes 36 Seconds
 Software Version     - 2.3.0(8) Jan 13 2003 09:29:38
 Factory MAC Address  - 00:11:22:33:44:55
 DSL Phy Description  - Motorola 850 SAR Alcatel/RT Adapter
 DSL Phy Version      - 
 DSL Interface State  - Down
 Host Name            - SpeedStream
 Domain Name          - domain.invalid
 IP Gateway           - 10.0.0.2
 Ethernet Interface   - 10.0.0.1/255.0.0.0
 DSL Interface        - /
 RIP Mode             - Disabled
 DNS Server           - Enabled
 DHCP Server          - Enabled
 NAPT Mode            - Enabled
 IP Filter Mode       - Disabled

Command-&gt; set pppauth xxxxxxxx@internetdsl xxxxxxxx
Command-&gt; set napt disable
Command-&gt; set ethip x.x.x.x x.x.x.x

Implement IP changes now? default: n [y,n] n

Changes will not take effect until modem is rebooted!

Command-&gt; set snmpcfg xxxxxxxx a a a 10.10.10.10 10.10.10.10
Command-&gt; set password
Setting user password:

New password : ********
New password : ********

Password updated
Command-&gt; reboot

Are you sure? default: n [y,n] y

System rebooting as requested!!!!</code></pre><p>The provisioning script connects, displays the modem&#8217;s data (using the <strong>show</strong> command), and sets configuration parameters, passwords, and the IP address. Finally, it reboots the modem so the new configuration can be loaded. As a side effect of the reboot, even if the attacker had initiated an administrative session (via Telnet or serial console using the factory password) before the script started configuring the modem, that session would be terminated by the reboot. If the reboot didn&#8217;t occur, the attacker would remain connected and could retrieve the newly set passwords.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-3" href="#footnote-3" target="_self">3</a></p><p>For a moment, I wondered why the script ran the <strong>show</strong> command, but the answer quickly became clear: this is how the script identifies the modem to assign it the correct configuration. The only piece of data likely to differ between modems is the MAC address&#8230;</p><h2>The masquerade</h2><p>I wrote and ran a simple Perl script. The script, when hooked up to <strong>inetd</strong> on port 23, emulates the behavior of the modem&#8217;s Telnet server after a factory reset. I set up a network similar to the previous one, but without the Speedstream modem, since it&#8217;s no longer needed.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2CHT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5c76f03-dc15-4c60-94f9-af3adbb7a1fd_1210x691.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2CHT!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5c76f03-dc15-4c60-94f9-af3adbb7a1fd_1210x691.png 424w, https://substackcdn.com/image/fetch/$s_!2CHT!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5c76f03-dc15-4c60-94f9-af3adbb7a1fd_1210x691.png 848w, https://substackcdn.com/image/fetch/$s_!2CHT!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5c76f03-dc15-4c60-94f9-af3adbb7a1fd_1210x691.png 1272w, https://substackcdn.com/image/fetch/$s_!2CHT!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5c76f03-dc15-4c60-94f9-af3adbb7a1fd_1210x691.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2CHT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5c76f03-dc15-4c60-94f9-af3adbb7a1fd_1210x691.png" width="1210" height="691" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d5c76f03-dc15-4c60-94f9-af3adbb7a1fd_1210x691.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:691,&quot;width&quot;:1210,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:52299,&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://blog.pawelpokrywka.com/i/160447249?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5c76f03-dc15-4c60-94f9-af3adbb7a1fd_1210x691.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_!2CHT!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5c76f03-dc15-4c60-94f9-af3adbb7a1fd_1210x691.png 424w, https://substackcdn.com/image/fetch/$s_!2CHT!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5c76f03-dc15-4c60-94f9-af3adbb7a1fd_1210x691.png 848w, https://substackcdn.com/image/fetch/$s_!2CHT!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5c76f03-dc15-4c60-94f9-af3adbb7a1fd_1210x691.png 1272w, https://substackcdn.com/image/fetch/$s_!2CHT!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd5c76f03-dc15-4c60-94f9-af3adbb7a1fd_1210x691.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 PC configured to impersonate a modem.</figcaption></figure></div><p>To check whether it&#8217;s possible to obtain passwords for any modem, I configured the script to display the MAC address of another existing device.</p><p>Indeed, the provisioning script connected to my script and handed over the configuration data of a different modem!</p><p>I could repeat this experiment successfully using the MAC address of a modem from another city. To obtain confidential configuration data for any modem in Poland, all you need is its MAC address!</p><h2>Packet filter</h2><p>I gained Telnet access to modems to which I had physical access. However, an attempt to log in to a modem in another city failed. Connections to the Telnet port were <em>hanging</em>&#8212;there was no response from the remote modem. Meanwhile, TCP connections to other ports were actively rejected (the modem sent a TCP packet with the RST flag set). This was a clear sign of packet filtering.</p><p>Using the <a href="https://linux.die.net/man/1/tcptraceroute">tcptraceroute</a> tool, I determined where the packets were being filtered. It turned out the filtering was done by the DSLAM located just before the remote modem. A DSLAM (DSL Access Multiplexer) is a <em>central hub</em> to which many DSL modems are connected. It provides connectivity between the modems and the external network. In the case of TP, these are <a href="https://web.archive.org/web/20050208192027/http://www.lucent.com/press/0503/030527.nsa.html">most likely</a> Lucent&#8217;s Stinger devices.</p><p>The packet filters and &#8220;firewalls&#8221; in the operating systems of such devices usually have little in common with professional solutions (like Cisco PIX, GNU/Linux, or *BSD systems). Instead, they function more like add-ons that &#8220;sort of work.&#8221;</p><p>Based on this assumption, one can try to bypass the DSLAM&#8217;s blocking rules. Simple packet filters may, by default, allow all fragmented packets through. I needed to structure the connection attempt to the modem so that at least the initial connection packet is fragmented. For this, I used the <a href="https://www.monkey.org/~dugsong/fragroute/">fragroute</a> tool. It turns out a small patch was necessary because the program doesn&#8217;t fragment packets with the SYN flag set, which are precisely the packets that need to be fragmented.</p><h2>Filter bypassed!</h2><p>The filter allowed the fragmented packets through, and the attacker gained remote access to the modem.</p><p>Another method of bypassing the filter is IP spoofing. For the attack to succeed, one would need to find an inactive IP address that&#8217;s on the DSLAM&#8217;s list of &#8220;trusted&#8221; addresses. These IPs should be searched for &#8220;near&#8221; the IP address of the server running the modem configuration script.</p><p>If such a trusted, inactive IP were found, the attack&#8212;despite the obvious challenge of performing it without feedback&#8212;would be relatively simple because, according to <a href="https://nmap.org/">Nmap</a>, the modem&#8217;s ISN (Initial Sequence Number) generator is predictable:</p><pre><code>Starting nmap x ( http://www.insecure.org/nmap/ ) at xxxx-xx-xx xx:xx CEST
Host x (x.x.x.x) appears to be up ... good.
Initiating SYN Stealth Scan against x (x.x.x.x) at xx:xx
Adding open port 1723/tcp
Adding open port 21/tcp
Adding open port 80/tcp
Adding open port 23/tcp
The SYN Stealth Scan took 6 seconds to scan 1659 ports.
For OSScan assuming that port 21 is open and port 1 is closed and neither are firewalled
Interesting ports on x (x.x.x.x):
(The 1655 ports scanned but not shown below are in state: closed)
PORT     STATE SERVICE
21/tcp   open  ftp
23/tcp   open  telnet
80/tcp   open  http
1723/tcp open  pptp
Device type: firewall|switch|WAP
Running: SonicWall embedded, Enterasys embedded, Cisco embedded
OS details: SonicWall SOHO firewall, Enterasys Matrix E1, or Accelerated Networks VoDSL, or Cisco 360 Access Point
<strong>TCP Sequence Prediction: Class=64K rule
                         Difficulty=1 (Trivial joke)</strong>
IPID Sequence Generation: Incremental

Nmap run completed -- 1 IP address (1 host up) scanned in 12.380 seconds</code></pre><h2>What&#8217;s next?</h2><p>Full access to the modem gives a malicious actor significant capabilities.</p><p>In the simplest case, the attacker could disconnect the subscriber from the Internet by disabling the LAN interface; they could change the modem&#8217;s access password, preventing TP technicians from reaching the device; or they could attempt to break into the subscriber&#8217;s internal network, which until now was protected by the modem&#8217;s NAT.</p><p>There is an officially <a href="http://www.quiezent.com/efficient_ 5660.html">undocumented</a> <strong>SET PRIV</strong> command that provides full access to the operating system (VxWorks). Full system access gives the attacker virtually unlimited possibilities&#8212;they could upload and run any program on the modem. Such a program could turn the modem into a so-called &#8220;zombie&#8221; used in DDoS attacks; it could allow the attacker to <a href="http://www.xs4all.nl/~borkhuis/vxworks/vxsniff.c">eavesdrop on traffic</a> and perform man-in-the-middle (MitM) attacks. Finally, the program could reprogram the modem&#8217;s FLASH memory to the point that it stops functioning entirely and would require service reprogramming.</p><p>The described threats become more serious when one considers the number of modems in use in Poland.<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-4" href="#footnote-4" target="_self">4</a></p><h2>The problem of obtaining MAC addresses</h2><p>To gain control over a modem, one must know its MAC address. It wasn&#8217;t possible to obtain this address based on the IP of the Speedstream 5660 modem.</p><p>After analyzing a transmission dump between the script and the modem using <a href="https://lcamtuf.coredump.cx/p0f3/">p0f</a>, it turned out that the server running the configuration script most likely operates under Solaris 8.</p><pre><code>p0f - passive os fingerprinting utility, version 2.0.2
(C) M. Zalewski &lt;lcamtuf@coredump.cx&gt;, W. Stearns &lt;wstearns@pobox.com&gt;
p0f: listening (SYN) on 'filtered.dump', 193 sigs (9 generic), rule: 'all'.
80.50.248.71:14396 - Solaris 8 (1) (NAT!) 
  -&gt; 10.0.0.1:23 (distance 8, link: unknown-1472)
[+] End of input file.</code></pre><p>If an attacker were able to gain control of this server, they would also have access to the MAC address database (and most likely passwords and other confidential data as well).</p><p>The server is protected by a firewall that likely performs packet defragmentation, so the previous trick with packet fragmentation wouldn&#8217;t work here. I did not attempt direct intrusion, but I tested the provisioning script by feeding it incorrect input. It proved to be resistant&#8212;at least to the inputs it was tested with.</p><p>While testing the buffer capacity of the remote side, it became clear what kind of firewall was in use. When my program sent a large amount of data, the script on the other side disconnected and closed the connection. The firewall interpreted this as a terminated connection, while my program kept sending data. As a result, the packets sent by the program were reset&#8212;but in a very specific way. The RST packets weren&#8217;t empty&#8212;they contained a payload that was a copy of the packet being reset. This is a known bug in Cisco PIX.</p><p>With other methods blocked, brute-forcing MAC addresses was the remaining option. Of course, it&#8217;s not necessary to scan the entire MAC address space. While there are 2&#8308;&#8312; possible MAC addresses, which might sound discouraging, the first three bytes are identical for all the modems. Additionally, the first half of the fourth byte is often the same. That narrows it down to 2&#178;&#8312; = 268,435,456 possibilities.</p><p>It&#8217;s reasonable to assume that TP purchased modems in batches. Devices from the same batch have almost identical MAC addresses. If the attacker &#8220;hits&#8221; one valid MAC address, finding others from the same batch becomes easy.</p><p>During testing, I observed that TP&#8217;s script has timeouts for modem configuration. If the modem doesn&#8217;t respond within a certain time, the script disconnects and reconnects&#8212;this cycle can repeat multiple times. This behavior can be exploited to speed up password collection.</p><p>With enough time, it would be possible to create a password-harvesting program that intelligently scans MAC addresses and extracts passwords from the database at a rate of one every few seconds&#8212;provided it &#8220;hits&#8221; a valid series. The wait time could be longer if the MAC address is a miss, but likely no more than 2&#8211;3 minutes.</p><h2>New modems</h2><p>After identifying and initially describing the vulnerability, it turned out that, in addition to the Speedstream 5660 modems, TP was also providing subscribers with newer modems&#8212;devices from the same manufacturer, but marked with the model number 5100.</p><p>Like the 5660, the Speedstream 5100 also features a web interface for managing and monitoring the modem. However, unlike the 5660, which protects the entire interface with a password, the 5100 allows users to monitor the modem's status without authentication. This allows subscribers to diagnose connection issues without needing full access.</p><p>The main page of the web interface also includes basic information about the device. From an attacker&#8217;s perspective, the key piece of information is&#8230; the MAC address.</p><h2>The problem of obtaining MAC addresses&#8212;solved</h2><p>This is a major convenience for an attacker. Knowing the IP address of a remote modem is enough to connect to the modem&#8217;s homepage (again using fragmented packets) and retrieve its MAC address.</p><p>An attacker, using a tool to scan all IP ranges assigned to TP and looking specifically for 5100 modems (which are easy to identify remotely), could very quickly collect the MAC addresses of all such modems in Poland. Needless to say, all of the previously described threats also apply to these devices.</p><h2>Default password</h2><p>An attempt to retrieve authentication data for the 5100 using the spoofing script failed. The provisioning script disconnected, just as it does when it receives an invalid response from the modem. It&#8217;s likely because the CLI interface was different.</p><p>To adapt the spoofing script to &#8220;work&#8221; with the 5100, I recreated the test setup&#8212;this time using the newer modem. Analysis of the captured transmission provided the necessary details and revealed an amusing fact: to prevent subscribers from tampering with the configuration, the factory password had been changed. This means even someone with the modem's manual&#8212;including the default password&#8212;cannot gain access to the device after a factory reset.</p><p>Unfortunately, by analyzing the transmission between the script and the modem, an attacker can easily learn this password.</p><h2>Countermeasures</h2><p>It's difficult for me to suggest what actions TP could take to secure the DSL system as I&#8217;m certainly unaware of all its features and the requirements it must meet. I also don't know the system's internal architecture or the capabilities of the devices that comprise it. Still, I will briefly discuss some simple remedies that wouldn't require major changes to the system design.</p><p>The most reliable solution would be to abandon automatic modem configuration entirely. However, I realize this could increase the cost of installing new modems and maintaining existing ones.</p><p>If the packet filters on DSLAMs allow it, a simple measure could be to block fragmented packets. However, this would serve only as a temporary fix, as it doesn&#8217;t prevent credential theft from the central database nor stop the use of those credentials when the attacker has physical access to the modem.</p><p>A more balanced solution might be to limit automatic modem provisioning to a few trusted locations&#8212;for example, one in each major city. In those locations, technicians would configure the modems before installing them at customer premises.</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 modem requires that all IP addresses available to the client be used; otherwise, it often "freezes."</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>TP FAQ, <a href="http://web.archive.org/web/20030720114445/http://internetdsl.pl/pages/technologia.php">question 59</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>Even the Telnet password, although it isn&#8217;t displayed&#8212;it can only be changed. However, the Telnet and SNMP passwords are the same in TP&#8217;s system, so obtaining one effectively gives access to the other.</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>According to information dated August 29, 2004, published on the website of Telekomunikacja Polska, the DSL Internet Access service from TP is used by <a href="http://www.tp.pl/otp/serwis_prasowy/biuro/show.php?mid=1490">35,000 subscribers</a>.</p></div></div>]]></content:encoded></item></channel></rss>