Performance Optimization Intermediate 10 min read

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:

Terminal window
npm install -D rollup-plugin-visualizer

Configure it in your Vite config:

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

Terminal window
npm run build

This 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:

  1. Unexpectedly large dependencies: Is lodash really 72KB? You’re probably importing it wrong.
  2. Duplicate packages: Multiple versions of the same library.
  3. Unused code: Large chunks that shouldn’t be in the main bundle.
  4. 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 import
import 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 import
import { motion, AnimatePresence, useAnimation } from 'framer-motion';
// GOOD: Lazy load animation features
const AnimatedComponent = lazy(() =>
import('@/components/AnimatedComponent')
);
// Or use CSS animations where possible

Ensuring 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-shaken
console.log('Module loaded');
window.myGlobal = {};
export function myFunction() {}
// GOOD: No side effects
export function myFunction() {}
// Initialize only when called
export function initialize() {
console.log('Initialized');
window.myGlobal = {};
}

Use Named Exports

utils.ts
// GOOD: Named exports can be tree-shaken individually
export function formatMoney(amount: number): string { /* ... */ }
export function formatDate(date: Date): string { /* ... */ }
export function slugify(text: string): string { /* ... */ }
// BAD: Default export of object - entire object included
export default {
formatMoney,
formatDate,
slugify,
};

Analyzing Specific Chunks

For deeper analysis, generate detailed stats:

vite.config.ts
visualizer({
filename: 'dist/stats.html',
sourcemap: true, // Analyze by original source files
projectRoot: process.cwd(),
})

Or use source-map-explorer for existing bundles:

Terminal window
npm install -D source-map-explorer
npx source-map-explorer dist/assets/*.js

Creating a Bundle Budget

Set limits to catch regressions:

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

scripts/check-bundle-size.ts
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:

Terminal window
# List duplicate packages
npm ls --all | grep -E "^[├└]" | sort | uniq -d
# Or use npm-dedupe
npx npm-dedupe

Fix 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'; // 32KB
import { format } from 'date-fns'; // 5.2KB
import _ 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

  1. Analyze regularly: Run bundle analysis after adding dependencies or making major changes.

  2. Question every dependency: Do you really need that library? Can you use a smaller alternative or native APIs?

  3. Import precisely: import { specific } from 'lib' instead of import lib from 'lib'.

  4. Verify tree shaking: Check that unused exports are actually removed.

  5. Set budgets: Define size limits and fail builds that exceed them.

  6. Deduplicate dependencies: Multiple versions of the same package multiply your bundle size.

  7. Use editor tools: Import Cost extension provides instant feedback.

  8. Native over library: Browser APIs like Intl can 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...