Bundle Analysis and Tree Shaking
Analyze your React Shopify theme bundles to identify bloat. Learn to use bundle analyzers, optimize imports, and ensure tree shaking works correctly.
You can’t optimize what you can’t measure. Bundle analysis reveals exactly what’s in your JavaScript files—which dependencies are bloating your build, what code isn’t being tree-shaken, and where you’re accidentally importing entire libraries when you only need one function.
Setting Up Bundle Analysis
Install the bundle analyzer for Vite:
npm install -D rollup-plugin-visualizerConfigure it in your Vite config:
import { defineConfig } from 'vite';import react from '@vitejs/plugin-react';import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({ plugins: [ react(), visualizer({ filename: 'dist/stats.html', open: true, // Opens automatically after build gzipSize: true, brotliSize: true, template: 'treemap', // or 'sunburst', 'network' }), ],});Run a production build:
npm run buildThis generates an interactive visualization showing every module in your bundle.
Reading Bundle Analysis
The visualizer shows your bundle as a treemap where size equals file size:
┌─────────────────────────────────────────────────────────────────┐│ BUNDLE ANALYSIS TREEMAP │├─────────────────────────────────────────────────────────────────┤│ ││ ┌──────────────────────────────────┐ ┌───────────────────────┐ ││ │ │ │ │ ││ │ react-dom │ │ framer-motion │ ││ │ (130KB) │ │ (32KB) │ ││ │ │ │ │ ││ └──────────────────────────────────┘ └───────────────────────┘ ││ ┌────────────────┐ ┌───────────────┐ ┌─────────┐ ┌──────────┐ ││ │ │ │ │ │ │ │ │ ││ │ components/ │ │ lodash │ │ zustand │ │ utils/ │ ││ │ (85KB) │ │ (72KB) ❌ │ │ (14KB) │ │ (12KB) │ ││ │ │ │ │ │ │ │ │ ││ └────────────────┘ └───────────────┘ └─────────┘ └──────────┘ ││ ││ ❌ = Unexpected large dependency (investigate!) ││ │└─────────────────────────────────────────────────────────────────┘Look for:
- Unexpectedly large dependencies: Is lodash really 72KB? You’re probably importing it wrong.
- Duplicate packages: Multiple versions of the same library.
- Unused code: Large chunks that shouldn’t be in the main bundle.
- Dev dependencies in production: Testing libraries or dev tools.
Common Bundle Bloat Culprits
1. Importing Entire Libraries
// BAD: Imports all of lodash (72KB)import _ from 'lodash';const sorted = _.sortBy(items, 'name');
// GOOD: Import only what you need (4KB)import sortBy from 'lodash/sortBy';const sorted = sortBy(items, 'name');
// BEST: Use native methods when possible (0KB)const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));2. Icon Libraries
// BAD: Imports entire icon set (500KB+)import { FaShoppingCart } from 'react-icons/fa';
// BETTER: Specific icon importimport FaShoppingCart from 'react-icons/fa/FaShoppingCart';
// BEST: Use SVG directly (smallest)function CartIcon({ className }: { className?: string }) { return ( <svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor"> <circle cx="9" cy="21" r="1" /> <circle cx="20" cy="21" r="1" /> <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" /> </svg> );}3. Date Libraries
// BAD: moment.js with all locales (300KB+)import moment from 'moment';
// BETTER: date-fns with tree shaking (specific functions only)import { format, parseISO } from 'date-fns';
// BEST: Native Intl API for simple formatting (0KB)function formatDate(date: Date): string { return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric', }).format(date);}
function formatPrice(amount: number, currency: string): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency, }).format(amount);}4. Animation Libraries
// Only import what you use from Framer Motion// BAD: Full importimport { motion, AnimatePresence, useAnimation } from 'framer-motion';
// GOOD: Lazy load animation featuresconst AnimatedComponent = lazy(() => import('@/components/AnimatedComponent'));
// Or use CSS animations where possibleEnsuring Tree Shaking Works
Tree shaking removes unused exports from your bundle. It only works with ES modules (import/export), not CommonJS (require).
Check Package Compatibility
Look for "module" or "exports" in package.json:
{ "name": "some-package", "main": "dist/index.cjs", "module": "dist/index.mjs", "exports": { ".": { "import": "./dist/index.mjs", "require": "./dist/index.cjs" } }, "sideEffects": false}"module"or ESM exports = tree-shakeable"sideEffects": false= bundler can safely remove unused code
Mark Your Own Code as Side-Effect-Free
In your package.json:
{ "sideEffects": [ "*.css", "*.scss", "./src/styles/**/*" ]}This tells the bundler that only CSS files have side effects; all JS can be tree-shaken.
Avoid Side Effects in Module Scope
// BAD: Side effect at module level - can't be tree-shakenconsole.log('Module loaded');window.myGlobal = {};
export function myFunction() {}
// GOOD: No side effectsexport function myFunction() {}
// Initialize only when calledexport function initialize() { console.log('Initialized'); window.myGlobal = {};}Use Named Exports
// GOOD: Named exports can be tree-shaken individuallyexport function formatMoney(amount: number): string { /* ... */ }export function formatDate(date: Date): string { /* ... */ }export function slugify(text: string): string { /* ... */ }
// BAD: Default export of object - entire object includedexport default { formatMoney, formatDate, slugify,};Analyzing Specific Chunks
For deeper analysis, generate detailed stats:
visualizer({ filename: 'dist/stats.html', sourcemap: true, // Analyze by original source files projectRoot: process.cwd(),})Or use source-map-explorer for existing bundles:
npm install -D source-map-explorernpx source-map-explorer dist/assets/*.jsCreating a Bundle Budget
Set limits to catch regressions:
export default defineConfig({ build: { rollupOptions: { output: { // Warn if chunks exceed size limit experimentalMinChunkSize: 10000, // 10KB minimum }, }, // Warn at these thresholds chunkSizeWarningLimit: 500, // KB },});Create a custom budget checker:
import { readFileSync, readdirSync, statSync } from 'fs';import { join } from 'path';import { gzipSync } from 'zlib';
interface BundleBudget { [pattern: string]: number; // Size in KB (gzipped)}
const budgets: BundleBudget = { 'main': 100, // Main bundle 'vendor': 150, // React + dependencies 'product': 50, // Product page chunk 'cart': 40, // Cart chunk 'account': 45, // Account pages chunk};
function checkBundles() { const distDir = 'dist/assets'; const files = readdirSync(distDir).filter(f => f.endsWith('.js'));
let failed = false;
for (const file of files) { const content = readFileSync(join(distDir, file)); const gzipped = gzipSync(content); const sizeKB = gzipped.length / 1024;
// Find matching budget const budgetKey = Object.keys(budgets).find(key => file.toLowerCase().includes(key) );
if (budgetKey && sizeKB > budgets[budgetKey]) { console.error( `❌ ${file}: ${sizeKB.toFixed(1)}KB exceeds budget of ${budgets[budgetKey]}KB` ); failed = true; } else { console.log(`✓ ${file}: ${sizeKB.toFixed(1)}KB`); } }
if (failed) { process.exit(1); }}
checkBundles();Add to your build process:
{ "scripts": { "build": "vite build && npm run check-bundle", "check-bundle": "tsx scripts/check-bundle-size.ts" }}Finding Duplicate Dependencies
Multiple versions of the same package bloat your bundle:
# List duplicate packagesnpm ls --all | grep -E "^[├└]" | sort | uniq -d
# Or use npm-dedupenpx npm-dedupeFix duplicates in package.json with resolutions (npm 8.3+):
{ "overrides": { "react": "18.2.0", "react-dom": "18.2.0" }}Import Cost in Your Editor
Install the “Import Cost” VS Code extension to see import sizes inline:
import { motion } from 'framer-motion'; // 32KBimport { format } from 'date-fns'; // 5.2KBimport _ from 'lodash'; // 72KB ⚠️import sortBy from 'lodash/sortBy'; // 4.1KB ✓This provides immediate feedback as you code.
Practical Optimization Checklist
┌─────────────────────────────────────────────────────────────────┐│ BUNDLE OPTIMIZATION CHECKLIST │├─────────────────────────────────────────────────────────────────┤│ ││ □ Run bundle analyzer and review treemap ││ ││ □ Check for unexpectedly large dependencies ││ □ lodash → lodash-es or individual imports ││ □ moment → date-fns or native Intl ││ □ Icon libraries → direct SVG imports ││ ││ □ Verify tree shaking is working ││ □ Use named exports ││ □ Set "sideEffects" in package.json ││ □ Import from specific paths ││ ││ □ Check for duplicate packages ││ □ Run npm ls to find duplicates ││ □ Use overrides to force single version ││ ││ □ Set up bundle budgets ││ □ Define size limits per chunk ││ □ Add to CI pipeline ││ ││ □ Split large chunks ││ □ Lazy load non-critical components ││ □ Separate vendor chunks ││ │└─────────────────────────────────────────────────────────────────┘Key Takeaways
-
Analyze regularly: Run bundle analysis after adding dependencies or making major changes.
-
Question every dependency: Do you really need that library? Can you use a smaller alternative or native APIs?
-
Import precisely:
import { specific } from 'lib'instead ofimport lib from 'lib'. -
Verify tree shaking: Check that unused exports are actually removed.
-
Set budgets: Define size limits and fail builds that exceed them.
-
Deduplicate dependencies: Multiple versions of the same package multiply your bundle size.
-
Use editor tools: Import Cost extension provides instant feedback.
-
Native over library: Browser APIs like
Intlcan replace many utility libraries.
In the next lesson, we’ll focus on critical CSS and above-the-fold optimization to ensure your theme renders fast even before JavaScript loads.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...