Shopify now renders everything in your layout up to {{ content_for_header }} and flushes it to the browser as soon as possible, without waiting for the rest of the content to render. The rest of the page follows in the same stream when the template finishes rendering.
Across all storefront page views, Time to First Byte (TTFB) for the median store dropped by about 29% at p75 and 21% at p90. To a lesser extent, between 2% and 6%, First Contentful Paint (FCP) and Largest Contentful Paint (LCP) improved as well, which matters more, because those are the metrics buyers actually see.
The change required a significant effort across multiple scopes, but the results push the performance of the entire platform, all without a single change needed from our merchants.
Think about what a Liquid page is made of. The in your layout is mostly static markup: meta tags, stylesheet links, font preloads, a few scripts. It renders in a couple of milliseconds. Then there are the sections in your template, and that’s where the vast majority of time goes. Until now, the browser got nothing until both were done. Only then could it parse the , find the stylesheets and scripts, and start downloading them.
Now the head goes out the moment it’s rendered, while the template is still rendering. The browser parses it, opens connections, and starts downloading everything it finds there. By the time the rest of the content arrives, the stylesheets and fonts the first paint needs are already downloading or done. The browser works in parallel with Shopify instead of after it.
The HTML itself doesn’t change. Glue the two parts together and you get the same document as before.
Most storefront pages are streamed, but not all. For the feature to work, we need to know what layout to render upfront, among other limitations. On the theme side, a page qualifies when:
.liquid templates aren’t streamed yet. A Liquid template can swap the layout mid-render or assign variables that the layout later reads, so there’s no safe point to cut the response.{{ content_for_header }} as a plain output tag inside . No filters, not wrapped in {% if %} or {% capture %}, not tucked away in a snippet, not assigned to a variable first.Preview themes, the preview bar, and the theme editor aren’t streamed, so if you want to see the effect, look at your live theme.
We’re actively working on expanding eligibility and will keep the platform documentation up to date as more pages qualify.
Every eligible store gets the TTFB improvement. How much of it turns into a faster first paint is up to the theme, because the browser can only download what it has already received. If your main stylesheet link sits below {{ content_for_header }}, it arrives with the second part of the response, after the template has rendered, and the head start is spent on little more than meta tags.
Dawn and Horizon sit at the two ends of this. Dawn puts {{ content_for_header }} early in the head and loads base.css, its font declarations, and its component stylesheets below it. Horizon defines pretty much everything before {{ content_for_header }}. Here’s how the two compared during the rollout, again at p75 for the median store:
TTFB improved by a similar share on both. Horizon turned that into roughly three times the FCP gain and twice the LCP gain, because its render-blocking resources were already in the browser’s hands while the template was still rendering.
If you maintain a theme, the most useful thing you can do is move as many critical resource definitions as you can above {{ content_for_header }}. Check the ordering dependencies before you do. Inline scripts that read Shopify.* will throw above the tag, and a stylesheet moved above content_for_header starts losing specificity ties against the styles Shopify injects. The new guide, Load first-paint resources before content_for_header, goes through each case with examples from both themes.
Nothing can paint until the content arrives, so the Liquid best practices matter as much as ever. And if your theme fetches above-the-fold content from an external API in JavaScript, send that request before the DOM is ready so it also overlaps with the template render.
Streaming the is not a new idea. It has been on our list for years and we scoped it more than once. Every time, the same blockers came back, each a project in itself, and there was always other work with a clearer payoff.
What changed this year is that AI coding agents made the wide, careful reading this kind of change needs a lot cheaper. The implementation touched dozens of files across our codebase. The proof of concept, the production code, and the measurement work behind the numbers in this post were all done with an agent doing most of the exploring and typing.
That doesn’t mean the work was handed off. Engineers from several teams reviewed every change for weeks and pushed back where it mattered. They kept the transport simpler than the first version, insisted that each unrelated fix that surfaced along the way shipped on its own, and said no to a generalisation that looked attractive on paper and turned out to be a net loss once measured. AI made the scope tractable. People made sure the result was something we would be comfortable running for every storefront on the platform.
Historically, Time to First Byte has always been the primary metric for measuring server work, with some margin for other components like network latency, redirects, and connection setup. Every tool from CrUX to WebPageTest to the DevTools Network panel reports it, and everyone tends to read it that way.
Early Hints broke that first. A 103 Early Hints response arrives before the final document, and browsers count its first byte as TTFB. A page with a slow server and quick Early Hints can report a great TTFB while the HTML is still hundreds of milliseconds away. The web.dev article on TTFB acknowledges the problem. The fix was a new Navigation Timing field, finalResponseHeadersStart, which marks when the final response begins regardless of any interim one.
Streaming breaks it again, and this time there’s no good candidate for a replacement. finalResponseHeadersStart now marks when the first part of the document arrives, which can be well before your template has rendered. There is no standard timestamp for when the second chunk of a response arrives, and nothing in Navigation Timing that says how long the server kept working after the first byte.
We’re aware of that gap and are discussing how to address it. For now, there are two ways to measure Liquid rendering time on a streamed page:
render call, this is where the saving shows up.responseEnd - finalResponseHeadersStart, the time between the first byte of the document and the last, works as a proxy for the server work that happens after the first flush. It gets influenced by network and device conditions, but so did TTFB. The testing guide covers how to read it in DevTools, WebPageTest, and Lighthouse.We’ll update the docs with best practices for monitoring as our thinking settles. In any case, FCP and LCP still measure what the buyer actually sees, and those are the metrics streaming was built to improve.
As mentioned above, we’re working on expanding the eligibility for streaming to as many requests as possible. At the same time, we’re also adjusting our rendering so that the first chunk of the HTML can leave as early as possible.
None of this changes how much work the server does. Streaming changes when the browser receives the output, and TTFB looks better because the browser gets something useful sooner. We’re not going to treat that smaller number as a job done. We remain committed to reducing the total processing time of every response, from the first byte to the last. Every millisecond we take out of template rendering still reaches buyers as a page that finishes loading sooner, and with streaming in place it also widens the window in which the browser downloads instead of waiting.