Production Build Configuration
Configure your React Shopify theme for production deployment. Learn build optimization, environment variables, minification settings, and output configuration for Shopify themes.
The build configuration determines how your React code transforms into production-ready assets for your Shopify theme. A well-configured build produces optimized bundles, handles environment differences, and outputs files in the format Shopify expects.
Build Pipeline Overview
Your React Shopify theme build follows this flow:
┌───────────────────────────────────────────────────────────────────────────────┐│ BUILD PIPELINE │├───────────────────────────────────────────────────────────────────────────────┤│ ││ src/ ││ ├── components/ ─┐ ││ ├── hooks/ │ ││ ├── lib/ ├──▶ Vite Build ──▶ dist/assets/ ││ ├── styles/ │ (transpile, ├── main.[hash].js ││ └── entries/ ─┘ bundle, ├── vendor.[hash].js ││ minify) ├── main.[hash].css ││ └── ... ││ │ ││ ▼ ││ Copy to theme ││ │ ││ ▼ ││ theme/assets/ ││ ├── main.js ││ ├── vendor.js ││ └── main.css ││ │└───────────────────────────────────────────────────────────────────────────────┘Complete Vite Configuration
Here’s a production-ready Vite configuration:
import { defineConfig, loadEnv } from 'vite';import react from '@vitejs/plugin-react';import { resolve } from 'path';
export default defineConfig(({ mode }) => { // Load env files based on mode const env = loadEnv(mode, process.cwd(), '');
return { plugins: [ react({ // Use automatic JSX runtime (no need to import React) jsxRuntime: 'automatic', }), ],
// Path aliases resolve: { alias: { '@': resolve(__dirname, 'src'), '@components': resolve(__dirname, 'src/components'), '@hooks': resolve(__dirname, 'src/hooks'), '@lib': resolve(__dirname, 'src/lib'), '@styles': resolve(__dirname, 'src/styles'), }, },
// Build configuration build: { // Output directory (we'll copy to theme/assets later) outDir: 'dist',
// Generate source maps for debugging (disable in production for smaller files) sourcemap: mode === 'development',
// Minimum browser targets target: ['es2020', 'edge88', 'firefox78', 'chrome87', 'safari14'],
// Minification settings minify: 'terser', terserOptions: { compress: { // Remove console.log in production drop_console: mode === 'production', drop_debugger: true, }, format: { comments: false, // Remove comments }, },
// CSS minification cssMinify: true,
// Rollup-specific options rollupOptions: { // Multiple entry points input: { main: resolve(__dirname, 'src/entries/main.tsx'), product: resolve(__dirname, 'src/entries/product.tsx'), collection: resolve(__dirname, 'src/entries/collection.tsx'), cart: resolve(__dirname, 'src/entries/cart.tsx'), account: resolve(__dirname, 'src/entries/account.tsx'), },
output: { // Naming patterns entryFileNames: '[name].js', chunkFileNames: '[name]-[hash].js', assetFileNames: (assetInfo) => { // Keep CSS names predictable if (assetInfo.name?.endsWith('.css')) { return '[name].css'; } return 'assets/[name]-[hash][extname]'; },
// Manual chunk splitting manualChunks: { 'vendor-react': ['react', 'react-dom'], 'vendor-motion': ['framer-motion'], }, }, },
// Chunk size warnings chunkSizeWarningLimit: 500, // KB },
// Environment variable handling define: { 'process.env.NODE_ENV': JSON.stringify(mode), __DEV__: mode === 'development', },
// Development server (for standalone testing) server: { port: 3000, strictPort: true, }, };});Environment Variables
Handle different environments properly:
VITE_API_URL=http://localhost:3000VITE_DEBUG=trueVITE_ANALYTICS_ID=
# .env.productionVITE_API_URL=https://api.yourstore.comVITE_DEBUG=falseVITE_ANALYTICS_ID=UA-XXXXXXXXAccess in code:
export const config = { isDev: import.meta.env.DEV, isProd: import.meta.env.PROD, apiUrl: import.meta.env.VITE_API_URL, debug: import.meta.env.VITE_DEBUG === 'true', analyticsId: import.meta.env.VITE_ANALYTICS_ID,} as const;
// Type-safe environment variablesdeclare global { interface ImportMetaEnv { readonly VITE_API_URL: string; readonly VITE_DEBUG: string; readonly VITE_ANALYTICS_ID: string; }}Entry Points Configuration
Organize entry points for different pages:
/** * Main entry point - loaded on every page * Contains: header, footer, cart drawer, search */import { initializeCore } from '@/lib/core';import { mountHeader } from '@/components/layout/Header';import { mountCartDrawer } from '@/components/cart/CartDrawer';
// Initialize core functionalityinitializeCore();
// Mount global componentsmountHeader();mountCartDrawer();/** * Product page entry point * Contains: gallery, variant selector, add to cart */import { mountProductPage } from '@/components/product/ProductPage';
// Only mount if on product pageif (document.querySelector('[data-product-root]')) { mountProductPage();}Build Scripts
Configure npm scripts for different scenarios:
{ "scripts": { "dev": "vite", "build": "npm run build:clean && npm run build:vite && npm run build:copy", "build:clean": "rm -rf dist && rm -rf theme/assets/*.js theme/assets/*.css", "build:vite": "vite build", "build:copy": "node scripts/copy-to-theme.js", "build:analyze": "vite build --mode analyze", "preview": "vite preview", "type-check": "tsc --noEmit" }}Copying Assets to Theme
Script to copy built files to the Shopify theme:
import { copyFileSync, readdirSync, mkdirSync, existsSync } from 'fs';import { join, basename } from 'path';
const DIST_DIR = 'dist';const THEME_ASSETS_DIR = 'theme/assets';
// Ensure theme/assets existsif (!existsSync(THEME_ASSETS_DIR)) { mkdirSync(THEME_ASSETS_DIR, { recursive: true });}
// Copy JS and CSS filesconst files = readdirSync(DIST_DIR).filter( (file) => file.endsWith('.js') || file.endsWith('.css'));
for (const file of files) { const src = join(DIST_DIR, file); const dest = join(THEME_ASSETS_DIR, file);
copyFileSync(src, dest); console.log(`Copied: ${file}`);}
// Also copy from dist/assets if existsconst assetsDir = join(DIST_DIR, 'assets');if (existsSync(assetsDir)) { const assetFiles = readdirSync(assetsDir); for (const file of assetFiles) { const src = join(assetsDir, file); const dest = join(THEME_ASSETS_DIR, file); copyFileSync(src, dest); console.log(`Copied: assets/${file}`); }}
console.log(`\n✓ Copied ${files.length} files to ${THEME_ASSETS_DIR}`);Integrating with Liquid
Reference built assets in your theme:
{% comment %} layout/theme.liquid {% endcomment %}<!DOCTYPE html><html><head> {%- comment -%} CSS - loaded before content {%- endcomment -%} {{ 'main.css' | asset_url | stylesheet_tag }}
{%- comment -%} Preload critical JS {%- endcomment -%} <link rel="modulepreload" href="{{ 'main.js' | asset_url }}"> <link rel="modulepreload" href="{{ 'vendor-react.js' | asset_url }}"></head><body> {{ content_for_header }}
{{ content_for_layout }}
{%- comment -%} JS - loaded at end of body {%- endcomment -%} <script type="module" src="{{ 'main.js' | asset_url }}"></script>
{%- comment -%} Page-specific JS {%- endcomment -%} {%- case template.name -%} {%- when 'product' -%} <script type="module" src="{{ 'product.js' | asset_url }}"></script> {%- when 'collection' -%} <script type="module" src="{{ 'collection.js' | asset_url }}"></script> {%- when 'cart' -%} <script type="module" src="{{ 'cart.js' | asset_url }}"></script> {%- endcase -%}</body></html>Conditional Development/Production Loading
Load different assets in development vs production:
{% comment %} snippets/react-assets.liquid {% endcomment %}
{%- if settings.dev_mode -%} {%- comment -%} Development: Load from Vite dev server {%- endcomment -%} <script type="module" src="http://localhost:3000/@vite/client"></script> <script type="module" src="http://localhost:3000/src/entries/main.tsx"></script>{%- else -%} {%- comment -%} Production: Load built assets {%- endcomment -%} <script type="module" src="{{ 'main.js' | asset_url }}"></script>{%- endif -%}Add to theme settings:
{ "name": "Developer Settings", "settings": [ { "type": "checkbox", "id": "dev_mode", "label": "Development Mode", "default": false, "info": "Enable to load assets from local Vite dev server" } ]}TypeScript Configuration
Optimize TypeScript for production:
{ "compilerOptions": { "target": "ES2020", "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", "strict": true, "noEmit": true, "skipLibCheck": true, "isolatedModules": true, "esModuleInterop": true, "resolveJsonModule": true, "declaration": false, "declarationMap": false, "baseUrl": ".", "paths": { "@/*": ["src/*"], "@components/*": ["src/components/*"], "@hooks/*": ["src/hooks/*"], "@lib/*": ["src/lib/*"] } }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"]}Production Checklist
Before deploying:
┌─────────────────────────────────────────────────────────────────┐│ PRODUCTION BUILD CHECKLIST │├─────────────────────────────────────────────────────────────────┤│ ││ □ Environment ││ □ NODE_ENV=production ││ □ Remove development-only code ││ □ Verify environment variables ││ ││ □ Bundle Optimization ││ □ Minification enabled ││ □ Tree shaking working ││ □ Code splitting configured ││ □ Source maps disabled (or hidden) ││ ││ □ Console/Debug ││ □ Console.log removed ││ □ Debug panels disabled ││ □ Error reporting configured ││ ││ □ Assets ││ □ CSS minified ││ □ Images optimized ││ □ Fonts subset ││ ││ □ Testing ││ □ All tests passing ││ □ Type checking passes ││ □ Build completes without errors ││ □ Manual smoke test on staging ││ │└─────────────────────────────────────────────────────────────────┘Build Validation Script
Verify the build before deployment:
import { existsSync, statSync, readFileSync } from 'fs';import { join } from 'path';
interface ValidationResult { passed: boolean; checks: { name: string; passed: boolean; message: string }[];}
function validateBuild(): ValidationResult { const checks: ValidationResult['checks'] = []; const distDir = 'dist'; const themeAssetsDir = 'theme/assets';
// Check dist directory exists checks.push({ name: 'Dist directory exists', passed: existsSync(distDir), message: existsSync(distDir) ? 'Found dist/' : 'Missing dist/ - run build first', });
// Check main.js exists const mainJs = join(distDir, 'main.js'); checks.push({ name: 'main.js exists', passed: existsSync(mainJs), message: existsSync(mainJs) ? 'Found main.js' : 'Missing main.js', });
// Check bundle size if (existsSync(mainJs)) { const size = statSync(mainJs).size / 1024; const passed = size < 200; // 200KB limit checks.push({ name: 'main.js size', passed, message: `${size.toFixed(1)}KB ${passed ? '(under 200KB limit)' : '(exceeds 200KB limit)'}`, }); }
// Check for console.log in production code if (existsSync(mainJs)) { const content = readFileSync(mainJs, 'utf-8'); const hasConsole = content.includes('console.log'); checks.push({ name: 'No console.log', passed: !hasConsole, message: hasConsole ? 'Found console.log - should be removed' : 'No console.log found', }); }
// Check vendor chunk exists const vendorPattern = /vendor-react.*\.js$/; const files = existsSync(distDir) ? require('fs').readdirSync(distDir) : []; const hasVendor = files.some((f: string) => vendorPattern.test(f)); checks.push({ name: 'Vendor chunk exists', passed: hasVendor, message: hasVendor ? 'Found vendor chunk' : 'Missing vendor chunk', });
return { passed: checks.every((c) => c.passed), checks, };}
// Run validationconst result = validateBuild();
console.log('\nBuild Validation Results\n' + '='.repeat(50));
for (const check of result.checks) { const icon = check.passed ? '✓' : '✗'; console.log(`${icon} ${check.name}: ${check.message}`);}
console.log('\n' + '='.repeat(50));console.log(result.passed ? '✓ All checks passed!' : '✗ Some checks failed');
process.exit(result.passed ? 0 : 1);Key Takeaways
-
Multiple entry points: Create separate entries for different pages to enable code splitting.
-
Environment handling: Use Vite’s environment variables for dev/prod differences.
-
Minification matters: Configure Terser to remove console.log and comments in production.
-
Vendor splitting: Separate React and other vendors for better caching.
-
Validate before deploy: Run automated checks to catch issues early.
-
Liquid integration: Conditionally load dev server or production assets.
-
Type safety: TypeScript catches errors at build time, not runtime.
-
Copy to theme: Automate copying built assets to the Shopify theme directory.
In the next lesson, we’ll cover asset versioning and cache busting to ensure users always get the latest code.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...