Back to blog

How to Reduce JavaScript Bundle Size: Advanced Tree Shaking and Code Splitting Techniques

In modern web development, shipping fast pages is critical for user experience and SEO index rankings. However, as developers install dependencies, bundle sizes swell. A large JavaScript bundle size blocks the browser's main thread during parsing and compile cycles, leading to sluggish pages and high Interaction to Next Paint (INP) latency.

Optimizing performance requires reducing the amount of JavaScript sent over the network.

In this guide, we will analyze why large bundles degrade performance, explore ESM-based Tree Shaking, configure Dynamic Imports to achieve Code Splitting, and audit packages using bundle analyzers.

The Cost of JavaScript

Unlike images, which only require decoding, JavaScript has a high execution cost. The browser must download the script, parse its syntax tree, compile it to machine instructions, and execute it.

Downloading 500KB of raw images is fast on modern networks. However, downloading 500KB of JavaScript can easily block a mobile phone's CPU for several seconds, freezing user inputs.

Strategy 1: ESM and Tree Shaking (Dead Code Elimination)

Tree Shaking is a build-time optimization that removes unused JavaScript code from your final bundle.

To enable tree shaking, your project must utilize ES Modules (ESM) using import and export statements.

// mathUtils.ts
export function add(a: number, b: number) {
  return a + b;
}

export function subtract(a: number, b: number) {
  return a - b;
}

// main.ts
import { add } from './mathUtils';
console.log(add(5, 5));

During production bundling, the compiler (Webpack or Rollup) statically analyzes the import path. It detects that subtract is never referenced anywhere in your codebase and excludes it from the compiled bundle, saving space.

The CommonJS Trap

Older packaging modules using CommonJS syntax (require and module.exports) cannot be tree-shaken. CommonJS imports are dynamic and run at runtime, meaning the compiler cannot guarantee whether a function will be executed, forcing it to include the entire library. Avoid importing CommonJS libraries if ESM equivalents are available.

Strategy 2: Code Splitting and Dynamic Imports

By default, bundlers merge all your source code and node modules into a single main.js file. This means visitors loading your homepage must download the entire codebase, including heavy components like chart renderers or admin panel layouts.

Code Splitting divides your codebase into smaller chunks that are loaded dynamically on-demand.

You achieve this using JavaScript's native dynamic import() statement:

// Inside React / Next.js Component
import React, { useState, Suspense } from 'react';

// Load the heavy chart component only when requested
const HeavyChart = React.lazy(() => import('./HeavyChartComponent'));

export function AnalyticsWidget() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <button onClick={() => setShowChart(true)}>Show Chart</button>
      
      {showChart && (
        <Suspense fallback={<div>Loading Chart assets...</div>}>
          <HeavyChart />
        </Suspense>
      )}
    </div>
  );
}

When Webpack or Vite encounters the dynamic import(), it automatically extracts the HeavyChartComponent into a separate chunk (e.g., chart.chunk.js). The browser only downloads this chunk if the user clicks the "Show Chart" button.

Strategy 3: Audit and Swap Bloated Packages

Many popular npm packages have massive size footprints. Using a bundle analyzer allows you to inspect what is consuming space.

1. Visualizing the Bundle

Install a visualizer plugin in your bundler (such as rollup-plugin-visualizer for Vite or webpack-bundle-analyzer for Webpack). When you build for production, it generates an interactive treemap showing the file size of every module:

2. Replace Heavy Dependencies

Once you identify the size culprits, replace them with modern, lightweight equivalents:

  • Replace Moment.js: Moment.js is notoriously heavy because it bundles extensive locale packages. Swap it for dayjs (2KB vs 70KB) or date-fns (which supports native tree shaking).
  • Replace Lodash: Instead of importing the entire utility package (import _ from 'lodash'), import specific ESM lodash submodules (import debounce from 'lodash-es/debounce').
  • Use Native Web APIs: Many tasks that historically required jQuery or utility libraries (like array flat mappings, deep clones, or element animations) are now natively supported in modern browsers.

Conclusion

Frontend optimization requires bundle discipline. By utilizing ES Modules to enable static Tree Shaking, implementing Dynamic Imports to split routes and heavy components into on-demand chunks, and auditing dependencies using visualizer tools to replace legacy, bloated packages with modern lightweight alternatives, you can minimize JS payloads and ensure fast page loads.