Build, Deploy, and Ship Intermediate 12 min read

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:

vite.config.ts
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:

.env.development
VITE_API_URL=http://localhost:3000
VITE_DEBUG=true
VITE_ANALYTICS_ID=
# .env.production
VITE_API_URL=https://api.yourstore.com
VITE_DEBUG=false
VITE_ANALYTICS_ID=UA-XXXXXXXX

Access in code:

src/lib/config.ts
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 variables
declare 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:

src/entries/main.tsx
/**
* 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 functionality
initializeCore();
// Mount global components
mountHeader();
mountCartDrawer();
src/entries/product.tsx
/**
* Product page entry point
* Contains: gallery, variant selector, add to cart
*/
import { mountProductPage } from '@/components/product/ProductPage';
// Only mount if on product page
if (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:

scripts/copy-to-theme.js
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 exists
if (!existsSync(THEME_ASSETS_DIR)) {
mkdirSync(THEME_ASSETS_DIR, { recursive: true });
}
// Copy JS and CSS files
const 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 exists
const 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:

config/settings_schema.json
{
"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:

tsconfig.json
{
"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:

scripts/validate-build.ts
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 validation
const 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

  1. Multiple entry points: Create separate entries for different pages to enable code splitting.

  2. Environment handling: Use Vite’s environment variables for dev/prod differences.

  3. Minification matters: Configure Terser to remove console.log and comments in production.

  4. Vendor splitting: Separate React and other vendors for better caching.

  5. Validate before deploy: Run automated checks to catch issues early.

  6. Liquid integration: Conditionally load dev server or production assets.

  7. Type safety: TypeScript catches errors at build time, not runtime.

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