npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vite-plugin-shopify-critical

v1.0.1

Published

Vite plugin for inlining critical CSS/JS in Shopify themes using inline_asset_content

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:

  1. Remove hashing from critical file names via custom assetFileNames logic
  2. Manually include critical files in Liquid using inline_asset_content
  3. Lose hot module reloading for critical files during development
  4. 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-shopify defaults

Requirements

  • Node.js >= 25.0.0
  • Vite >= 7.0.0

Installation

npm install -D vite-plugin-shopify-critical

Configuration

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.ts

Usage

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:

  1. Lowest network priority - Browsers assign media="print" stylesheets the lowest fetch priority, causing very slow download times (12+ seconds in real-world tests)
  2. Race condition - If the async CSS arrives while the browser is still parsing <head>, it becomes render-blocking anyway, negating the benefit
  3. 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:

  1. Are prefixed with critical- (e.g., critical-theme.scss)
  2. Are in the entrypoints directory
  3. Are being processed by Vite (check the build output)
  4. 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-embedded

See 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

  1. Keep critical CSS small - Only include above-the-fold styles, each file under 15KB
  2. Use the critical- prefix consistently - All critical files must start with critical-
  3. Match entrypointsDir - Ensure both plugins use the same entrypoints directory
  4. 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.ts

Related

License

MIT