Skip to content

Code Splitting — Splitting SPA Bundles into Smaller Chunks

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Code Splitting. We cover key concepts, practical examples, and best practices to help you master this topic.

Code splitting divides your SPA JavaScript bundle into smaller chunks loaded on demand, reducing initial download size and improving load performance for users on slow connections.

What You'll Learn

By the end of this tutorial, you will understand code splitting strategies (route-based, vendor, and component-level), how to configure Webpack and Vite for splitting, and how to analyze bundle output.

Why It Matters

A single 500KB JavaScript bundle blocks rendering for over 2 seconds on mobile 3G. Code splitting reduces this to under 1 second by loading only the code needed for the current page. For large SPAs, code splitting can cut initial load time by 60-80%.

Real-World Use

A SaaS dashboard SPA with 200+ components split their bundle into: a 50KB vendor chunk (React, React Router), a 30KB app shell, and 5-20KB page chunks. The initial login page loaded in 1.2 seconds instead of 4.5 seconds. Users on slow connections saw a 73% improvement.

Code Splitting Strategies

Code Splitting Strategies
    ┌──────────────────────────────────────────────────────────────┐
    │  Strategy 1: Route-Based Splitting                          │
    │  / → home.chunk.js (15KB)                                   │
    │  /dashboard → dashboard.chunk.js (25KB)                     │
    │  /settings → settings.chunk.js (12KB)                       │
    │                                                             │
    │  Strategy 2: Vendor Splitting                                │
    │  vendor.chunk.js (React, Redux, Router) — 150KB              │
    │  app.chunk.js (your code) — 50KB                            │
    │                                                             │
    │  Strategy 3: Component-Level Splitting                       │
    │  Chart library → charts.chunk.js (80KB, lazy loaded)        │
    │  Markdown editor → editor.chunk.js (60KB, lazy loaded)      │
    └──────────────────────────────────────────────────────────────┘

Think of code splitting like packing for a trip. Instead of one giant suitcase (single bundle), you use multiple smaller bags: a carry-on for essentials (app shell), a checked bag for clothes (vendor), and specific bags for activities you might do (lazy-loaded features).

Webpack Code Splitting

// webpack.config.js — Route-based and vendor splitting
module.exports = {
    entry: './src/index.js',
    output: {
        filename: '[name].[contenthash].js',
        chunkFilename: '[name].[contenthash].chunk.js',
        path: path.resolve(__dirname, 'dist'),
        clean: true
    },
    optimization: {
        splitChunks: {
            chunks: 'all',
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendor',
                    chunks: 'all',
                    priority: 10
                },
                common: {
                    minChunks: 2,
                    minSize: 10000,
                    priority: 5,
                    reuseExistingChunk: true
                }
            }
        }
    }
};

Vite Code Splitting

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
    build: {
        rollupOptions: {
            output: {
                manualChunks(id) {
                    // Vendor chunk
                    if (id.includes('node_modules')) {
                        if (id.includes('react')) return 'vendor-react';
                        if (id.includes('lodash')) return 'vendor-lodash';
                        return 'vendor';
                    }

                    // Route chunks (auto-handled by Vite for dynamic imports)
                },
                chunkFileNames: 'assets/[name]-[hash].js',
                entryFileNames: 'assets/[name]-[hash].js'
            }
        }
    }
});

Analyzing Bundle Output

// package.json scripts
{
    "scripts": {
        "analyze": "webpack-bundle-analyzer dist/stats.json",
        "build:stats": "webpack --profile --json > dist/stats.json",
        "analyze:vite": "vite build && npx vite-bundle-analyzer"
    }
}

// Programmatic bundle analysis
import { statSync } from 'fs';
import { join } from 'path';

function analyzeBundles(distDir) {
    const files = [
        'vendor.[hash].js',
        'main.[hash].js',
        'dashboard.[hash].js',
        'settings.[hash].js'
    ];

    files.forEach(pattern => {
        const file = findFile(distDir, pattern);
        if (file) {
            const stats = statSync(join(distDir, file));
            const sizeKB = (stats.size / 1024).toFixed(1);
            console.log(`${file}: ${sizeKB}KB`);
        }
    });
}

Dynamic Imports for Code Splitting

// Route-level dynamic imports
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Users = React.lazy(() => import('./pages/Users'));

// Vendor chunk extraction
// webpack automatically extracts shared modules into vendor chunks
// Vite does this by default

// Named exports from dynamic imports
async function loadFeature() {
    const { exportFeature, helperFunction } = await import('./features/export');
    exportFeature();
}

Common Mistakes

  1. Not extracting vendor chunks. Without vendor splitting, library code (React, Redux) is included in every route chunk or in one giant bundle. Extract vendors once.
  2. Splitting into too many chunks. Each chunk has HTTP overhead (headers, TLS negotiation). More than 10-15 chunks per page may hurt performance.
  3. Ignoring chunk name collisions. Without content hashes, cached browsers may serve stale chunks. Always use [contenthash] in filenames.
  4. Not analyzing bundle output. Guessing what is in your bundle leads to missed opportunities. Always analyze with webpack-bundle-analyzer or source-map-explorer.
  5. Splitting synchronous imports. Code splitting only works with dynamic import() syntax. Static imports are always bundled together.

Practice Questions

  1. What are the three main code splitting strategies?
  2. How does vendor splitting differ from route-based splitting?
  3. Why should you use content hashes in chunk filenames?
  4. How do you analyze your bundle to find large dependencies?
  5. What is the difference between static and dynamic imports for code splitting?

Challenge: Set up code splitting in an SPA with 3 strategies: vendor chunk (all node_modules), route-based chunks (6 pages), and component-level Lazy Loading (a chart library). Analyze the output with webpack-bundle-analyzer. Achieve initial bundle under 100KB.

FAQ

Does code splitting work with all bundlers?

Webpack, Vite, Parcel, and Rollup all support code splitting via dynamic import(). The configuration details differ but the concept is the same.

How do I handle shared modules across chunks?

Webpack and Vite automatically extract shared modules into common chunks. Configure cacheGroups to control this behavior.

Does code splitting affect caching?

Yes. With content hashes, unchanged chunks keep the same filename and remain cached. Changed chunks get a new hash and are downloaded fresh.

What is the ideal number of chunks?

5-10 chunks per page is reasonable. More than 20 chunks increases HTTP overhead. Monitor with performance audits.

Can code splitting cause duplicate code?

Improper configuration can cause the same module to appear in multiple chunks. Use SplitChunksPlugin or manualChunks to deduplicate.

Mini Project

Set up code splitting in a Vite-based React SPA with 8 pages. Configure vendor chunk, 8 route-level chunks, and lazy-load a heavy chart library component. Analyze with vite-bundle-analyzer. Measure initial vs total bundle size. Document the optimization achieved.

What's Next

You split your bundles. Now master dynamic imports — the syntax that powers code splitting and lazy loading.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro