ES Modules Logo ESModules.com

Performance Optimization

Maximize ES Modules performance in production.

Module Preloading

Preload modules before they're needed:

<link rel="modulepreload" href="/js/heavy-module.js">
<script type="module" src="/js/app.js"></script>

✅ Benefits: Faster load times, reduced waterfall effects

Tree-Shaking

Remove unused code during bundling:

// utils.js - exports many functions
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }

// main.js - only imports what's needed
import { add } from './utils.js';
// multiply() is removed from final bundle!

Requirements:

  • Use ES Modules (not CommonJS)
  • Avoid side effects in modules
  • Use named imports/exports
  • Configure bundler correctly

Lazy Loading

Load modules only when needed:

// Load heavy feature on user interaction
button.addEventListener('click', async () => {
  const { init } = await import('./heavy-feature.js');
  init();
});

HTTP/2 & Multiplexing

ES Modules work great with HTTP/2's multiplexing:

  • Load many small modules in parallel
  • No head-of-line blocking
  • Better caching granularity
  • Server push for dependencies

Bundle Splitting

Split code into vendor, app, and route chunks:

// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          utils: ['lodash']
        }
      }
    }
  }
}

Performance Checklist

✅ Do

  • Use dynamic imports for code splitting
  • Enable tree-shaking in bundler
  • Preload critical modules
  • Minimize module depth
  • Use HTTP/2 or HTTP/3

❌ Avoid

  • Deep module dependency chains
  • Large barrel files (index.js)
  • Side effects in modules
  • Circular dependencies
  • Importing everything from large libraries

Barrel File Performance Pitfalls

Barrel files (index.js re-exports) can silently destroy tree-shaking and inflate bundle sizes:

// utils/index.js - barrel file re-exports everything
export { formatDate } from './date.js';
export { formatCurrency } from './currency.js';
export { heavyChartLib } from './charts.js'; // 200KB!

// consumer.js - only needs formatDate, but...
import { formatDate } from './utils';
// Some bundlers may include ALL exports from the barrel!

Best Practices:

  • Import directly from source files when possible
  • Mark barrel files with "sideEffects": false in package.json
  • Use import defer (Stage 3) to defer heavy barrel imports
  • Analyze your bundle to identify barrel file bloat

Deferred Module Loading

The import defer proposal (TC39 Stage 3) provides a middle ground between eager static imports and lazy dynamic imports:

// Eager: executes immediately on page load
import { track } from './analytics.js';

// Deferred: loaded but NOT executed until accessed
import defer * as analytics from './analytics.js';

// Dynamic: not loaded at all until called
const analytics = await import('./analytics.js');

// Deferred is ideal for:
// - Modules needed synchronously but not at startup
// - Reducing main thread blocking during page load
// - Features that are likely but not guaranteed to be used

Continue Learning