Load first-paint resources before content_ for_ header
Place the stylesheets, font preloads, and scripts that render anything the user sees before they scroll above {{ content_for_header }} in your layout. Shopify can send everything above that tag to the browser as soon as it's rendered. The rest of the page follows when your sections finish. What sits above the tag reaches the browser first.
Shopify can stream storefront HTML in two parts, and on most pages it does. The first part is your layout from <!doctype html> to {{ content_for_header }}. The browser receives it while Shopify is still rendering the sections in your template, and it starts fetching every stylesheet, font, and script it finds there right away. The second part, starting with the output of content_for_header and continuing through </body>, arrives when rendering completes.
If your critical stylesheet link sits below {{ content_for_header }}, then on a streamed page the browser can't request it until your sections have finished rendering. On a product or collection page with a heavy theme that can be hundreds of milliseconds of Liquid work. Move the link above the tag and the download overlaps with that work instead of waiting for it, so First Contentful Paint (FCP) and Largest Contentful Paint (LCP) come earlier.
Shopify streams eligible pages automatically, and it's expanding the set of pages that qualify. A layout that follows this practice loses nothing when a page isn't streamed, because the browser still receives the resources in the order it needs them. Streaming doesn't change how long your Liquid takes to render, so the Liquid best practices still apply. It changes when the browser can start its own work.
Horizon renders its stylesheets, font preloads, import map, module preloads, and CSS variables above {{ content_for_header }}, so all of them reach the browser in the first part of a streamed response. Dawn puts {{ content_for_header }} early in the head and loads base.css, its font_face declarations, and its component stylesheets below it, so those wait for the sections.
Anchor to Move first-paint resources upMove first-paint resources up
Everything the browser needs to paint the initial viewport belongs above {{ content_for_header }}:
<meta charset>,<meta name="viewport">, and<title>.- Stylesheet links for your base styles and for components visible in the initial viewport, loaded through
stylesheet_tag. @font-facedeclarations throughfont_face, and, if you preload a font for text the user sees before scrolling, thatpreload_tagtoo.- Inline
{% style %}blocks that define CSS custom properties from theme settings. - Your import map and any
modulepreloadlinks, followed by the module scripts that use them. asyncscripts that fetch above-the-fold content from an external API, so the request goes out during the section render. See Send data requests for critical content before the DOM is ready.
Keep base styles in a CSS file in assets/ rather than in {% stylesheet %} tags. Shopify compiles {% stylesheet %} output from sections, blocks, and snippets into one file and links it from content_for_header. On themes such as Horizon that file is subset to the sections and blocks on the current page, which Shopify only knows once they've rendered, so the link always arrives with the second part of the response. That's the right place for section-scoped styles, and the wrong place for the styles your first paint depends on.
Anchor to Keep ,[object Object], as it isKeep content_for_header as it is
content_for_header as it isStreaming depends on Shopify finding {{ content_for_header }} as a plain output tag inside <head>. Don't apply filters to it, wrap it in {% if %}, {% capture %}, or {% liquid %}, assign it to a variable, or move it into a snippet. The ContentForHeaderModification theme check flags some of these.
The same applies to layouts that apps provide. A landing page builder or a similar app that ships its own layout and captures or rewrites content_for_header turns streaming off for every page that uses that layout. Check the layouts under layout/ that you didn't write.
For the full list of what makes a page eligible, including template type, see Streamed HTML responses on the platform page. Preview themes, the preview bar, and the theme editor aren't streamed. Test on your live theme.
Anchor to Check what depends on ,[object Object]Check what depends on content_for_header
content_for_headercontent_for_header defines the window.Shopify object and outputs stylesheets and deferred scripts of its own, so moving your code above it changes what's available when that code runs. Check three things before you move anything:
-
Scripts that read
Shopify.*while the page is parsing. An inline<script>, or an external<script src>withoutdefer,async, ortype="module"(the formscript_tagproduces), runs as soon as the parser reaches it. Above{{ content_for_header }}, theShopifyobject doesn't exist yet, soShopify.shop,Shopify.locale,Shopify.currency,Shopify.routes.root, and the rest throw aReferenceError. Leave such scripts where they are. They gain little from moving up, and making them wait forShopifyadds complexity for no benefit. If the value is available in Liquid, read it there instead:request.design_mode,routes.root_url,request.locale, andcart.currencycover the common cases.layout/theme.liquid
{%- comment -%} Breaks above the tag: Shopify isn't defined yet {%- endcomment -%}<script>window.themeRoot = Shopify.routes.root;</script>{%- comment -%} Works anywhere: the value comes from Liquid {%- endcomment -%}<script>window.themeRoot = {{ routes.root_url | json }};</script>{{ content_for_header }} -
Stylesheets that compete with Shopify's.
content_for_headerlinks the stylesheet compiled from{% stylesheet %}tags and, on pages that render dynamic checkout buttons, the styles for those buttons. When two rules have equal specificity, the later stylesheet wins. A theme stylesheet moved from below the tag to above it now loses those conflicts. After you move a stylesheet, check components styled through{% stylesheet %}and the.shopify-payment-buttonarea, and raise specificity where a rule stopped applying.layout/theme.liquid
{%- comment -%}Before: base.css comes after content_for_header, so its rules winties against the compiled stylesheet file.{%- endcomment -%}{{ content_for_header }}{{ 'base.css' | asset_url | stylesheet_tag }}{%- comment -%}After: base.css comes first, so the compiled file wins the same ties.A rule such as `.card { padding: 0 }` in base.css that used to override`.card { padding: 1rem }` from a section's stylesheet tag no longer does.{%- endcomment -%}{{ 'base.css' | asset_url | stylesheet_tag }}{{ content_for_header }} -
deferscripts that depend on{% javascript %}code.content_for_headeralso outputs the deferred scripts compiled from{% javascript %}tags. Deferred scripts run in document order, so a theme script moved above the tag now runs before them. This only matters if your theme script reads something a{% javascript %}block defines. The reverse dependency, section code that reads globals from your theme script, becomes safer.layout/theme.liquid
{%- comment -%}theme.js runs after the compiled javascript bundle when it's below the tag,and before it when it's above. Only code in theme.js that readssomething the bundle defines notices the difference.{%- endcomment -%}<script src="{{ 'theme.js' | asset_url }}" defer></script>{{ content_for_header }}
Import maps need no extra care. Keep yours ahead of the module scripts that use it, as you would anyway. Its position relative to {{ content_for_header }} doesn't matter.
Apps need nothing from you. App embed blocks, theme app extension assets, and scripts that apps inject land after content_for_header regardless of how you arrange your layout.
Anchor to Keep the layout head cheapKeep the layout head cheap
Shopify renders everything above {{ content_for_header }} before it can send the first part of the response. Expensive Liquid in the layout head, such as a meta-tags snippet that loops over collections or product.variants, delays the whole benefit. Keep catalog loops in sections, where they belong anyway, and keep the head to metadata, resource links, and settings-driven CSS.
Anchor to ExamplesExamples
A layout head arranged for streaming. This is the structure Horizon uses, with the snippets inlined:
layout/theme.liquid
An inline script that shouldn't move above the tag. Dawn's head ends with this, well below {{ content_for_header }}:
layout/theme.liquid
Moved above {{ content_for_header }}, it throws ReferenceError: Shopify is not defined on the live storefront. Leave it in place, or read the value in Liquid, which removes the script altogether:
layout/theme.liquid
Anchor to TestingTesting
- Open the Network panel in Chrome DevTools, reload a product or collection page on your live theme, and select the document request. Preview themes and the theme editor aren't streamed, so the split doesn't show there. On a streamed page, Waiting for server response in the Timing tab ends when the first part of the response arrives and Content download covers the rest, so your stylesheet and font requests should start during Content download rather than after it. If the page wasn't streamed, they start right after the document arrives either way, and the comparison in the next step is the one to rely on.
- Compare two versions of the layout: one with your stylesheet link below
{{ content_for_header }}and one with it above. In the waterfall, the stylesheet request starts earlier in the second version. Run each several times and compare medians, because network noise is larger than a single run's difference. - Run a Lighthouse audit before and after. FCP and LCP should improve. The render-blocking resources audit still lists your stylesheets, which is expected. The goal is to start them earlier, not to remove them.
- Load the page with the theme editor and with a normal storefront visit, and check the console for
ReferenceError: Shopify is not defined. The editor defines theShopifyobject at the top of the head, so a script that only breaks outside the editor is easy to miss. - Run Theme Check to confirm
ContentForHeaderModificationpasses.
Anchor to ReferencesReferences
- The Shopify platform: Streamed HTML responses
- Layouts
stylesheet_tagfilterpreload_tagfilterfont_facefilterstylesheetandjavascripttagsrequest.design_modeContentForHeaderModificationtheme check- Load critical CSS synchronously
- Self-host web fonts on Shopify CDN
- Load JavaScript modules with import maps
- Measuring Liquid rendering time