性能優化方案

優化分類:

  1. 優化打包後的結果(分包、減小包體積、CDN 服務器) ==> 更重要
  2. 優化打包速度(exclude、cache-loader)

代碼分割(Code Splitting)

一、主要目的

  • 減少首屏加載體積​:避免一次性加載全部代碼
  • 利用瀏覽器緩存​:第三方庫(如 React、Lodash)變動少,可單獨緩存
  • 按需加載/並行請求​:路由、組件、功能模塊只在需要時加載(按需加載或者並行加載文件,而不是一次性加載所有代碼)

二、三種主要的代碼分割方式

1. 入口起點(Entry Points)手動分割

通過配置多個 entry 實現。

// webpack.config.js
module.exports = {
  entry: {
    main: './src/main.js',
    vendor: './src/vendor.js', // 手動引入公共依賴
  },
  output: {
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
  },
};

缺點:

  • 無法自動提取公共依賴(比如 mainvendor 都用了 Lodash,會重複打包)
  • 維護成本高

上面寫的是通用配置,但我們在公司一般會分別配置開發和生產環境的配置。大多數項目中,entry 在 dev 和 prod 基本一致,無需差異化配置。差異主要體現在 output 和其他插件/加載器行為上。

// webpack.config.prod.js
module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    filename: 'js/[name].[contenthash:8].js', // 生產環境用 [contenthash](而非 [hash] 或 [chunkhash]),確保精準緩存
    chunkFilename: 'js/[name].[contenthash:8].js',
    path: path.resolve(__dirname, 'dist'), // 必須輸出到磁盤用於部署
    publicPath: '/static/', // 用於 CDN 或靜態資源服務器
    clean: true, // 清理舊文件
  },
};
// webpack.config.dev.js
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'js/[name].js',  // 開發環境若加 hash,每次保存都會生成新文件,可能干擾熱更新或者devtools混亂
    chunkFilename: 'js/[name].js',
    path: path.resolve(__dirname, 'dist'), // 通常仍寫 dist,但實際不寫入磁盤(webpack-dev-server 默認內存存儲),節省IO,提高編譯速度
    publicPath: '/', // 與 devServer 一致
    // clean: false (默認)
  },
};
2. SplitChunksPlugin(推薦!自動代碼分割)

自動提取公共模塊和第三方庫。webpack 已默認安裝相關插件。

默認行為(僅在 production 模式生效):

// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'async', // 默認只分割異步模塊
    },
  },
};

常用配置:

// webpack.config.prod.js
optimization: { 
  // 自動分割
​  ​// https://twitter.com/wSokra/status/969633336732905474
  // https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366    
  splitChunks: {
    // chunks: async | initial(對通過的代碼處理) | all(同步+異步都處理)
    chunks: 'initial',
    minSize: 20000, // 模塊大於 20KB 才分割(Webpack 5 默認值)
    maxSize: 244000, // 單個 chunk 最大不超過 244KB(可選)
    cacheGroups: { // 拆分分組規則
      // 提取 node_modules 中的第三方庫
      vendor: {
        test: /[\\/]node_modules[\\/]/, // 匹配符合規則的包
        name: 'vendors', // 拆分包的name 屬性
        chunks: 'initial',
        priority: 10, // 優先級高於 default
        enforce: true,
      },
      // 提取多個 chunk 公共代碼
      default: {
        minChunks: 2, // 至少被 2 個 chunk 引用
        priority: -20,
        reuseExistingChunk: true, // 複用已存在的 chunk
        maxInitialRequests: 5, // 默認限制太小,無法顯示效果
        minSize: 0, // 這個示例太小,無法創建公共塊
      },
    },
  },
  // runtime相關的代碼是否抽取到一個單獨的chunk中,比如import動態加載的代碼就是通過runtime 代碼完成的
  // 抽離出來利於瀏覽器緩存,比如修改了業務代碼,那麼runtime加載的chunk無需重新加載
  runtimeChunk: true,
}

在開發環境下 splitChunks: false, 即可。

生產環境:

  • 生成 vendors.xxxx.js(第三方庫)
  • 生成 default.xxxx.js(項目公共代碼)
  • 主 bundle 體積顯著減小
3. 動態導入(Dynamic Imports)—— 按需加載

使用 import() 語法(符合 ES Module 規範),實現懶加載。

Webpack 會為每個 import() 創建一個獨立的 chunk,並自動處理加載邏輯。

三、魔法註釋(Magic Comments)—— 控制 chunk 名稱等行為

// 自定義 chunk 名稱(便於調試和長期緩存)
const module = await import(
  /* webpackChunkName: "my-module" */
  './my-module'
);

其他常見註釋:

  • /* webpackPrefetch: true */:空閒時預加載(提升後續訪問速度)
  • /* webpackPreload: true */:當前導航關鍵資源預加載(慎用)
// 預加載“下一個可能訪問”的頁面
import(
  /* webpackChunkName: "login-page" */
  /* webpackPrefetch: true */
  './LoginPage'
);

詳細比較:

  • preload chunk 會在父 chunk 加載時,以並行方式開始加載。prefetch chunk 會在父 chunk 加載結束後開始加載。
  • preload chunk 具有中等優先級,並立即下載。prefetch chunk 在瀏覽器閒置時下載。

CND

內容分發網絡(Content Delivery Network 或 Content Distribution Network)

它是指通過相互連接的網絡系統,利用最靠近每個用户的服務器;更快、更可靠地將音樂、圖片、視頻、應用程序及其他文件發送給用户;提供高性能、可擴展性及低成本的網絡內容傳遞。

工作中,我們使用 CDN 的主要方式有兩種:

  1. 打包所有靜態資源,放到 CDN 服務器,用户所有資源都是通過 CND 服務器加載的
    1. 通過 output.publicPath 改為自己的的 CDN 服務器,打包後就可以從上面獲取資源
    2. 如果是自己的話,一般會從阿里、騰訊等買 CDN 服務器。
  2. 一些第三方資源放在 CDN 服務器上
    1. 一些庫/框架會將打包後的源碼放到一些免費的 CDN 上,比如 JSDeliver、bootcdn 等
    2. 這樣的話,打包的時候就不需要對這些庫進行打包,直接使用 CDN 服務器中的源碼(通過 externals 配置排除某些包)

CSS 提取

將 css 提取到一個獨立的 css 文件。

npm install mini-css-extract-plugin -D
// webpack.config.prod.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  mode: 'production',
  module: {
    rules: [
      // 生產環境:使用 MiniCssExtractPlugin.loader
      {
        test: /\.css$/i,
        use: [
          MiniCssExtractPlugin.loader, // 替換 style-loader
          'css-loader',
          'postcss-loader',
        ],
      },
      {
        test: /\.s[ac]ss$/i,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
          'sass-loader',
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:8].css',
      chunkFilename: 'css/[name].[contenthash:8].css',
    }),
  ],
};

Terser 代碼壓縮

Terser 可以幫助我們壓縮、醜化(混淆)我們的代碼,讓我們的 bundle 變得更小。

Terser 是一個單獨的工具,擁有非常多的配置,這裏我們只講工作中如何使用,以一個工程的角度學習這個工具。

真實開發中,我們不需要手動的通過 terser 來處理我們的代碼。webpack 中 minimizer 屬性,在 production 模式下,默認就是使用的 TerserPlugin 來處理我們代碼的。我們也可以手動創建 TerserPlugin 實例覆蓋默認配置。

// webpack.prod.js 
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  mode: 'production',
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true, // 多核 CPU 並行壓縮,默認為true,併發數默認為os.cpus().length-1
        terserOptions: {
          compress: { // 壓縮配置
            drop_console: true,
            drop_debugger: true, // 刪除debugger
            pure_funcs: ['console.info', 'console.debug'], // 只刪除特定的函數調用
          },
          mangle: true, // 是否醜化代碼(變量)
          toplevel: true, // 頂層變量是否進行轉換
          keep_classnames: true, // 是否保留類的名稱
          keep_fnames: true, // 是否保留函數的名稱
          format: {
            comments: /@license|@preserve/i, // 保留含 license/preserve 的註釋(某些開源庫要求保留版權註釋)
          },
        },
        extractComments: true, // 默認為true會將註釋提取到一個單獨的文件(這裏用於保留版權註釋),false表示不希望保留註釋
        sourceMap: true,   // 需要 webpack 配置 devtool 生成 source map
      }),
    ],
  },
};

不要在開發環境啓動 terser,因為:

  • 壓縮會拖慢構建速度
  • 混淆後的代碼無法調試
  • hmr 和 source-map 會失效

CSS 壓縮

CSS 壓縮通常是去除無用的空格等,因為很難去修改選擇器、屬性的名稱、值等;我們一般使用插件 css-minimizer-webpack-plugin;他的底層是使用 cssnano 工具來優化、壓縮 CSS(也可以單獨使用)。

使用也是非常簡單:

minimizer: [
  new CssMiniMizerPlugin()({
    parallel: true
  })
]

Tree Shaking 搖樹

詳情見之前文章:《簡單聊聊 webpack 搖樹的原理》

HTTP 壓縮

HTTP 壓縮(HTTP Compression)是一種 在服務器和客户端之間傳輸數據時減小響應體體積 ​的技術,通過壓縮 HTML、CSS、JavaScript、JSON 等文本資源,顯著提升網頁加載速度、節省帶寬。

一、主流壓縮算法

算法 兼容性 壓縮率 速度 説明
gzip ✅ 幾乎所有瀏覽器(IE6+) 最廣泛使用​,Web 標準推薦
Brotli (br) ✅ 現代瀏覽器(Chrome 49+, Firefox 44+, Safari 11+) ⭐ 更高(比 gzip 高 15%~30%) 較慢(壓縮),解壓快 推薦用於靜態資源
deflate ⚠️ 支持不一致(部分瀏覽器實現有問題) 已基本淘汰,不推薦使用

二、工作原理(協商壓縮)

HTTP 壓縮基於 請求頭 ↔ 響應頭協商機制:

  1. 客户端請求(表明支持的壓縮格式)
GET /app.js HTTP/1.1
Host: example.com
Accept-Encoding: gzip, deflate, br // 客户端支持的壓縮算法列表
  1. 服務端響應(返回壓縮後的內容)
HTTP/1.1 200 OK
Content-Encoding: br  // 服務端使用的壓縮算法
Content-Type: application/javascript
Content-Length: 102400  // 注意:這是壓縮後的大小!

...(二進制壓縮數據)...
  • 瀏覽器自動解壓,開發者無感知

三、如何啓用 HTTP 壓縮?

我們一般會優先使用 Nginx 配置做壓縮(生產環境最常用),這樣就無需應用層處理。

除此之外,我們還會進行預壓縮 + 靜態文件服務,這主要就是 webpack 要做的工作。

在構建階段(Webpack/Vite)就生成 .gz.br 文件,部署到 CDN 或靜態服務器。

// webpack.config.js
const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  plugins: [
    // 生成 .gz 文件
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /\.(js|css|html|svg)$/,
      threshold: 8192, // 大於 8KB 才壓縮
      minRatio: 0.8,  // 至少的壓縮比例
    }),
    // 生成 .br 文件(需額外安裝)
    new CompressionPlugin({
      algorithm: 'brotliCompress',
      test: /\.(js|css|html|svg)$/,
      compressionOptions: { level: 11 }, // 最高壓縮率
    }),
  ],
};

Nginx 配合預壓縮文件:

gzip_static on;    # 優先返回 .gz 文件
brotli_static on;  # 優先返回 .br 文件

打包分析

打包時間分析

我們需要藉助一個插件 speed-measure-webpack-plugin,即可看到每個 loader、每個 plugin 消耗的打包時間。

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

const smp = new SpeedMeasurePlugin();

const config = {
  // 你的正常 Webpack 配置
  entry: './src/index.js',
  module: { /* ... */ },
  plugins: [ /* ... */ ],
};

// 僅當環境變量 ANALYZE_SPEED=1 時包裹配置
module.exports = process.env.ANALYZE_SPEED ? smp.wrap(config) : config;

打包文件分析

方法一、生成 stats.json 文件
"build:stats": "w--config ./config/webpack.common.js --env production --profile --json=stats.json",

運行 npm run build:stats,可以獲取到一個 stats.json 文件,然後放到到 http://webpack.github.com/analyse 進行分析。

方法二、webpack-bundle-analyzer

更常用的方式是使用 webpack-bundle-analyzer 插件分析。

// webpack.prod.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  mode: 'production',
  plugins: [
    // 其他插件...
    new BundleAnalyzerPlugin({
      analyzerMode: 'static', // 生成靜態 HTML 報告(默認)
      openAnalyzer: false,    // 不自動打開瀏覽器
      reportFilename: 'bundle-report.html',
      generateStatsFile: true, // 可選:同時生成 stats.json
      statsFilename: 'stats.json',
    }),
  ],
};