Skip to content

Webpack Chunks — Configuring Webpack for Optimal Code Splitting

DodaTech Updated 2026-06-28 6 min read

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

Webpack chunk configuration controls how code is split into separate files, balancing chunk count, size, and Caching for optimal loading performance.

What You'll Learn

By the end of this tutorial, you'll understand Webpack chunk configuration, how to configure splitChunks, how to create vendor and shared chunks, how to name chunks, and how to analyze bundle output.

Why It Matters

Webpack's default Code Splitting works, but production apps need fine-tuned chunk configuration. Poor chunk config leads to too many tiny files (wasted HTTP overhead), too few large files (poor caching), or duplicated code across chunks.

Real-World Use

A large React SPA with 50+ pages and 200+ npm dependencies uses Webpack splitChunks to create a vendor chunk (React, ReactDOM, React Router), a UI library chunk, a utils chunk, and per-route page chunks. Total chunks: 12. Cache hit rate: 85%.

Webpack Chunk Architecture

graph TD
    A[Webpack Bundle] --> B[splitChunks config]
    B --> C[Vendor chunk
React, ReactDOM] B --> D[Common chunk
shared components] B --> E[Page chunks
per-route code] B --> F[Async chunks
dynamic imports] C --> G[Vendors ~250KB
changes rarely] D --> H[Common ~50KB
changes with code] E --> I[Pages ~30KB each
changes per feature] F --> J[Async ~20KB each
loaded on demand] style C fill:#27ae60,color:#fff style D fill:#4a90d9,color:#fff style E fill:#e67e22,color:#fff style F fill:#f39c12,color:#fff

Basic splitChunks Configuration

// webpack.config.js — Basic splitChunks
module.exports = {
    optimization: {
        splitChunks: {
            // Apply to all chunks (including async)
            chunks: 'all',

            // Minimum size before splitting (in bytes)
            minSize: 20000,     // 20KB
            maxSize: 244000,    // ~240KB (avoid bundles > 250KB)

            // Minimum times a module must be shared before splitting
            minChunks: 2,

            // Maximum parallel requests
            maxInitialRequests: 25,
            maxAsyncRequests: 10,

            // Cache group configuration
            cacheGroups: {
                defaultVendors: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    priority: -10,
                    reuseExistingChunk: true,
                },
                default: {
                    minChunks: 2,
                    priority: -20,
                    reuseExistingChunk: true,
                },
            },
        },
    },
};

Advanced Chunk Configuration

// webpack.config.js — Production chunk strategy
module.exports = {
    optimization: {
        splitChunks: {
            chunks: 'all',
            maxInitialRequests: 25,
            minSize: 20000,
            maxSize: 244000,

            // Automatic name generation with content hash
            automaticNameDelimiter: '~',
            hidePathInfo: true,

            cacheGroups: {
                // Core React vendor chunk
                react: {
                    test: /[\\/]node_modules[\\/](react|react-dom|react-router)[\\/]/,
                    name: 'vendor-react',
                    chunks: 'all',
                    priority: 30,
                    enforce: true,
                },

                // Large utility libraries
            utilities: {
                test: /[\\/]node_modules[\\/](lodash|moment|date-fns|axios)[\\/]/,
                name: 'vendor-utils',
                chunks: 'all',
                priority: 25,
            },

            // UI component libraries
            ui: {
                test: /[\\/]node_modules[\\/](@mui|antd|@chakra-ui|bootstrap)[\\/]/,
                name: 'vendor-ui',
                chunks: 'all',
                priority: 20,
            },

            // Chart libraries (loaded async)
            charts: {
                test: /[\\/]node_modules[\\/](chart\.js|d3|echarts|recharts)[\\/]/,
                name: 'vendor-charts',
                chunks: 'async',
                priority: 15,
            },

            // Shared application code
            common: {
                name: 'common',
                minChunks: 3,      // Shared by 3+ modules
                priority: 5,
                reuseExistingChunk: true,
            },

            // Styles
            styles: {
                name: 'styles',
                test: /\.css$/,
                chunks: 'all',
                enforce: true,
                priority: 10,
            },

            // Default for everything else
            default: {
                minChunks: 2,
                priority: -20,
                reuseExistingChunk: true,
            },
        },
    },
},

// Output configuration for named chunks
output: {
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/',
    clean: true,
},

Chunk Naming and Magic Comments

// Magic comments in dynamic imports
const routes = [
    {
        path: '/dashboard',
        // Name the chunk explicitly
        component: lazy(() => import(/* webpackChunkName: "dashboard" */ './pages/Dashboard')),
    },
    {
        path: '/analytics',
        // Preload this chunk in priority
        component: lazy(() => import(/* webpackChunkName: "analytics" */ /* webpackPreload: true */ './pages/Analytics')),
    },
    {
        path: '/reports',
        // Prefetch this chunk (load in idle time)
        component: lazy(() => import(/* webpackChunkName: "reports" */ /* webpackPrefetch: true */ './pages/Reports')),
    },
    {
        path: '/admin',
        // Group into admin chunk
        component: lazy(() => import(/* webpackChunkName: "admin" */ './pages/admin/Dashboard')),
    },
    {
        path: '/admin/users',
        // Keep in admin chunk
        component: lazy(() => import(/* webpackChunkName: "admin" */ './pages/admin/Users')),
    },
    {
        path: '/heavy-chart',
        // Exclude from main chunk
        component: lazy(() => import(/* webpackChunkName: "heavy-chart" */ /* webpackMode: "lazy" */ './pages/HeavyChart')),
    },
];

// Mode options:
// /* webpackMode: "lazy" */       — Default, load on demand
// /* webpackMode: "lazy-once" */  — Load once, all instances share
// /* webpackMode: "eager" */      — Include in main bundle (no async)
// /* webpackMode: "weak" */       — Only load if already requested

Bundle Analysis

// package.json — Bundle analysis scripts
{
    "scripts": {
        "build": "webpack --mode production",
        "analyze": "ANALYZE=true webpack --mode production",
        "analyze:simple": "webpack --mode production --json > stats.json && npx webpack-bundle-analyzer stats.json"
    }
}

// webpack.config.js — Bundle analyzer plugin
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
    plugins: [
        process.env.ANALYZE && new BundleAnalyzerPlugin({
            analyzerMode: 'static',
            reportFilename: 'bundle-report.html',
            openAnalyzer: false,
            generateStatsFile: true,
            statsFilename: 'stats.json',
        }),
    ].filter(Boolean),
};

// Custom bundle size checker
// scripts/check-bundle-size.js
const fs = require('fs');
const path = require('path');

const MAX_BUNDLE_SIZE = 300 * 1024; // 300KB for main vendor
const MAX_PAGE_SIZE = 100 * 1024;   // 100KB per page chunk

function checkBundleSizes(distDir) {
    const files = fs.readdirSync(distDir);
    let errors = [];

    files.forEach(file => {
        if (!file.endsWith('.js')) return;

        const size = fs.statSync(path.join(distDir, file)).size;
        const sizeKB = (size / 1024).toFixed(1);

        if (file.includes('vendor') && size > MAX_BUNDLE_SIZE) {
            errors.push(`WARNING: ${file} is ${sizeKB}KB (max ${MAX_BUNDLE_SIZE / 1024}KB)`);
        }

        if (file.includes('chunk') && size > MAX_PAGE_SIZE) {
            errors.push(`WARNING: ${file} is ${sizeKB}KB (max ${MAX_PAGE_SIZE / 1024}KB)`);
        }

        console.log(`${file}: ${sizeKB}KB`);
    });

    if (errors.length > 0) {
        console.error('\nBundle size warnings:');
        errors.forEach(e => console.error(e));
        process.exit(1);
    }
}

Common Mistakes

  1. Not configuring splitChunks at all. Webpack's defaults are conservative. Without explicit config, node_modules may end up in every page chunk, causing massive duplication.
  2. Setting maxInitialRequests too low. A limit of 5 means Webpack may bundle everything into fewer, larger files. Set 20-30 for modern HTTP/2 multiplexing.
  3. Mixing content hashes with long-term caching. Content hashes invalidate caches on code change. Separate frequently-changing app code from stable vendor code.
  4. Not analyzing bundles regularly. Bundle bloat creeps in gradually. Run bundle analysis after each major dependency update.
  5. Creating too many tiny chunks. Each chunk is an HTTP request. With HTTP/1.1, 50+ chunks cause connection bottlenecks. With HTTP/2, 20-30 chunks is acceptable.

Practice Questions

  1. What does the splitChunks configuration control in Webpack?
  2. How do you separate vendor code from application code?
  3. What is the purpose of cacheGroups in splitChunks?
  4. How do magic comments like webpackChunkName affect chunk naming?
  5. How do you analyze and visualize Webpack bundle output?

Challenge: Configure Webpack for a production SPA: create separate chunks for React vendor, UI library, common components, and page routes, configure HTTP/2-friendly chunk sizes, add bundle analysis, and keep main vendor under 250KB.

FAQ

Should I use HTTP/1.1 or HTTP/2 for split chunks?

HTTP/2 is preferred. It multiplexes multiple requests over one connection, making 20-30 chunks efficient. HTTP/1.1 has connection limits (6-8 per domain).

What is the ideal chunk size?

20-100KB is optimal for async chunks. Vendor chunks can be larger (200-300KB). Avoid chunks under 5KB (HTTP overhead exceeds benefit) or over 500KB (blocks rendering).

How do I handle duplications between chunks?

Increase minChunks in common cacheGroup. Use reuseExistingChunk: true. Extract shared components into a common module that multiple chunks reference.

Does Webpack code splitting work with TypeScript?

Yes. Webpack handles TypeScript via ts-loader or babel-loader. Dynamic imports and magic comments work identically with TypeScript.

How do I cache vendor chunks long-term?

Separate vendor code (react, react-dom) into a stable chunk. Use content hashes in filenames. Vendor chunks only change when you upgrade the library.

Mini Project

Configure Webpack for a multi-page application: set up splitChunks with vendor, common, and page chunk groups, use magic comments for named chunks, add bundle analysis with webpack-bundle-analyzer, implement a bundle size checker in CI, and optimize to keep vendor under 250KB.

What's Next

You've mastered Webpack chunks. Now learn about Vite Dynamic Import for fast Bundling with Vite.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro