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.