MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Webpack 5: What's New and Migration Guide

Upgrade to Webpack 5: module federation, improved caching, asset modules, and breaking changes.

WebpackBundlingJavaScriptFrontend

By MinhVo

Introduction

Webpack 5 is the most significant major release in the bundler's history, arriving after years of refinement in the 4.x series. This release introduces Module Federation for runtime code sharing between applications, persistent filesystem caching that cuts rebuild times by 90%+, asset modules that replace four separate loaders, improved tree-shaking with better dead code elimination, and a host of breaking changes that modernize the configuration API. If you're still on Webpack 4, this guide will walk you through every new feature and provide a step-by-step migration path.

Hero image

The migration from Webpack 4 to 5 is not as daunting as it might seem. While there are breaking changes, most of them remove deprecated behaviors that were already discouraged. The core configuration structure remains familiar, and the new features provide immediate, tangible benefits. A typical medium-sized project can complete the migration in a single focused day, and the performance improvements alone justify the effort.

This guide covers the conceptual changes, practical migration steps, new features with production code examples, and common pitfalls you'll encounter along the way.

Understanding Webpack 5: Core Concepts

Breaking Changes Overview

Webpack 5 removes several long-deprecated behaviors:

Node.js polyfills removed: Webpack 4 automatically polyfilled Node.js core modules like buffer, crypto, and path for browser bundles. Webpack 5 stops this practice entirely. You must explicitly install and configure polyfills, or better yet, use browser-native alternatives.

// Webpack 4: automatically polyfilled
const buffer = require('buffer'); // worked, but bloated the bundle
 
// Webpack 5: explicit polyfill required
// Option 1: Install the package
npm install buffer
 
// Option 2: Configure fallback in webpack.config.js
module.exports = {
  resolve: {
    fallback: {
      buffer: require.resolve('buffer/'),
      crypto: require.resolve('crypto-browserify'),
      stream: require.resolve('stream-browserify'),
      path: require.resolve('path-browserify'),
      os: require.resolve('os-browserify/browser'),
    },
  },
};

require.ensure removed: The legacy code-splitting method require.ensure is no longer supported. Use dynamic import() syntax instead.

// Old (Webpack 4)
require.ensure([], function(require) {
  const module = require('./heavy-module');
});
 
// New (Webpack 5)
const module = await import('./heavy-module');

module.hot API changes: Hot Module Replacement hooks have been updated with clearer semantics and better error handling.

[hash] replaced by [contenthash]: The [hash] placeholder is deprecated in favor of [contenthash] for long-term caching, which only changes when file content changes.

Improved Chunk and Module IDs

Webpack 5 introduces deterministic chunk and module IDs that produce consistent, reproducible builds:

module.exports = {
  optimization: {
    moduleIds: 'deterministic',    // Was 'hashed' in Webpack 4
    chunkIds: 'deterministic',     // Was 'size' in Webpack 4
    runtimeChunk: 'single',
  },
};

Deterministic IDs ensure that builds are reproducible — the same source code always produces the same output, regardless of build order or timing. This is critical for long-term caching strategies.

Improved Tree Shaking

Webpack 5 extends tree-shaking to handle more patterns:

// Nested tree-shaking now works
export * as utils from './utils';
// If only utils.format is used, utils.parse is eliminated
 
// Inner module tree-shaking
module.exports = {
  optimization: {
    innerGraph: true,  // Enables analysis of module-internal dependencies
  },
};
 
// CommonJS tree-shaking (limited)
const { usedFunction } = require('./module');
// Webpack 5 can now detect some unused CommonJS exports

Concept illustration

Architecture and Design Patterns

The Migration Strategy Pattern

Approach migration methodically with this three-phase strategy:

Phase 1: Update dependencies and fix breaking changes

npm install webpack@5 webpack-cli@4 webpack-dev-server@4 --save-dev
npm install html-webpack-plugin@5 --save-dev

Phase 2: Update configuration for new defaults

// webpack.config.js migration
module.exports = {
  mode: 'production',
  
  // Remove deprecated options
  // target: 'web', // Now the default
  
  // Update hash to contenthash
  output: {
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].js',
    clean: true, // Replaces CleanWebpackPlugin
  },
  
  // Add Node.js polyfill fallbacks
  resolve: {
    fallback: {
      buffer: require.resolve('buffer/'),
      stream: require.resolve('stream-browserify'),
    },
  },
  
  // Update caching
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename],
    },
  },
};

Phase 3: Adopt new features

// Add Module Federation
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
 
plugins: [
  new ModuleFederationPlugin({
    name: 'app',
    shared: ['react', 'react-dom'],
  }),
],

Asset Module Migration Pattern

Replace four loaders with built-in asset modules:

// BEFORE (Webpack 4)
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        use: [{ loader: 'url-loader', options: { limit: 8192 } }],
      },
      {
        test: /\.svg$/,
        use: ['file-loader'],
      },
      {
        test: /\.txt$/,
        use: ['raw-loader'],
      },
      {
        test: /\.html$/,
        use: ['html-loader'],
      },
    ],
  },
};
 
// AFTER (Webpack 5)
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        type: 'asset',
        parser: { dataUrlCondition: { maxSize: 8192 } },
      },
      {
        test: /\.svg$/,
        type: 'asset/resource',
      },
      {
        test: /\.txt$/,
        type: 'asset/source',
      },
      {
        test: /\.html$/,
        type: 'asset/source',
      },
    ],
  },
};

Persistent Cache Architecture

module.exports = {
  cache: {
    type: 'filesystem',
    allowCollectingMemory: true,
    maxMemoryGenerations: 5,
    maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days
    buildDependencies: {
      config: [__filename],
      tsconfig: [path.resolve(__dirname, 'tsconfig.json')],
    },
    cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'),
    name: `${process.env.NODE_ENV}-${process.env.TARGET}`,
    version: require('./package.json').version,
  },
};

Step-by-Step Implementation

Complete Migration Checklist

Here's a production-tested migration configuration:

// webpack.config.js — fully migrated Webpack 5 config
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
 
const isDev = process.env.NODE_ENV !== 'production';
 
module.exports = {
  mode: isDev ? 'development' : 'production',
  entry: './src/index.tsx',
  
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: isDev ? '[name].js' : '[name].[contenthash:8].js',
    chunkFilename: isDev ? '[name].chunk.js' : '[name].[contenthash:8].chunk.js',
    assetModuleFilename: 'assets/[name].[contenthash:8][ext]',
    publicPath: '/',
    clean: true,
  },
 
  resolve: {
    extensions: ['.tsx', '.ts', '.jsx', '.js'],
    fallback: {
      buffer: false,
      crypto: false,
      stream: false,
      path: false,
      os: false,
      fs: false,
    },
  },
 
  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
      {
        test: /\.css$/,
        use: [
          isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
          { loader: 'css-loader', options: { modules: { auto: true } } },
          'postcss-loader',
        ],
      },
      {
        test: /\.(png|jpe?g|gif|webp)$/,
        type: 'asset',
        parser: { dataUrlCondition: { maxSize: 10240 } },
      },
      {
        test: /\.svg$/,
        type: 'asset/resource',
        generator: { filename: 'icons/[name].[contenthash:8][ext]' },
      },
      {
        test: /\.(woff2?|eot|ttf|otf)$/,
        type: 'asset/resource',
      },
    ],
  },
 
  plugins: [
    new HtmlWebpackPlugin({ template: './public/index.html' }),
    !isDev && new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
    }),
  ].filter(Boolean),
 
  optimization: {
    minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],
    moduleIds: 'deterministic',
    chunkIds: 'deterministic',
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
    runtimeChunk: 'single',
  },
 
  cache: {
    type: 'filesystem',
    buildDependencies: { config: [__filename] },
  },
 
  devtool: isDev ? 'eval-cheap-module-source-map' : 'source-map',
 
  devServer: {
    port: 3000,
    hot: true,
    historyApiFallback: true,
    client: { overlay: { errors: true, warnings: false } },
  },
};

Handling Node.js Polyfill Removal

# Install explicit polyfills only for modules you actually use
npm install buffer process
// ProvidePlugin for process and Buffer if needed
const webpack = require('webpack');
 
plugins: [
  new webpack.ProvidePlugin({
    Buffer: ['buffer', 'Buffer'],
    process: 'process/browser',
  }),
]

Code Splitting with Dynamic Imports

// Route-based code splitting
const Dashboard = React.lazy(() => import(
  /* webpackChunkName: "dashboard" */
  './pages/Dashboard'
));
 
const Settings = React.lazy(() => import(
  /* webpackChunkName: "settings" */
  './pages/Settings'
));
 
// Named chunk groups for related modules
const Editor = React.lazy(() => import(
  /* webpackChunkName: "editor" */
  /* webpackPrefetch: true */
  './pages/Editor'
));

Implementation workflow

Real-World Use Cases and Case Studies

Use Case 1: Large-Scale SPA Migration

A SaaS platform with 200+ React components and 50+ routes migrated from Webpack 4 to 5 in three days. The persistent cache reduced development rebuild times from 45 seconds to 3 seconds. Asset modules eliminated four loader dependencies, simplifying the configuration by 30 lines. The deterministic module IDs improved cache hit rates from 60% to 95% for returning users.

Use Case 2: Monorepo Build Optimization

A monorepo with 12 packages used Webpack 5's improved splitChunks and persistent caching to reduce full build times from 8 minutes to 90 seconds. The filesystem cache persisted across CI runs, and deterministic chunk IDs ensured that unchanged packages produced identical output hashes.

Use Case 3: Library Build Pipeline

A component library migrated its build pipeline to Webpack 5's module and exports fields in package.json, enabling proper tree-shaking for consumers. Downstream applications saw bundle size reductions of 15–25% as unused components were now eliminated.

Use Case 4: Progressive Web App Upgrade

A PWA used Webpack 5's improved workbox-webpack-plugin integration and asset modules to simplify its service worker configuration. The new caching strategy reduced the number of precached assets by 40% through better content-hash-based invalidation.

Best Practices for Production

  1. Run both Webpack 4 and 5 in parallel: Set up Webpack 5 as a separate build target and compare outputs before fully switching over. This catches subtle behavioral differences early.

  2. Audit Node.js polyfill usage: Run npx node-libs-browser to identify which Node.js polyfills your project uses, then install only the ones you actually need rather than polyfilling everything.

  3. Use [contenthash] everywhere: Replace all [hash] references with [contenthash] for proper long-term caching that only invalidates when content actually changes.

  4. Enable filesystem caching in CI: Configure the cache directory to persist between CI runs. Use the buildDependencies option to ensure cache invalidation when dependencies or configuration change.

  5. Test tree-shaking with source-map-explorer: Use source-map-explorer or webpack-bundle-analyzer to verify that unused code is actually being eliminated after the migration.

  6. Update all webpack plugins: Most popular plugins have Webpack 5-compatible versions. Check for updates to html-webpack-plugin, mini-css-extract-plugin, terser-webpack-plugin, and others.

  7. Remove dead configuration: Webpack 5 changes many defaults. Remove options that now match the new defaults rather than explicitly setting them.

  8. Monitor bundle size regression: Compare bundle sizes before and after migration. If sizes increase unexpectedly, check for missing sideEffects flags or broken tree-shaking.

Common Pitfalls and Solutions

PitfallImpactSolution
Node.js polyfill errors at build timeBuild fails with "Module not found: Buffer"Add resolve.fallback entries or install polyfill packages
require.ensure syntax errorsBuild fails on legacy code-splittingReplace with dynamic import() syntax
[hash] deprecation warningsBuild warnings, potential caching issuesReplace [hash] with [contenthash] in output filenames
Plugin compatibility errorsBuild crashes with "Cannot read property"Update all plugins to latest Webpack 5-compatible versions
Increased bundle sizeSlower load times, regressionCheck sideEffects in package.json and verify tree-shaking with source-map-explorer
Dev server configuration changesDev server fails to startUpdate devServer config to Webpack 5 API (client, static, etc.)

Performance Optimization

Build Speed Benchmarks

// Measure build time impact
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
 
module.exports = smp.wrap({
  // Your webpack config
});
 
// Typical results after migration:
// Webpack 4 full build: 45s
// Webpack 5 full build (no cache): 38s
// Webpack 5 full build (with cache): 4s
// Webpack 5 rebuild (with cache): 0.8s

Bundle Analysis

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
 
module.exports = {
  plugins: [
    process.env.ANALYZE && new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html',
      openAnalyzer: false,
    }),
  ].filter(Boolean),
};

Comparison with Alternatives

FeatureWebpack 5ViteesbuildRollup
Module FederationYes (native)Via pluginNoNo
Persistent cacheFilesystemesbuild nativeN/ANo
Asset modulesBuilt-inBuilt-inBuilt-inVia plugin
HMRFull supportNative ESM HMRNoNo
Dev start speed5–15s< 1s< 1sN/A
Production buildOptimizedRollup-basedFast but less optimizedOptimized
Ecosystem maturityExcellentGrowingLimitedGood
ConfigurationVerboseMinimalMinimalModerate

Advanced Patterns and Techniques

Custom Module Federation Plugin

class AutoFederationPlugin {
  constructor(options) {
    this.manifestUrl = options.manifestUrl;
  }
 
  apply(compiler) {
    compiler.hooks.beforeCompile.tapAsync('AutoFederation', async (params, callback) => {
      const manifest = await fetch(this.manifestUrl).then(r => r.json());
      // Dynamically configure remotes based on manifest
      this.configureRemotes(compiler, manifest.remotes);
      callback();
    });
  }
}

Build Performance Monitoring

class BuildMetricsPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('BuildMetrics', (stats) => {
      const metrics = {
        buildTime: stats.endTime - stats.startTime,
        assets: stats.assets.length,
        modules: stats.modules.length,
        chunks: stats.chunks.length,
        errors: stats.errors.length,
        warnings: stats.warnings.length,
        cacheHit: stats.compilation.cache.getCacheStats(),
      };
 
      // Send to monitoring service
      this.reportMetrics(metrics);
    });
  }
}

Testing Strategies

// Verify migration didn't break anything
describe('Webpack 5 Migration Tests', () => {
  it('produces correct output files', () => {
    const files = fs.readdirSync(path.resolve(__dirname, 'dist'));
    expect(files).toContain('index.html');
    expect(files.some(f => f.startsWith('main.'))).toBe(true);
    expect(files.some(f => f.startsWith('vendors.'))).toBe(true);
  });
 
  it('applies content hashing correctly', () => {
    const files = fs.readdirSync(path.resolve(__dirname, 'dist/js'));
    files.forEach(file => {
      if (file.endsWith('.js')) {
        expect(file).toMatch(/\.[a-f0-9]{8}\.js$/);
      }
    });
  });
 
  it('tree-shakes unused exports', () => {
    const bundleContent = fs.readFileSync(
      path.resolve(__dirname, 'dist/js/main.*.js'),
      'utf-8'
    );
    expect(bundleContent).not.toContain('unusedExportFunction');
  });
});

Future Outlook

Webpack 5 continues to evolve with improved performance, better compatibility with modern JavaScript features like import assertions and top-level await, and closer integration with the broader bundler ecosystem. The Module Federation project has expanded into a standalone runtime that supports Vite and Rspack, ensuring the patterns pioneered in Webpack 5 remain relevant regardless of bundler choice.

Community Resources and Further Learning

The technology landscape evolves rapidly, making continuous learning essential for maintaining expertise. Building a systematic approach to staying current with developments in your technology stack ensures you can leverage new features and avoid deprecated patterns.

Curated Learning Pathways

Rather than consuming content randomly, create structured learning pathways aligned with your current projects and career goals. Start with official documentation and specification documents, which provide the most accurate and comprehensive information. Follow this with hands-on tutorials and workshops that reinforce concepts through practical application.

Technical blogs from framework maintainers and core team members often provide deeper insights into design decisions and upcoming features. Subscribe to the official blogs of your primary frameworks and libraries to stay ahead of breaking changes and deprecation timelines.

Contributing to Open Source

Contributing to open-source projects in your technology stack provides unparalleled learning opportunities. Start with documentation improvements and bug reports, then progress to fixing small issues tagged as "good first issue" in your favorite projects. This direct engagement with maintainers and the codebase accelerates your understanding far beyond what passive learning can achieve.

# Setting up for contribution
git clone https://github.com/project/repository.git
cd repository
git checkout -b fix/issue-description
 
# Run the project's contribution setup
npm run setup:dev
npm run test  # Ensure tests pass before making changes
 
# Make your changes, then run the full test suite
npm run test:full
npm run lint
npm run build
 
# Submit your contribution
git add -A
git commit -m "fix: description of the fix
 
Closes #1234"
git push origin fix/issue-description

Building a Technical Knowledge Base

Maintain a personal knowledge base that captures insights, solutions, and patterns you discover during your work. Tools like Obsidian, Notion, or even a simple Markdown repository can serve as an external memory that grows more valuable over time.

Organize your notes by topic rather than chronologically, and include code examples, links to relevant documentation, and explanations of why certain approaches work better than others. When you encounter a particularly insightful article or conference talk, write a summary that captures the key takeaways and how they apply to your current projects.

Follow key conferences and their published talks to stay informed about emerging patterns and best practices. Many conferences publish recorded talks on YouTube within weeks of the event, making world-class technical content freely accessible.

Join relevant Discord servers, Slack communities, and forums where practitioners discuss real-world challenges and solutions. These communities provide early warning about emerging issues and access to collective wisdom that isn't available through formal documentation.

Mentorship and Knowledge Sharing

Teaching others is one of the most effective ways to deepen your own understanding. Consider writing technical blog posts, giving talks at local meetups, or mentoring junior developers. The process of explaining concepts to others forces you to organize your knowledge and identify gaps in your understanding.

Pair programming sessions with colleagues of different experience levels create mutual learning opportunities. Senior developers gain fresh perspectives on problems they've solved the same way for years, while junior developers benefit from exposure to production-grade thinking and decision-making processes.

Conclusion

Migrating to Webpack 5 is a high-value investment that pays immediate dividends in build performance, bundle optimization, and development experience. The removal of automatic Node.js polyfills modernizes browser bundles, asset modules simplify configuration, and persistent caching transforms rebuild times.

Key takeaways:

  1. Node.js polyfills are gone — explicitly configure resolve.fallback for only the polyfills you need.
  2. Use [contenthash] instead of [hash] for proper long-term caching.
  3. Replace all loader-based asset handling with built-in asset/resource, asset/inline, asset/source, and asset module types.
  4. Enable persistent filesystem caching for dramatically faster development rebuilds.
  5. Module Federation enables micro-frontends — start exploring it for multi-team architectures.