发布于

Webpack打包优化实战:代码分割、Tree Shaking与缓存策略

作者

Webpack打包优化实战:代码分割、Tree Shaking与缓存策略

Webpack作为现代前端构建工具的核心,其优化配置直接影响应用性能。本文将分享Webpack打包优化的实战经验和高级技巧。

代码分割策略

基础代码分割配置

// webpack.config.js - 代码分割配置
const path = require('path');

module.exports = {
  mode: 'production',
  entry: {
    main: './src/index.js',
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js',
    clean: true,
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 20000,
      maxSize: 244000,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      cacheGroups: {
        // 第三方库分离
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10,
          reuseExistingChunk: true,
        },
        // React相关库单独分离
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react',
          chunks: 'all',
          priority: 20,
        },
        // UI库分离
        antd: {
          test: /[\\/]node_modules[\\/]antd[\\/]/,
          name: 'antd',
          chunks: 'all',
          priority: 15,
        },
        // 工具库分离
        utils: {
          test: /[\\/]node_modules[\\/](lodash|moment|dayjs)[\\/]/,
          name: 'utils',
          chunks: 'all',
          priority: 15,
        },
        // 公共代码分离
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          priority: 5,
          reuseExistingChunk: true,
          enforce: true,
        },
        // 默认分组
        default: {
          minChunks: 2,
          priority: -10,
          reuseExistingChunk: true,
        },
      },
    },
    // 运行时代码分离
    runtimeChunk: {
      name: 'runtime',
    },
  },
};

动态导入与路由分割

// 路由级别的代码分割
import { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

// 懒加载组件
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));

// 预加载策略
const DashboardWithPreload = lazy(() => 
  import(
    /* webpackChunkName: "dashboard" */
    /* webpackPreload: true */
    './pages/Dashboard'
  )
);

// 按需加载工具函数
const loadComponent = (componentName) => {
  return lazy(() => 
    import(`./components/${componentName}`)
      .catch(() => import('./components/ErrorFallback'))
  );
};

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

// 智能预加载Hook
import { useEffect } from 'react';

function usePreloadRoute(routePath, condition = true) {
  useEffect(() => {
    if (condition) {
      // 预加载路由组件
      import(
        /* webpackChunkName: "[request]" */
        `./pages/${routePath}`
      ).catch(console.error);
    }
  }, [routePath, condition]);
}

// 使用示例
function HomePage() {
  // 当用户在首页时,预加载Dashboard页面
  usePreloadRoute('Dashboard', true);
  
  return <div>Home Page</div>;
}

第三方库优化分割

// 第三方库优化配置
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        // 大型UI库单独分割
        antd: {
          test: /[\\/]node_modules[\\/]antd[\\/]/,
          name: 'antd',
          chunks: 'all',
          priority: 20,
        },
        // 图表库分割
        charts: {
          test: /[\\/]node_modules[\\/](echarts|d3|chart\.js)[\\/]/,
          name: 'charts',
          chunks: 'all',
          priority: 15,
        },
        // 工具库分割
        lodash: {
          test: /[\\/]node_modules[\\/]lodash[\\/]/,
          name: 'lodash',
          chunks: 'all',
          priority: 15,
        },
        // 日期库分割
        date: {
          test: /[\\/]node_modules[\\/](moment|dayjs|date-fns)[\\/]/,
          name: 'date-utils',
          chunks: 'all',
          priority: 15,
        },
        // 异步加载的第三方库
        asyncVendor: {
          test: /[\\/]node_modules[\\/]/,
          chunks: 'async',
          name: 'async-vendor',
          priority: 10,
        },
      },
    },
  },
  
  // 外部化大型库
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM',
    'lodash': '_',
    'moment': 'moment',
  },
};

// 对应的HTML模板
/*
<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
</head>
<body>
  <div id="root"></div>
  
  <!-- CDN引入外部化的库 -->
  <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
  <script src="https://unpkg.com/lodash@4/lodash.min.js"></script>
  <script src="https://unpkg.com/moment@2/min/moment.min.js"></script>
</body>
</html>
*/

Tree Shaking优化

ES6模块与Tree Shaking

// package.json - 确保sideEffects配置正确
{
  "name": "my-app",
  "sideEffects": [
    "*.css",
    "*.scss",
    "./src/polyfills.js",
    "./src/global-styles.js"
  ]
}

// webpack.config.js - Tree Shaking配置
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true,
    sideEffects: false,
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              ['@babel/preset-env', {
                modules: false, // 保持ES6模块格式
                useBuiltIns: 'usage',
                corejs: 3,
              }],
            ],
          },
        },
      },
    ],
  },
};

// 工具函数模块化设计
// utils/index.js - 避免barrel exports
export { default as formatDate } from './formatDate';
export { default as debounce } from './debounce';
export { default as throttle } from './throttle';
export { default as deepClone } from './deepClone';

// 更好的做法:直接导入具体模块
// import { formatDate } from './utils/formatDate';
// import { debounce } from './utils/debounce';

// utils/formatDate.js
export default function formatDate(date, format = 'YYYY-MM-DD') {
  // 实现代码
}

// 避免副作用的模块设计
// ❌ 有副作用的代码
console.log('Module loaded'); // 这会阻止Tree Shaking

// ✅ 无副作用的纯函数
export function pureFunction(input) {
  return input * 2;
}

第三方库Tree Shaking

// 针对不同库的Tree Shaking策略

// 1. Lodash优化
// ❌ 导入整个lodash
import _ from 'lodash';

// ✅ 按需导入
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';

// 或使用babel-plugin-lodash
// .babelrc
{
  "plugins": [
    ["lodash", { "id": ["lodash"] }]
  ]
}

// 2. Ant Design优化
// ❌ 导入整个antd
import { Button, Input } from 'antd';

// ✅ 使用babel-plugin-import
// .babelrc
{
  "plugins": [
    ["import", {
      "libraryName": "antd",
      "libraryDirectory": "es",
      "style": "css"
    }]
  ]
}

// 3. 日期库优化
// ❌ 导入整个moment
import moment from 'moment';

// ✅ 使用dayjs或date-fns
import dayjs from 'dayjs';
import { format, parseISO } from 'date-fns';

// 4. 图标库优化
// ❌ 导入所有图标
import * as Icons from '@ant-design/icons';

// ✅ 按需导入图标
import { UserOutlined, SettingOutlined } from '@ant-design/icons';

自定义Tree Shaking插件

// webpack-tree-shaking-plugin.js
class TreeShakingAnalyzerPlugin {
  constructor(options = {}) {
    this.options = options;
  }
  
  apply(compiler) {
    compiler.hooks.afterEmit.tap('TreeShakingAnalyzer', (compilation) => {
      const stats = compilation.getStats().toJson();
      const modules = stats.modules || [];
      
      // 分析未使用的导出
      const unusedExports = [];
      const sideEffectModules = [];
      
      modules.forEach(module => {
        if (module.providedExports && module.usedExports) {
          const unused = module.providedExports.filter(
            exp => !module.usedExports.includes(exp)
          );
          
          if (unused.length > 0) {
            unusedExports.push({
              module: module.name,
              unused: unused
            });
          }
        }
        
        if (module.sideEffectFree === false) {
          sideEffectModules.push(module.name);
        }
      });
      
      // 输出分析报告
      if (this.options.verbose) {
        console.log('\n=== Tree Shaking 分析报告 ===');
        console.log(`未使用的导出: ${unusedExports.length} 个模块`);
        console.log(`有副作用的模块: ${sideEffectModules.length}`);
        
        if (unusedExports.length > 0) {
          console.log('\n未使用的导出:');
          unusedExports.forEach(item => {
            console.log(`  ${item.module}: ${item.unused.join(', ')}`);
          });
        }
        
        if (sideEffectModules.length > 0) {
          console.log('\n有副作用的模块:');
          sideEffectModules.forEach(module => {
            console.log(`  ${module}`);
          });
        }
      }
      
      // 生成报告文件
      if (this.options.outputFile) {
        const report = {
          timestamp: new Date().toISOString(),
          unusedExports,
          sideEffectModules,
          totalModules: modules.length
        };
        
        const fs = require('fs');
        fs.writeFileSync(
          this.options.outputFile,
          JSON.stringify(report, null, 2)
        );
      }
    });
  }
}

module.exports = TreeShakingAnalyzerPlugin;

// 使用插件
const TreeShakingAnalyzerPlugin = require('./webpack-tree-shaking-plugin');

module.exports = {
  plugins: [
    new TreeShakingAnalyzerPlugin({
      verbose: true,
      outputFile: 'tree-shaking-report.json'
    })
  ]
};

缓存策略优化

文件名哈希策略

// webpack.config.js - 缓存优化配置
const path = require('path');

module.exports = {
  output: {
    path: path.resolve(__dirname, 'dist'),
    // 入口文件使用contenthash
    filename: '[name].[contenthash:8].js',
    // 异步chunk使用contenthash
    chunkFilename: '[name].[contenthash:8].chunk.js',
    // 静态资源使用contenthash
    assetModuleFilename: 'assets/[name].[contenthash:8][ext]',
  },
  
  optimization: {
    // 模块ID稳定化
    moduleIds: 'deterministic',
    chunkIds: 'deterministic',
    
    // 运行时代码分离,避免影响业务代码缓存
    runtimeChunk: {
      name: 'runtime',
    },
    
    splitChunks: {
      cacheGroups: {
        // 第三方库缓存组
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          // 确保第三方库的稳定缓存
          priority: 10,
        },
      },
    },
  },
  
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
        ],
      },
      {
        test: /\.(png|jpe?g|gif|svg)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 8 * 1024, // 8KB
          },
        },
      },
    ],
  },
  
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
      chunkFilename: 'css/[name].[contenthash:8].chunk.css',
    }),
  ],
};

持久化缓存配置

// webpack.config.js - Webpack 5 持久化缓存
module.exports = {
  cache: {
    type: 'filesystem',
    cacheDirectory: path.resolve(__dirname, '.webpack-cache'),
    buildDependencies: {
      config: [__filename],
      tsconfig: [path.resolve(__dirname, 'tsconfig.json')],
    },
    // 缓存版本控制
    version: '1.0.0',
    // 缓存压缩
    compression: 'gzip',
    // 缓存存储策略
    store: 'pack',
    // 缓存失效条件
    managedPaths: [path.resolve(__dirname, 'node_modules')],
    profile: true,
  },
  
  // 快照配置
  snapshot: {
    managedPaths: [path.resolve(__dirname, 'node_modules')],
    immutablePaths: [],
    buildDependencies: {
      hash: true,
      timestamp: true,
    },
    module: {
      timestamp: true,
    },
    resolve: {
      timestamp: true,
    },
    resolveBuildDependencies: {
      hash: true,
      timestamp: true,
    },
  },
};

// 缓存分析工具
class CacheAnalyzerPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('CacheAnalyzer', (stats) => {
      const compilation = stats.compilation;
      const cache = compilation.cache;
      
      if (cache && cache.getCache) {
        const cacheStats = {
          hits: 0,
          misses: 0,
          stores: 0,
        };
        
        // 分析缓存命中率
        console.log('\n=== 缓存分析 ===');
        console.log(`缓存命中率: ${((cacheStats.hits / (cacheStats.hits + cacheStats.misses)) * 100).toFixed(2)}%`);
        console.log(`缓存命中: ${cacheStats.hits}`);
        console.log(`缓存未命中: ${cacheStats.misses}`);
        console.log(`缓存存储: ${cacheStats.stores}`);
      }
    });
  }
}

资源预加载与缓存

// 资源预加载策略
class ResourcePreloadPlugin {
  constructor(options = {}) {
    this.options = {
      preload: [],
      prefetch: [],
      ...options
    };
  }
  
  apply(compiler) {
    compiler.hooks.compilation.tap('ResourcePreload', (compilation) => {
      compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration.tap(
        'ResourcePreload',
        (data) => {
          const { preload, prefetch } = this.options;
          
          // 添加preload链接
          preload.forEach(resource => {
            data.assets.js.unshift({
              ...resource,
              attributes: { rel: 'preload', as: resource.as || 'script' }
            });
          });
          
          // 添加prefetch链接
          prefetch.forEach(resource => {
            data.assets.js.push({
              ...resource,
              attributes: { rel: 'prefetch' }
            });
          });
        }
      );
    });
  }
}

// Service Worker缓存策略
// sw.js
const CACHE_NAME = 'my-app-v1.0.0';
const STATIC_CACHE = 'static-v1.0.0';
const DYNAMIC_CACHE = 'dynamic-v1.0.0';

// 静态资源缓存策略
const STATIC_ASSETS = [
  '/',
  '/static/js/runtime.js',
  '/static/js/vendors.js',
  '/static/css/main.css',
];

// 缓存策略配置
const CACHE_STRATEGIES = {
  // 静态资源:缓存优先
  static: {
    pattern: /\.(js|css|png|jpg|jpeg|gif|svg|woff|woff2)$/,
    strategy: 'cacheFirst',
    maxAge: 30 * 24 * 60 * 60 * 1000, // 30天
  },
  
  // API请求:网络优先
  api: {
    pattern: /\/api\//,
    strategy: 'networkFirst',
    maxAge: 5 * 60 * 1000, // 5分钟
  },
  
  // HTML页面:网络优先,离线回退
  pages: {
    pattern: /\.html$/,
    strategy: 'networkFirst',
    fallback: '/offline.html',
  },
};

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(STATIC_CACHE)
      .then(cache => cache.addAll(STATIC_ASSETS))
  );
});

self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);
  
  // 选择缓存策略
  const strategy = Object.values(CACHE_STRATEGIES)
    .find(s => s.pattern.test(url.pathname));
  
  if (strategy) {
    event.respondWith(handleRequest(request, strategy));
  }
});

async function handleRequest(request, strategy) {
  const cache = await caches.open(DYNAMIC_CACHE);
  
  switch (strategy.strategy) {
    case 'cacheFirst':
      return cacheFirst(request, cache, strategy);
    case 'networkFirst':
      return networkFirst(request, cache, strategy);
    default:
      return fetch(request);
  }
}

async function cacheFirst(request, cache, strategy) {
  const cached = await cache.match(request);
  if (cached) {
    return cached;
  }
  
  try {
    const response = await fetch(request);
    if (response.ok) {
      cache.put(request, response.clone());
    }
    return response;
  } catch (error) {
    return new Response('Network error', { status: 408 });
  }
}

async function networkFirst(request, cache, strategy) {
  try {
    const response = await fetch(request);
    if (response.ok) {
      cache.put(request, response.clone());
    }
    return response;
  } catch (error) {
    const cached = await cache.match(request);
    return cached || new Response('Offline', { status: 503 });
  }
}

构建性能优化

并行处理与缓存

// webpack.config.js - 构建性能优化
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      // 并行压缩JS
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: true,
            drop_debugger: true,
          },
        },
      }),
      
      // 并行压缩CSS
      new CssMinimizerPlugin({
        parallel: true,
      }),
    ],
  },
  
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          // 并行处理
          {
            loader: 'thread-loader',
            options: {
              workers: require('os').cpus().length - 1,
            },
          },
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true,
              cacheCompression: false,
            },
          },
        ],
      },
    ],
  },
  
  // 解析优化
  resolve: {
    modules: [
      path.resolve(__dirname, 'src'),
      'node_modules',
    ],
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
    // 缓存解析结果
    cache: true,
  },
  
  // 监听优化
  watchOptions: {
    ignored: /node_modules/,
    aggregateTimeout: 300,
    poll: 1000,
  },
};

总结

Webpack优化的核心要点:

  1. 代码分割:合理的chunk分割策略,提升缓存效率
  2. Tree Shaking:消除死代码,减小bundle体积
  3. 缓存策略:文件名哈希、持久化缓存、Service Worker
  4. 构建性能:并行处理、缓存机制、解析优化
  5. 监控分析:构建分析工具,持续优化

Webpack优化是一个持续的过程,需要根据项目特点和性能需求不断调整和完善配置。