Skip to content

JavaScript Bundle Optimization — Code Splitting and Tree Shaking

DodaTech Updated 2026-06-23 7 min read

In this tutorial, you will learn how to reduce JavaScript bundle sizes through Code Splitting, Tree Shaking, and dynamic import patterns. JavaScript is the most expensive resource on the web, often blocking the main thread for hundreds of milliseconds. Doda Browser extensions use aggressive Code Splitting to keep initial bundle sizes under 50KB.

What You Will Learn

  • How Tree Shaking eliminates unused exports from your bundles
  • How to implement route-based and component-based Code Splitting
  • How to analyze bundle composition with Webpack Bundle Analyzer
  • How to set up dynamic imports for on-demand loading

Why It Matters

Every kilobyte of JavaScript costs approximately 5 milliseconds of CPU time on mobile devices. A 500KB bundle means 2.5 seconds of main thread blocking before the page becomes interactive. DodaTech reduced its main application bundle from 340KB to 85KB through systematic optimization.

Real-World Use Case

The DodaZIP web interface used a single monolithic bundle containing chart libraries, markdown parsers, and admin panel code. After splitting these into separate chunks loaded only on their respective pages, the initial bundle dropped to 45KB and Time to Interactive improved by 3 seconds.

Prerequisites

You should have experience with JavaScript ES6 module syntax and a module bundler like Webpack or Vite. Understanding of Babel Transpilation is helpful.

Step-by-Step Tutorial

Step 1: Analyze Your Current Bundle

Before optimizing, understand what is in your bundle. Use the Webpack Bundle Analyzer plugin.

// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html'
    })
  ]
};

Expected output: An interactive treemap visualization showing each module and its size. You will likely see large libraries like moment.js or lodash consuming hundreds of kilobytes.

Step 2: Enable Tree Shaking

Tree Shaking relies on ES6 module syntax (import/export). Ensure your Webpack configuration has mode: 'production' which enables tree shaking by default.

// webpack.config.js
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true,
    sideEffects: false  // Only if your code has no side effects
  }
};
// Before: importing the entire library
import _ from 'lodash';
_.debounce(myFunction, 300);

// After: importing only what you need
import debounce from 'lodash/debounce';
debounce(myFunction, 300);

Expected result: Unused lodash functions are removed from the bundle. The imported file drops from the full 71KB lodash to the 2KB debounce module.

Step 3: Avoid Barrel Files

Barrel files re-export from multiple modules and defeat Tree Shaking because bundlers cannot determine which exports are actually used.

// Bad: barrel file (index.js)
export { UserService } from './user.service';
export { AuthService } from './auth.service';
export { PaymentService } from './payment.service';

// Better: import directly from the source file
import { UserService } from './user.service';

Step 4: Implement Route-Based Code Splitting

Split your application at route boundaries so each page loads only its own code.

// React Router with lazy loading
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Reports = lazy(() => import('./pages/Reports'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/reports" element={<Reports />} />
      </Routes>
    </Suspense>
  );
}

Expected behavior: The browser downloads only the JavaScript for the current route. Navigating to Settings triggers a lazy chunk download of approximately 5-15KB instead of loading all pages upfront.

Step 5: Split Vendor Code

Separate third-party libraries from application code so vendor chunks can be cached independently.

// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
};

Expected output: Two output files: vendors.abc123.js containing React and other libraries (approximately 120KB) and main.xyz456.js containing application code (approximately 30KB).

Step 6: Use Dynamic Imports for Heavy Features

Load heavy features like charts or PDF generation only when the user triggers them.

// Dynamic import for chart library
async function renderChart(data) {
  const { Chart } = await import('chart.js');
  const canvas = document.getElementById('myChart');
  new Chart(canvas, { type: 'bar', data });
}

// Button click triggers the import
document.getElementById('show-chart').addEventListener('click', () => {
  renderChart(salesData);
});

Expected behavior: Clicking the button triggers a network request for the chart.js chunk (approximately 60KB). The chart renders once the download and initialization complete.

Step 7: Configure Vite for Even Faster Bundling

Vite uses native ES modules and esbuild for extremely fast builds with built-in Code Splitting.

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

Expected output: Vite produces small, focused chunks that load in parallel, reducing the total waterfall depth compared to a single large bundle.

Learning Path

flowchart LR
  A[Image Optimization] --> B[JavaScript Bundle Optimization]
  B --> C[CSS Performance]
  B --> D[Preload, Prefetch, Preconnect]
  C --> E[Critical Rendering Path]
  D --> E
  
  style B fill:#4f46e5,color:#fff
  style A fill:#6366f1,color:#fff
  style C fill:#6366f1,color:#fff

Common Errors

  1. Not analyzing the bundle before optimizing: Developers guess which libraries are large instead of checking the analyzer. The result is effort spent on small modules while a 200KB library goes unnoticed.

  2. Tree Shaking only works with ES6 modules: CommonJS modules (require()) cannot be tree-shaken. Use ES6 import syntax and configure Webpack to prefer ES6 module versions via the module field in package.json.

  3. Creating too many chunks: Splitting every small module into its own chunk creates dozens of tiny HTTP requests. Aim for chunks between 20KB and 100KB each.

  4. Forgetting to lazy-load heavy third-party libraries: Libraries like moment.js (231KB), chart.js (60KB), and pdfmake (300KB+) should always be lazy-loaded unless they are needed on every page.

  5. No cache-busting on vendor chunks: Without a content hash in the filename, the vendor chunk never gets updated in the browser cache when you upgrade a library.

  6. Splitting code that loads in the critical path: Splitting code that is already needed for the initial render adds an extra network round trip. Only split code that is conditionally loaded.

Practice Questions

  1. What is Tree Shaking and how does it work?
  2. How does the import() function differ from a static import?
  3. What is a barrel file and why is it bad for Tree Shaking?
  4. What does the splitChunks configuration do in Webpack?
  5. Why should vendor code be in a separate chunk?

Answers: 1. Tree Shaking statically analyzes ES6 module exports and removes unused exports from the production bundle. 2. import() returns a promise and loads the module asynchronously, enabling Code Splitting. Static import is evaluated at parse time. 3. A barrel file re-exports from multiple modules; bundlers cannot determine which exports are unused and include all of them. 4. It extracts shared dependencies into separate chunks to avoid duplication across entry points. 5. Vendor code changes infrequently, so a separate vendor chunk with a long cache lifetime means users download it only once.

Challenge

Take an existing React or Vue application, install Webpack Bundle Analyzer, and identify the three largest modules. Refactor the imports to be tree-shakeable, split the application by routes, and lazy-load the heaviest third-party library. Run the analysis again and document the size reduction.

FAQ

Does Tree Shaking work with TypeScript?

Yes, Tree Shaking works with TypeScript when you compile to ES6 module syntax. Configure tsconfig.json with module: "esnext" and your bundler handles the rest.

What is the ideal chunk size?

Aim for chunks between 20KB and 100KB (gzipped). Chunks smaller than 10KB have too much HTTP overhead, while chunks over 200KB block the main thread for too long.

How does Code Splitting affect SEO?

Code Splitting does not affect SEO if you use server-side rendering or pre-rendering. For client-only rendered apps, ensure critical content is not hidden behind a lazy import that search bots may not execute.

Can I use Code Splitting with Vue.js?

Yes, Vue supports lazy loading in its router with () => import('./Component.vue') syntax for route-level Code Splitting, identical to React's lazy loading pattern.

Does DodaTech use Webpack or Vite?

DodaTech uses Vite for new projects due to its faster build times and native ES module support. Legacy projects still on Webpack are gradually being migrated.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro