Assets Pipeline: How CSS, JS, and Images Load
Understand how Shopify serves your theme's CSS, JavaScript, and images through its CDN, and learn best practices for loading assets efficiently.
Every theme needs CSS for styling, JavaScript for interactivity, and images for visual content. Shopify provides a robust asset pipeline that serves these files through a global CDN, handles cache busting automatically, and gives you filters to load assets efficiently.
The Assets Directory
All static files live in your theme’s assets/ directory:
theme/├── assets/│ ├── base.css│ ├── component-card.css│ ├── theme.js│ ├── product.js│ ├── logo.svg│ └── placeholder.pngUnlike other directories, assets/ is flat with no subdirectories allowed. Use naming conventions to organize:
assets/├── base.css├── component-header.css├── component-footer.css├── component-product-card.css├── section-hero.css├── section-featured-collection.css├── theme.js├── component-cart-drawer.js└── component-product-form.jsThe asset_url Filter
The asset_url filter converts a filename into a full CDN URL:
{{ 'theme.css' | asset_url }}Output:
https://cdn.shopify.com/s/files/1/0123/4567/8901/t/1/assets/theme.css?v=123456789Key features:
- CDN delivery: Assets are served from Shopify’s global CDN
- Cache busting: The
?v=parameter changes when the file changes - HTTPS: All assets are served securely
Loading Stylesheets
Using stylesheet_tag
The simplest way to load CSS:
{{ 'base.css' | asset_url | stylesheet_tag }}Output:
<link href="//cdn.shopify.com/.../base.css?v=123" rel="stylesheet" type="text/css" media="all" />Manual Link Tag
For more control, build the tag yourself:
<link rel="stylesheet" href="{{ 'base.css' | asset_url }}" media="all">Preloading Critical CSS
Preload CSS that’s needed immediately:
<link rel="preload" href="{{ 'base.css' | asset_url }}" as="style"><link rel="stylesheet" href="{{ 'base.css' | asset_url }}">Loading CSS Conditionally
Load page-specific CSS only where needed:
{%- case request.page_type -%} {%- when 'product' -%} {{ 'section-product.css' | asset_url | stylesheet_tag }} {%- when 'collection' -%} {{ 'section-collection.css' | asset_url | stylesheet_tag }}{%- endcase -%}Loading JavaScript
Using script_tag
{{ 'theme.js' | asset_url | script_tag }}Output:
<script src="//cdn.shopify.com/.../theme.js?v=123" type="text/javascript"></script>Defer and Async
For better performance, load scripts without blocking:
<script src="{{ 'theme.js' | asset_url }}" defer></script>Or async for independent scripts:
<script src="{{ 'analytics.js' | asset_url }}" async></script>Defer vs Async:
- defer: Executes after HTML is parsed, maintains order
- async: Executes as soon as downloaded, no guaranteed order
Loading Scripts at End of Body
Best practice is to load scripts before </body>:
<body> {% sections 'header-group' %}
<main>{{ content_for_layout }}</main>
{% sections 'footer-group' %}
{# Scripts at the end #} <script src="{{ 'vendor.js' | asset_url }}" defer></script> <script src="{{ 'theme.js' | asset_url }}" defer></script></body>Module Scripts
For modern JavaScript modules:
<script type="module" src="{{ 'theme.js' | asset_url }}"></script>With a fallback for older browsers:
<script type="module" src="{{ 'theme.js' | asset_url }}"></script><script nomodule src="{{ 'theme-legacy.js' | asset_url }}" defer></script>Loading Images
In Liquid Templates
For theme asset images:
<img src="{{ 'logo.svg' | asset_url }}" alt="{{ shop.name }}">
<img src="{{ 'placeholder.png' | asset_url }}" alt="Placeholder">Product and Collection Images
Use the image_url filter for dynamic images:
{{ product.featured_image | image_url: width: 600 | image_tag }}Background Images in CSS
For CSS background images, use inline styles with Liquid:
<div style="background-image: url('{{ section.settings.image | image_url: width: 1920 }}');">Or in a style block:
<style> .hero-banner { background-image: url('{{ section.settings.image | image_url: width: 1920 }}'); }</style>Preconnect and Prefetch
Speed up asset loading with resource hints:
<head> {# Preconnect to Shopify's CDN #} <link rel="preconnect" href="https://cdn.shopify.com" crossorigin>
{# Preconnect to fonts #} <link rel="preconnect" href="https://fonts.shopifycdn.com" crossorigin>
{# Prefetch assets for likely next pages #} <link rel="prefetch" href="{{ 'product.js' | asset_url }}"></head>Resource hints:
- preconnect: Establish connection early (DNS, TCP, TLS)
- prefetch: Load resources that might be needed soon
- preload: Load resources needed for current page
Font Loading
Using Theme Settings Fonts
{%- liquid assign body_font = settings.type_body_font assign heading_font = settings.type_header_font-%}
<style> {{ body_font | font_face: font_display: 'swap' }} {{ heading_font | font_face: font_display: 'swap' }}
body { font-family: {{ body_font.family }}, {{ body_font.fallback_families }}; }
h1, h2, h3 { font-family: {{ heading_font.family }}, {{ heading_font.fallback_families }}; }</style>Custom Font Files
For custom fonts in your assets:
<style> @font-face { font-family: 'CustomFont'; src: url('{{ "custom-font.woff2" | asset_url }}') format('woff2'), url('{{ "custom-font.woff" | asset_url }}') format('woff'); font-weight: 400; font-style: normal; font-display: swap; }</style>Preload for faster loading:
<link rel="preload" href="{{ 'custom-font.woff2' | asset_url }}" as="font" type="font/woff2" crossorigin>Inline Styles and Scripts
For critical CSS or small scripts, inline them:
<style> /* Critical above-the-fold CSS */ .header { ... } .hero { ... }</style><script> // Small inline script document.documentElement.classList.replace('no-js', 'js');</script>Using {% style %} Tag
The style tag is useful for Liquid-generated CSS:
{% style %} .section-{{ section.id }} { background-color: {{ section.settings.background_color }}; padding: {{ section.settings.padding }}px; }{% endstyle %}Complete Asset Loading Example
Here’s a production-ready theme.liquid head section:
<head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ page_title }}</title>
{# Preconnect to CDN and fonts #} <link rel="preconnect" href="https://cdn.shopify.com" crossorigin> <link rel="preconnect" href="https://fonts.shopifycdn.com" crossorigin>
{# Preload critical assets #} <link rel="preload" href="{{ 'base.css' | asset_url }}" as="style"> <link rel="preload" href="{{ settings.type_header_font | font_url }}" as="font" type="font/woff2" crossorigin>
{# Required Shopify content #} {{ content_for_header }}
{# Theme fonts #} {% style %} {{ settings.type_body_font | font_face: font_display: 'swap' }} {{ settings.type_header_font | font_face: font_display: 'swap' }} {% endstyle %}
{# CSS custom properties #} {% render 'css-variables' %}
{# Main stylesheet #} {{ 'base.css' | asset_url | stylesheet_tag }}
{# Page-specific CSS #} {%- case request.page_type -%} {%- when 'product' -%} {{ 'template-product.css' | asset_url | stylesheet_tag }} {%- when 'collection' -%} {{ 'template-collection.css' | asset_url | stylesheet_tag }} {%- endcase -%}
{# No-JS detection #} <script> document.documentElement.className = document.documentElement.className.replace('no-js', 'js'); </script></head>
<body> {# ... page content ... #}
{# Scripts at end of body #} <script src="{{ 'vendor.js' | asset_url }}" defer></script> <script src="{{ 'theme.js' | asset_url }}" defer></script></body>Shopify Asset URLs
Shopify provides special asset URLs for built-in resources:
{# Shopify's hosted assets #}{{ 'option_selection.js' | shopify_asset_url | script_tag }}
{# Payment icons #}{{ 'visa' | payment_type_svg_tag }}
{# Placeholder images #}{{ 'product-1' | placeholder_svg_tag }}File URL for Uploaded Files
For files uploaded via theme settings (like PDFs):
{%- if settings.size_guide_pdf -%} <a href="{{ settings.size_guide_pdf | file_url }}">Download Size Guide</a>{%- endif -%}Practice Exercise
Create the asset loading section for a theme.liquid that:
- Preconnects to the CDN
- Preloads the main CSS file
- Loads fonts with font_face
- Loads base CSS
- Conditionally loads product CSS on product pages
- Defers JavaScript loading
<head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{{ page_title }} | {{ shop.name }}</title>
<link rel="preconnect" href="https://cdn.shopify.com" crossorigin> <link rel="preload" href="{{ 'theme.css' | asset_url }}" as="style">
{{ content_for_header }}
{% style %} {{ settings.type_body_font | font_face: font_display: 'swap' }} {{ settings.type_header_font | font_face: font_display: 'swap' }} {% endstyle %}
{{ 'theme.css' | asset_url | stylesheet_tag }}
{%- if request.page_type == 'product' -%} {{ 'product.css' | asset_url | stylesheet_tag }} {%- endif -%}</head>
<body> {{ content_for_layout }}
<script src="{{ 'theme.js' | asset_url }}" defer></script>
{%- if request.page_type == 'product' -%} <script src="{{ 'product.js' | asset_url }}" defer></script> {%- endif -%}</body>Key Takeaways
- All assets live in
assets/with no subdirectories asset_urlconverts filenames to CDN URLs with cache bustingstylesheet_tagandscript_taggenerate proper HTML tags- Preconnect and preload speed up critical resource loading
- Defer JavaScript to avoid blocking page rendering
- Load conditionally to avoid unnecessary downloads
- Inline critical CSS for fastest first paint
- Use
font_display: swapfor better font loading UX
What’s Next?
Now that you understand how assets load, the next lesson covers CSS Structure for Maintainability and how to organize your stylesheets effectively.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...