Nội dung

DaiPhan

DaiPhan

Full-Stack Developer

Full-stack developer passionate about modern web technologies, best practices, and sharing knowledge with the community.

Skills & Expertise

JavaScript TypeScript React Node.js DevOps
150+
Articles
50k+
Readers
4.9
Rating

Webpack Bundling Guide: Tối ưu performance cho ứng dụng web

Hướng dẫn cấu hình Webpack để tối ưu bundle size và performance

Webpack Bundling Guide: Tối ưu performance cho ứng dụng web

Webpack là một trong những công cụ quan trọng nhất trong modern web development. Hiểu rõ cách configure và optimize Webpack sẽ giúp bạn tạo ra applications nhanh và efficient.

Webpack Fundamentals

Core Concepts

// webpack.config.js - Basic configuration
const path = require('path');

module.exports = {
  // Entry point
  entry: './src/index.js',
  
  // Output configuration
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    clean: true, // Clean the output directory before emit
  },
  
  // Loaders
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        type: 'asset/resource',
      },
    ],
  },
  
  // Plugins
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html'
    })
  ],
  
  // Development server
  devServer: {
    static: './dist',
    hot: true,
  },
  
  // Mode
  mode: 'development',
};

Entry Points và Code Splitting

// Multiple entry points
module.exports = {
  entry: {
    main: './src/index.js',
    vendor: './src/vendor.js',
    admin: './src/admin.js',
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash].js', // Content hash for caching
  },
};

// Dynamic imports for code splitting
// src/router.js
const routes = {
  '/': () => import('./pages/Home.js'),
  '/about': () => import('./pages/About.js'),
  '/dashboard': () => import('./pages/Dashboard.js'),
};

// Lazy loading components
const Dashboard = React.lazy(() => 
  import('./components/Dashboard').then(module => ({
    default: module.Dashboard
  }))
);

// Usage with React.lazy
function App() {
  return (
    <React.Suspense fallback={<Loading />}>
      <Dashboard />
    </React.Suspense>
  );
}

Code Splitting Strategies

1. Route-based Code Splitting

// src/App.js
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { lazy, Suspense } from 'react';

// Lazy load route components
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const Dashboard = lazy(() => import('./routes/Dashboard'));

function App() {
  return (
    <Router>
      <Suspense fallback={<div>Loading...</div>}>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/about" component={About} />
          <Route path="/dashboard" component={Dashboard} />
        </Switch>
      </Suspense>
    </Router>
  );
}

2. Component-based Code Splitting

// src/components/LazyComponent.js
import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => 
  import(/* webpackChunkName: "heavy-component" */ './HeavyComponent')
);

const ExpensiveChart = lazy(() => 
  import(/* webpackChunkName: "expensive-chart" */ './ExpensiveChart')
);

function Dashboard() {
  const [showChart, setShowChart] = useState(false);
  
  return (
    <div>
      <h1>Dashboard</h1>
      
      <Suspense fallback={<div>Loading component...</div>}>
        <HeavyComponent />
      </Suspense>
      
      <button onClick={() => setShowChart(true)}>
        Show Chart
      </button>
      
      {showChart && (
        <Suspense fallback={<div>Loading chart...</div>}>
          <ExpensiveChart />
        </Suspense>
      )}
    </div>
  );
}

3. Library Code Splitting

// webpack.config.js - Split vendor code
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: -10,
          reuseExistingChunk: true,
        },
        common: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
        // React specific splitting
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react',
          priority: 0,
        },
        // UI library splitting
        ui: {
          test: /[\\/]node_modules[\\/](@mui|@material-ui|antd)[\\/]/,
          name: 'ui',
          priority: 5,
        },
      },
    },
  },
};

Optimization Techniques

1. Tree Shaking

// webpack.config.js
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true, // Enable tree shaking
    sideEffects: false, // Assume no side effects
  },
};

// package.json - Mark side effects
{
  "name": "my-app",
  "sideEffects": [
    "*.css",
    "*.scss",
    "./src/polyfills.js"
  ],
  "scripts": {
    "build": "webpack --mode=production"
  }
}

// Use ES modules for better tree shaking
// Good - ES modules
export const utils = {
  formatDate: (date) => { /* ... */ },
  parseDate: (str) => { /* ... */ },
  validateEmail: (email) => { /* ... */ }
};

// Bad - CommonJS
module.exports = {
  formatDate: function(date) { /* ... */ },
  parseDate: function(str) { /* ... */ }
};

2. Minification và Compression

// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true, // Remove console.log
            drop_debugger: true, // Remove debugger statements
          },
          format: {
            comments: false, // Remove comments
          },
        },
        extractComments: false,
      }),
      new CssMinimizerPlugin(),
    ],
  },
  
  plugins: [
    new CompressionPlugin({
      filename: '[path][base].gz',
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 8192,
      minRatio: 0.8,
    }),
    new CompressionPlugin({
      filename: '[path][base].br',
      algorithm: 'brotliCompress',
      test: /\.(js|css|html|svg)$/,
      compressionOptions: {
        params: {
          [zlib.constants.BROTLI_PARAM_QUALITY]: 11,
        },
      },
      threshold: 8192,
      minRatio: 0.8,
    }),
  ],
};

3. Image Optimization

// webpack.config.js
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif|svg)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8192, // Inline images < 8KB
          },
        },
      },
    ],
  },
  
  plugins: [
    new ImageMinimizerPlugin({
      minimizer: {
        implementation: ImageMinimizerPlugin.imageminMinify,
        options: {
          plugins: [
            ['gifsicle', { interlaced: true }],
            ['jpegtran', { progressive: true }],
            ['optipng', { optimizationLevel: 5 }],
            ['svgo', {
              plugins: [
                {
                  name: 'preset-default',
                  params: {
                    overrides: {
                      removeViewBox: false,
                    },
                  },
                },
              ],
            }],
          ],
        },
      },
    }),
  ],
};

4. Font Optimization

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: 'asset/resource',
        generator: {
          filename: 'fonts/[name].[hash][ext]',
        },
      },
    ],
  },
};

// src/styles/fonts.scss
@font-face {
  font-family: 'CustomFont';
  src: url('@/fonts/custom-font.woff2') format('woff2'),
       url('@/fonts/custom-font.woff') format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap; // Improve perceived performance
}

Development vs Production Configuration

Development Configuration

// webpack.dev.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'development',
  devtool: 'eval-source-map', // Fast source maps
  
  devServer: {
    static: './dist',
    hot: true, // Hot Module Replacement
    open: true, // Open browser automatically
    port: 3000,
    historyApiFallback: true, // For React Router
    compress: true,
    headers: {
      'Cache-Control': 'no-cache',
    },
  },
  
  optimization: {
    runtimeChunk: 'single', // Better caching
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: -10,
          chunks: 'all',
        },
      },
    },
  },
});

Production Configuration

// webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = merge(common, {
  mode: 'production',
  devtool: 'source-map', // Production source maps
  
  output: {
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].js',
    clean: true,
  },
  
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
    ],
  },
  
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css',
      chunkFilename: '[name].[contenthash].css',
    }),
    
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 8192,
      minRatio: 0.8,
    }),
    
    // Optional: Bundle analyzer
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false,
      reportFilename: 'bundle-report.html',
    }),
  ],
  
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true, // Use multiple CPUs
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true,
          },
        },
      }),
      new CssMinimizerPlugin(),
    ],
    
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: -10,
          chunks: 'all',
        },
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react',
          priority: 0,
        },
      },
    },
    
    runtimeChunk: 'single',
  },
});

Performance Monitoring

Bundle Analysis

// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const DuplicatePackageCheckerPlugin = require('duplicate-package-checker-webpack-plugin');

module.exports = {
  plugins: [
    // Bundle analyzer
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false,
      reportFilename: 'bundle-report.html',
      generateStatsFile: true,
      statsFilename: 'bundle-stats.json',
    }),
    
    // Check for duplicate packages
    new DuplicatePackageCheckerPlugin({
      verbose: true,
      emitError: true,
    }),
  ],
};

Performance Budgets

// webpack.config.js
const { PerformancePlugin } = require('webpack-performance-plugin');

module.exports = {
  plugins: [
    new PerformancePlugin({
      maxAssetSize: 250000, // 250KB
      maxEntrypointSize: 500000, // 500KB
      hints: 'error', // Show errors when budget exceeded
    }),
  ],
  
  performance: {
    maxAssetSize: 250000,
    maxEntrypointSize: 500000,
    hints: 'warning',
    assetFilter: function(assetFilename) {
      return !assetFilename.endsWith('.map'); // Exclude source maps
    },
  },
};

Build Time Analysis

// webpack.config.js
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');

const smp = new SpeedMeasurePlugin();

module.exports = smp.wrap({
  // Your webpack configuration
  plugins: [
    // Your plugins
  ],
});

Advanced Techniques

Module Federation

// webpack.config.js - Host application
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
};

// Remote application
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'remoteApp',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/components/Button',
        './Header': './src/components/Header',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
};

Web Workers

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.worker\.js$/,
        use: { loader: 'worker-loader' },
      },
    ],
  },
};

// src/calculations.worker.js
self.onmessage = function(e) {
  const result = performHeavyCalculation(e.data);
  self.postMessage(result);
};

function performHeavyCalculation(data) {
  // Heavy computation here
  return data.reduce((sum, num) => sum + num, 0);
}

// src/app.js
import Worker from './calculations.worker.js';

const worker = new Worker();
worker.postMessage([1, 2, 3, 4, 5]);
worker.onmessage = function(e) {
  console.log('Result:', e.data);
};

Kết luận

Một Webpack configuration hiệu quả cần:

  • Code Splitting: Load only what’s needed
  • Tree Shaking: Remove unused code
  • Compression: Minimize file sizes
  • Caching: Optimize for return visits
  • Monitoring: Track performance metrics

Bài viết này là phần đầu tiên trong series về Build Tools. Trong các bài tiếp theo, chúng ta sẽ explore Vite và Rollup.