perf hacks

Tree Shaking

Eliminate unused code from your production bundles

Tree shaking removes unused code from your JavaScript bundles during the build process. This can significantly reduce bundle size and improve load times.

How Tree Shaking Works

math.ts
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export function multiply(a, b) { return a * b; }
export function divide(a, b) { return a / b; }

// app.ts - Only uses add
import { add } from './math';
console.log(add(1, 2));

// After tree shaking: only add() is in bundle
// subtract, multiply, divide are removed

Enabling Effective Tree Shaking

Use ES Modules

// ✅ Good: ES modules are tree-shakeable
import { debounce } from 'lodash-es';

// ❌ Bad: CommonJS imports entire library
const _ = require('lodash');
const { debounce } = require('lodash');

Named Imports vs Default

// ✅ Good: Named imports tree-shake well
import { Button, Input } from '@/components/ui';

// ⚠️ Careful: Barrel files can prevent tree shaking
// If components/ui/index.ts re-exports everything
import { Button } from '@/components/ui'; // May load all components

Configure sideEffects

package.json
{
  "sideEffects": false
}

// Or specify files with side effects
{
  "sideEffects": [
    "*.css",
    "*.scss",
    "./src/polyfills.js"
  ]
}

Common Tree Shaking Issues

Barrel Files

components/index.ts
export * from './Button';
export * from './Input';
export * from './Select';
export * from './Modal';
// ... 50 more components

// ❌ Problem: Importing one may bundle all
import { Button } from '@/components';

// ✅ Solution: Direct imports
import { Button } from '@/components/Button';

Dynamic Property Access

// ❌ Bad: Dynamic access prevents tree shaking
const icons = {
  home: HomeIcon,
  settings: SettingsIcon,
  user: UserIcon,
};

function Icon({ name }) {
  const IconComponent = icons[name]; // All icons bundled
  return <IconComponent />;
}

// ✅ Good: Static imports
import { HomeIcon } from './icons/HomeIcon';

function Icon({ name }) {
  switch (name) {
    case 'home': return <HomeIcon />;
    case 'settings': return <SettingsIcon />;
    // Only imported icons are bundled
  }
}

Class Instantiation

// ❌ Bad: Classes are harder to tree shake
import { Validator } from './validators';
const v = new Validator();

// ✅ Good: Pure functions tree shake better
import { validateEmail } from './validators';
validateEmail(email);

Next.js Optimization

modularizeImports

next.config.js
module.exports = {
  modularizeImports: {
    'lodash': {
      transform: 'lodash/{{member}}',
    },
    '@mui/material': {
      transform: '@mui/material/{{member}}',
    },
    '@mui/icons-material': {
      transform: '@mui/icons-material/{{member}}',
    },
  },
};

Before:

import { debounce, throttle } from 'lodash';
// Bundles entire lodash

After (with modularizeImports):

import { debounce, throttle } from 'lodash';
// Transforms to:
// import debounce from 'lodash/debounce';
// import throttle from 'lodash/throttle';

optimizePackageImports

next.config.js
module.exports = {
  experimental: {
    optimizePackageImports: [
      'lucide-react',
      '@headlessui/react',
      'date-fns',
      'rxjs',
    ],
  },
};

Library-Specific Tips

Lodash

// ❌ Bad: 70KB
import _ from 'lodash';

// ❌ Still bad: 70KB (CommonJS)
import { debounce } from 'lodash';

// ✅ Good: ~2KB
import debounce from 'lodash/debounce';

// ✅ Best: ~2KB (ES modules)
import { debounce } from 'lodash-es';

date-fns

// ✅ Already tree-shakeable
import { format, parseISO } from 'date-fns';
// Only these functions are bundled

Material UI

// ❌ Bad: Imports all of MUI
import { Button, TextField } from '@mui/material';

// ✅ Good: Direct imports
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';

// ✅ With modularizeImports, named imports work
import { Button, TextField } from '@mui/material';

Icon Libraries

// ❌ Bad: All 1000+ icons
import { FaHome } from 'react-icons/fa';

// ✅ Good: Just one icon
import { FaHome } from 'react-icons/fa';
// This is actually fine - react-icons is already optimized

// For lucide-react, use optimizePackageImports
import { Home, Settings } from 'lucide-react';

Analyzing Tree Shaking

Bundle Analyzer

npm install @next/bundle-analyzer

# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({});

# Run analysis
ANALYZE=true npm run build

Import Cost Extension

VS Code extension that shows import sizes inline:

import { debounce } from 'lodash';  // 70KB ❌
import debounce from 'lodash/debounce';  // 2KB ✅

Writing Tree-Shakeable Code

Export Pure Functions

// ✅ Tree-shakeable
export function formatDate(date) {
  return date.toISOString();
}

export function parseDate(str) {
  return new Date(str);
}

Avoid Side Effects at Module Level

// ❌ Bad: Side effect prevents tree shaking
console.log('Module loaded');
export function helper() {}

// ✅ Good: No side effects
export function helper() {}

Use Explicit Re-exports

// ❌ Bad: Wildcard re-export
export * from './utils';

// ✅ Good: Explicit re-exports
export { formatDate, parseDate } from './utils';

Checklist

Project Setup

Use ES modules (import/export) CommonJS cannot be tree-shaken.

Add sideEffects to package.json Tells bundler which files have side effects.

Configure modularizeImports Automatically transform imports to subpaths.

Enable optimizePackageImports Next.js experimental flag for specific packages.

Import Patterns

Use named imports Named exports enable better dead code elimination.

Avoid barrel file wildcards export * can bundle everything.

Direct import from subpaths Import from 'lodash/debounce' not 'lodash'.

Avoid dynamic property access Static access enables tree shaking.

Library Selection

Prefer ESM versions (lodash-es) ESM libraries are fully tree-shakeable.

Check library tree-shake support Some libraries don't support tree shaking.

Use bundle analyzer Verify what's actually in your bundle.

Consider smaller alternatives date-fns instead of moment, etc.

Impact

IssueBundle IncreaseFix
Full lodash import+70KBDirect import
MUI barrel import+200KB+modularizeImports
All icons imported+50-500KBDirect import
moment.js+300KBUse date-fns

On this page