Asset Versioning and Cache Busting
Implement effective cache strategies for your React Shopify theme. Learn content hashing, versioned filenames, and cache invalidation to ensure users get updates while maximizing performance.
When you deploy an update, users need to get the new code. But aggressive caching—essential for performance—can serve stale files. Asset versioning solves this by changing filenames when content changes, forcing browsers to fetch fresh versions while still enabling long-term caching.
The Caching Problem
┌─────────────────────────────────────────────────────────────────┐│ THE CACHING DILEMMA │├─────────────────────────────────────────────────────────────────┤│ ││ Without versioning: ││ ──────────────────── ││ main.js ──▶ Browser caches for 1 year ││ ──▶ You deploy fix ││ ──▶ User still sees old cached version ❌ ││ ││ With versioning: ││ ───────────────── ││ main.abc123.js ──▶ Browser caches for 1 year ││ ──▶ You deploy fix ││ main.def456.js ──▶ Different filename = fresh download ✓ ││ ──▶ Old file naturally expires ││ │└─────────────────────────────────────────────────────────────────┘Content Hashing in Vite
Vite automatically adds content hashes to filenames:
export default defineConfig({ build: { rollupOptions: { output: { // Include hash in filenames entryFileNames: '[name]-[hash].js', chunkFileNames: '[name]-[hash].js', assetFileNames: '[name]-[hash][extname]', }, }, },});This produces:
main-a1b2c3d4.jsvendor-e5f6g7h8.jsmain-i9j0k1l2.css
When code changes, the hash changes, and browsers fetch the new file.
Shopify Asset Handling
Shopify’s CDN adds its own caching layer. Here’s how to work with it:
Option 1: Let Shopify Handle Versioning
Shopify appends a ?v= parameter based on file modification time:
{{ 'main.js' | asset_url }}{%- comment -%} Outputs: //cdn.shopify.com/s/.../main.js?v=1234567890 {%- endcomment -%}The downside: ?v= changes on every deploy, even if the file didn’t change.
Option 2: Content-Hashed Filenames
Better approach—use content hashes in the filename itself:
{%- comment -%} Generate a manifest of hashed filenames during build {%- endcomment -%}{{ 'main-a1b2c3d4.js' | asset_url }}But how do you know the hash at build time? You generate a manifest.
Build Manifest Strategy
Generate a manifest file during build:
import { readFileSync, writeFileSync, readdirSync } from 'fs';import { join } from 'path';
interface Manifest { [key: string]: string;}
function generateManifest() { const distDir = 'dist'; const files = readdirSync(distDir);
const manifest: Manifest = {};
// Map unhashed names to hashed names for (const file of files) { if (file.endsWith('.js') || file.endsWith('.css')) { // Extract base name: main-abc123.js -> main const match = file.match(/^(.+?)-[a-f0-9]+\.(js|css)$/); if (match) { const baseName = match[1]; const ext = match[2]; manifest[`${baseName}.${ext}`] = file; } } }
// Write manifest writeFileSync( 'theme/snippets/asset-manifest.liquid', generateLiquidManifest(manifest) );
console.log('Generated asset manifest:', manifest);}
function generateLiquidManifest(manifest: Manifest): string { let liquid = '{%- comment -%}\n Auto-generated asset manifest\n Do not edit manually\n{%- endcomment -%}\n\n';
liquid += '{%- liquid\n';
for (const [key, value] of Object.entries(manifest)) { const varName = key.replace('.', '_').replace('-', '_'); liquid += ` assign ${varName} = '${value}'\n`; }
liquid += '-%}\n';
return liquid;}
generateManifest();Use in your theme:
{% comment %} layout/theme.liquid {% endcomment %}
{%- comment -%} Load the manifest {%- endcomment -%}{% render 'asset-manifest' %}
{%- comment -%} Use the versioned filenames {%- endcomment -%}<script type="module" src="{{ main_js | asset_url }}"></script><script type="module" src="{{ vendor_react_js | asset_url }}"></script><link rel="stylesheet" href="{{ main_css | asset_url }}">Liquid-Based Versioning
Alternative approach using Liquid variables:
{% comment %} snippets/asset-version.liquid {% endcomment %}
{%- comment -%} Update this version string when deploying new assets. This forces cache invalidation across all assets.{%- endcomment -%}{%- assign asset_version = 'v2.3.1' -%}{% comment %} layout/theme.liquid {% endcomment %}{% render 'asset-version' %}
<script type="module" src="{{ 'main.js' | asset_url }}?{{ asset_version }}"></script>Simple but requires manual updates.
Automated Version Injection
Inject version during build:
import { readFileSync, writeFileSync } from 'fs';import { execSync } from 'child_process';
// Get version from package.json or gitconst packageJson = JSON.parse(readFileSync('package.json', 'utf-8'));const version = packageJson.version;
// Or use git commit hashconst gitHash = execSync('git rev-parse --short HEAD').toString().trim();
// Update Liquid snippetconst template = `{%- comment -%} Auto-generated during build Version: ${version} Git: ${gitHash} Built: ${new Date().toISOString()}{%- endcomment -%}{%- assign asset_version = '${version}-${gitHash}' -%}`;
writeFileSync('theme/snippets/asset-version.liquid', template);console.log(`Injected version: ${version}-${gitHash}`);Add to build script:
{ "scripts": { "build": "npm run build:vite && npm run build:version && npm run build:copy", "build:version": "tsx scripts/inject-version.ts" }}Cache Headers Strategy
While Shopify controls CDN headers, understand the caching layers:
┌─────────────────────────────────────────────────────────────────┐│ CACHING LAYERS │├─────────────────────────────────────────────────────────────────┤│ ││ 1. Browser Cache ││ └─ Controlled by Cache-Control headers ││ └─ Shopify sets: public, max-age=31536000 (1 year) ││ └─ Content-hashed files can cache forever ││ ││ 2. Shopify CDN ││ └─ Automatically caches theme assets ││ └─ Invalidated on theme publish ││ └─ ?v= parameter forces fresh fetch ││ ││ 3. Service Worker (if you add one) ││ └─ You control this layer ││ └─ Can implement sophisticated strategies ││ │└─────────────────────────────────────────────────────────────────┘Service Worker for Advanced Caching
For more control, add a service worker:
const CACHE_NAME = 'theme-v1.0.0';
const PRECACHE_ASSETS = [ '/assets/main.js', '/assets/vendor-react.js', '/assets/main.css',];
// Install: precache critical assetsself.addEventListener('install', (event: ExtendableEvent) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => { return cache.addAll(PRECACHE_ASSETS); }) );});
// Activate: clean up old cachesself.addEventListener('activate', (event: ExtendableEvent) => { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames .filter((name) => name !== CACHE_NAME) .map((name) => caches.delete(name)) ); }) );});
// Fetch: serve from cache, fall back to networkself.addEventListener('fetch', (event: FetchEvent) => { // Only cache same-origin requests if (!event.request.url.startsWith(self.location.origin)) { return; }
// Cache-first for assets if (event.request.url.includes('/assets/')) { event.respondWith( caches.match(event.request).then((cached) => { return cached || fetch(event.request).then((response) => { // Cache the new response const clone = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(event.request, clone); }); return response; }); }) ); }});Register in your main entry:
if ('serviceWorker' in navigator && process.env.NODE_ENV === 'production') { window.addEventListener('load', () => { navigator.serviceWorker.register('/assets/service-worker.js'); });}Handling Updates
Notify users when new versions are available:
export function checkForUpdates() { if (!('serviceWorker' in navigator)) return;
navigator.serviceWorker.addEventListener('controllerchange', () => { // New service worker activated - page should refresh showUpdateNotification(); });}
function showUpdateNotification() { // Create a toast/banner const notification = document.createElement('div'); notification.className = 'update-notification'; notification.innerHTML = ` <p>A new version is available!</p> <button onclick="window.location.reload()">Refresh</button> <button onclick="this.parentElement.remove()">Later</button> `; document.body.appendChild(notification);}Debugging Cache Issues
Tools for diagnosing caching problems:
export function debugCacheStatus() { // Check what's in the browser cache if ('caches' in window) { caches.keys().then((names) => { console.log('Cache names:', names);
names.forEach((name) => { caches.open(name).then((cache) => { cache.keys().then((requests) => { console.log(`Cache "${name}" contains:`, requests.map(r => r.url)); }); }); }); }); }
// Check resource timing for cache hits const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[]; const cached = resources.filter((r) => r.transferSize === 0);
console.log('Cached resources:', cached.map((r) => r.name)); console.log('Downloaded resources:', resources.filter(r => r.transferSize > 0).map(r => ({ name: r.name.split('/').pop(), size: r.transferSize, })));}
// Run in console: debugCacheStatus()if (typeof window !== 'undefined') { (window as any).debugCacheStatus = debugCacheStatus;}Cache Invalidation Strategies
Different approaches for different scenarios:
┌─────────────────────────────────────────────────────────────────┐│ CACHE INVALIDATION STRATEGIES │├─────────────────────────────────────────────────────────────────┤│ ││ Strategy │ When to Use ││ ──────────────────┼─────────────────────────────────────────── ││ Content Hash │ JS/CSS that changes frequently ││ (main-abc123.js) │ Best for: code bundles ││ │ ││ Query String │ Assets that rarely change ││ (logo.png?v=1.0) │ Best for: images, fonts ││ │ ││ Version Folder │ Complete redesigns ││ (/v2/assets/...) │ Best for: major releases ││ │ ││ No Cache │ API responses, dynamic content ││ (Cache-Control: │ Best for: cart data, user info ││ no-store) │ ││ │└─────────────────────────────────────────────────────────────────┘Key Takeaways
-
Content hashing is essential: Changed content = changed filename = fresh download.
-
Generate manifests: Automate mapping between logical names and hashed filenames.
-
Shopify CDN helps: Theme assets are automatically cached on Shopify’s edge network.
-
Service workers add control: Implement custom caching strategies when needed.
-
Handle updates gracefully: Notify users when new versions are available.
-
Debug with tools: Use browser DevTools and custom helpers to diagnose cache issues.
-
Different strategies for different assets: Code needs hashing; images can use query strings.
-
Automate version injection: Don’t rely on manual version bumps.
In the next lesson, we’ll set up CI/CD with GitHub Actions to automate building, testing, and deploying your theme.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...