vite-plugin-shopify-critical
v1.0.1
Published
Vite plugin for inlining critical CSS/JS in Shopify themes using inline_asset_content
Maintainers
Readme
vite-plugin-shopify-critical
A Vite plugin for inlining critical CSS/JS in Shopify themes using Shopify's inline_asset_content filter.
Overview
This plugin extends the vite-plugin-shopify ecosystem by enabling critical CSS and JavaScript inlining directly into Liquid templates. Critical assets are identified by a critical- prefix and automatically handled in both development and production modes.
Why This Plugin?
The Problem
vite-plugin-shopify only supports external asset loading via stylesheet_tag and script_tag. For CSS, this results in render-blocking <link> tags.
Important context on CSS performance:
Render-blocking CSS does not necessarily mean a slower website. For sites that aren't CSS-heavy, the performance impact is minimal. In fact, optimizing JavaScript delivery typically yields greater performance benefits than optimizing CSS. Read this discussion to find out more.
If JavaScript delivery is causing performance issues on your site, consider:
- Code splitting - Break your JS into smaller chunks loaded on demand
- Islands architecture - Check out hydrogen-theme which implements the "islands" approach for Shopify
Additionally, vite-plugin-shopify already supports preloading stylesheets to minimize render-blocking time, which may be sufficient for many use cases.
That said, if you're identifying CSS as your performance bottleneck, inlining critical CSS can provide benefits for:
- Improving First Contentful Paint (FCP) on CSS-heavy sites
- Reducing Critical Rendering Path when every millisecond counts
- Sites where above-the-fold content depends heavily on CSS
Without This Plugin
If you want to use critical CSS/JS with vite-plugin-shopify, you must manually:
- Remove hashing from critical file names via custom
assetFileNameslogic - Manually include critical files in Liquid using
inline_asset_content - Lose hot module reloading for critical files during development
- Manually rebuild when critical CSS/JS changes
This plugin automates all of this.
Features
- Simple naming convention - Prefix files with
critical-to mark them as critical - Development mode - Full HMR support via Vite's dev server
- Production builds - Automatic inlining with
inline_asset_content - Generated Liquid snippet - Same usage syntax in dev and prod
- Zero configuration - Works out of the box with
vite-plugin-shopifydefaults
Requirements
- Node.js >= 25.0.0
- Vite >= 7.0.0
Installation
npm install -D vite-plugin-shopify-criticalConfiguration
Basic Setup
// vite.config.ts
import { defineConfig } from "vite";
import shopify from "vite-plugin-shopify";
import shopifyCritical from "vite-plugin-shopify-critical";
export default defineConfig({
plugins: [
shopify(),
shopifyCritical(),
],
});Options
| Option | Type | Default | Description |
|------------------|----------|------------------------------|-----------------------------------------------------------------------------|
| entrypointsDir | string | "frontend/entrypoints" | Path to entrypoints folder. Should match vite-plugin-shopify's setting. |
| snippetFile | string | "vite-critical-tag.liquid" | Name of the generated Liquid snippet file. |
| themeRoot | string | "./" | Shopify theme root directory. Should match vite-plugin-shopify's setting. |
Custom Configuration Example
import { defineConfig } from "vite";
import shopify from "vite-plugin-shopify";
import shopifyCritical from "vite-plugin-shopify-critical";
export default defineConfig({
plugins: [
shopify({
entrypointsDir: "src/entrypoints",
themeRoot: "./",
}),
shopifyCritical({
entrypointsDir: "src/entrypoints",
themeRoot: "./",
}),
],
});Folder Structure
Critical assets go in the same folder as your regular entrypoints, but with a critical- prefix:
your-theme/
├── frontend/
│ └── entrypoints/
│ ├── critical-theme.scss ← Critical CSS (prefix: critical-)
│ ├── critical-above-fold.scss
│ ├── critical-analytics.ts ← Critical JS (prefix: critical-)
│ ├── theme.scss ← Regular assets (no prefix)
│ └── theme.ts
├── assets/ ← Compiled files (managed by Vite)
├── snippets/
│ ├── vite-tag.liquid ← Generated by vite-plugin-shopify
│ └── vite-critical-tag.liquid ← Generated by vite-plugin-shopify-critical
└── vite.config.tsUsage
In Liquid Templates
Use the generated vite-critical-tag snippet to include your critical assets.
Important: The placement of your stylesheets matters. Read the Critical CSS? Not So Fast! article to understand why the full stylesheet should be at </body>, not in <head>.
Basic Usage
<!DOCTYPE html>
<html lang="en">
<head>
{%- comment -%}Critical CSS - inlined in production{%- endcomment -%}
{%- render 'vite-critical-tag', entry: 'critical-theme.scss' -%}
{%- render 'vite-critical-tag', entry: 'critical-above-fold.scss' -%}
{%- comment -%}Critical JS - inlined in production{%- endcomment -%}
{%- render 'vite-critical-tag', entry: 'critical-analytics.ts' -%}
{%- comment -%}Regular JS - safe in head (type="module" is deferred){%- endcomment -%}
{%- render 'vite-tag', entry: 'theme.ts' -%}
</head>
<body>
<!-- ... page content ... -->
{%- comment -%}Full stylesheet at </body> to avoid render-blocking{%- endcomment -%}
{%- render 'vite-tag', entry: 'theme.scss' -%}
</body>
</html>Code Splitting Example
Load page-specific JavaScript only where needed:
<!DOCTYPE html>
<html lang="en">
<head>
{%- render 'vite-critical-tag', entry: 'critical-theme.scss' -%}
{%- render 'vite-critical-tag', entry: 'critical-analytics.ts' -%}
{%- render 'vite-tag', entry: 'theme.ts' -%}
{%- if template.name == 'product' -%}
{%- render 'vite-tag', entry: 'product.ts' -%}
{%- endif -%}
{%- if template.name == 'cart' -%}
{%- render 'vite-tag', entry: 'cart.ts' -%}
{%- endif -%}
</head>
<body>
<!-- ... page content ... -->
{%- render 'vite-tag', entry: 'theme.scss' -%}
</body>
</html>Entry Names
Use the source filename (including critical- prefix and extension) as the entry parameter:
| Source File | Entry Name |
|---------------------------|------------------------------------|
| critical-theme.scss | entry: 'critical-theme.scss' |
| critical-above-fold.css | entry: 'critical-above-fold.css' |
| critical-analytics.ts | entry: 'critical-analytics.ts' |
How It Works
Development Mode
In development, the plugin generates a snippet that outputs <link> and <script> tags pointing to Vite's dev server:
{%- comment -%}
IMPORTANT: This snippet is automatically generated by vite-plugin-shopify-critical.
Do not attempt to modify this file directly, as any changes will be overwritten.
{%- endcomment -%}
{%- liquid
assign file_url_prefix = 'http://localhost:5173/frontend/entrypoints/'
assign file_url = entry | prepend: file_url_prefix
-%}
{%- if is_css -%}
<link rel="stylesheet" href="{{- file_url -}}" crossorigin="anonymous">
{%- else -%}
<script src="{{- file_url -}}" type="module"></script>
{%- endif -%}This means:
- Full HMR support - Changes to critical files trigger hot updates just like any other file
- Same dev experience - No difference from regular Vite development
Production Build
In production, the plugin scans the bundle for files starting with critical- and generates a snippet using inline_asset_content:
{%- comment -%}
IMPORTANT: This snippet is automatically generated by vite-plugin-shopify-critical.
Do not attempt to modify this file directly, as any changes will be overwritten.
{%- endcomment -%}
{%- case entry -%}
{%- when 'critical-theme.scss' -%}
<style>{{- 'critical-theme-BxqFivnI.css' | inline_asset_content -}}</style>
{%- when 'critical-analytics.ts' -%}
<script>{{- 'critical-analytics-a1b2c3d4.js' | inline_asset_content -}}</script>
{%- endcase -%}This means:
- Automatic inlining - CSS is wrapped in
<style>, JS in<script> - Cache busting - Hashed filenames ensure proper cache invalidation
- Zero runtime cost - Content is inlined directly into the HTML
CSS Loading Strategy
Understanding where to place your stylesheets is crucial for performance. We recommend reading Critical CSS? Not So Fast! by Harry Roberts for an in-depth explanation.
Key Concepts
Why the full stylesheet should be at </body>, not in <head>:
Anything synchronous in the <head> is render-blocking by definition. Even with critical CSS inlined, if your full stylesheet is in <head>, it will still block rendering. The browser must parse the entire <head> before it can start rendering the page.
What about the media="print" hack?
You may have seen the defer non-critical CSS technique from web.dev that uses media="print" with an onload handler to async-load CSS:
<link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'">This approach has problems:
- Lowest network priority - Browsers assign
media="print"stylesheets the lowest fetch priority, causing very slow download times (12+ seconds in real-world tests) - Race condition - If the async CSS arrives while the browser is still parsing
<head>, it becomes render-blocking anyway, negating the benefit - Unpredictable timing - You have no control over when the stylesheet loads and applies
Placing the stylesheet at </body> is more predictable and avoids these issues entirely.
JavaScript is safe in <head>:
Vite outputs <script type="module"> tags, which are deferred by default. This means JavaScript in <head> won't block rendering - it loads in parallel and executes after the DOM is ready.
Recommended Loading Order
<head>
1. Critical CSS (inlined via this plugin)
2. Critical JS (inlined via this plugin)
3. Regular JS (deferred, safe in head)
</head>
<body>
... page content ...
4. Full stylesheet (at closing </body> to avoid blocking)
</body>Trade-offs to Consider
Placing stylesheets at </body> has trade-offs:
- FOUC risk - Users may see unstyled content briefly if they scroll before styles load
- Layout shifts - Late-loading styles can cause cumulative layout shift (CLS)
- Double paint - The page renders with critical CSS, then re-renders when full styles apply
For most Shopify themes, these trade-offs are acceptable because:
- Critical CSS handles above-the-fold content
- Users typically don't scroll instantly
- The performance gain from non-blocking CSS outweighs the brief unstyled state
Supported File Types
The plugin supports any CSS file type that Vite supports:
.css,.scss,.sass,.less,.styl,.stylus,.pcss,.postcss
All other files are treated as JavaScript and wrapped in <script> tags.
API Reference
shopifyCritical(options?)
Creates the plugin. Returns an array of Vite plugins.
interface ShopifyCriticalOptions {
/**
* Path to the entrypoints folder.
* Should match vite-plugin-shopify's entrypointsDir.
* @default "frontend/entrypoints"
*/
entrypointsDir?: string;
/**
* Name of the generated Liquid snippet file.
* @default "vite-critical-tag.liquid"
*/
snippetFile?: string;
/**
* Shopify theme root directory.
* @default "./"
*/
themeRoot?: string;
}Exported Types
import type {
ShopifyCriticalOptions,
ResolvedOptions,
CriticalFileMapping,
} from "vite-plugin-shopify-critical";Troubleshooting
Critical files not being inlined in production
Ensure your critical files:
- Are prefixed with
critical-(e.g.,critical-theme.scss) - Are in the entrypoints directory
- Are being processed by Vite (check the build output)
- Are under 15KB (check file sizes in Vite build output; Shopify will show an error in the page source if exceeded)
SCSS/SASS files not compiling
Install a sass compiler:
npm install -D sass
# or for faster builds
npm install -D sass-embeddedSee Vite's CSS documentation for more details.
Snippet not updating
The snippet is regenerated on every dev server start and production build. If you're seeing stale content, restart the dev server or rebuild.
File Size Limit
Shopify's inline_asset_content filter has a 15KB limit per file. Each critical asset must be under 15KB to be inlined successfully. You can split critical CSS into multiple files to stay under this limit.
Best Practices
- Keep critical CSS small - Only include above-the-fold styles, each file under 15KB
- Use the
critical-prefix consistently - All critical files must start withcritical- - Match entrypointsDir - Ensure both plugins use the same entrypoints directory
- Test production builds - Verify inlining works correctly before deploying
Example Project Structure
my-shopify-theme/
├── frontend/
│ └── entrypoints/
│ ├── critical-reset.scss # Critical: CSS reset
│ ├── critical-typography.scss # Critical: Font styles
│ ├── critical-layout.scss # Critical: Above-fold layout
│ ├── critical-analytics.ts # Critical: Analytics snippet
│ ├── theme.scss # Regular: Full stylesheet
│ └── theme.ts # Regular: Main JavaScript
├── assets/
├── blocks/
├── config/
├── layout/
│ └── theme.liquid
├── locales/
├── sections/
├── snippets/
│ ├── vite-tag.liquid # Generated by vite-plugin-shopify
│ └── vite-critical-tag.liquid # Generated by this plugin
├── templates/
├── package.json
└── vite.config.tsRelated
- vite-plugin-shopify - Core Vite plugin for Shopify themes
- inline_asset_content - Shopify's inline asset filter documentation
- Critical CSS? Not So Fast! - Important article on CSS loading strategy
License
MIT
