Desmond

Desmond

An introvert who loves web programming, graphic design and guitar
github
bilibili
twitter

Webpack 性能調優

Webpack 的優化瓶頸,主要是兩個方面:

  • Webpack 的構建過程太花時間
  • Webpack 打包的結果體積太大

構建過程提速策略#

不要讓 loader 做太多事情 —— 以 babel-loader 為例#

最常見的優化方式是,用 include 或 exclude 來幫我們避免不必要的轉譯,比如 Webpack 官方在介紹 babel-loader 時給出的示例:

module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}

這段代碼幫我們規避了對龐大的 node_modules 文件夾或者 bower_components 文件夾的處理。但通過限定文件範圍帶來的性能提升是有限的。除此之外,如果我們選擇開啟緩存將轉譯結果緩存至文件系統,則至少可以將 babel-loader 的工作效率提升兩倍。要做到這點,我們只需要為 loader 增加相應的參數設定:

loader: 'babel-loader?cacheDirectory=true'

不要放過第三方庫#

處理第三方庫的姿勢有很多,其中,CommonsChunkPlugin 每次構建時都會重新構建一次 vendor;出於對效率的考量,我們更多是使用 DllPlugin。

DllPlugin 是基於 Windows 動態鏈接庫(dll)的思想被創作出來的。這個插件會把第三方庫單獨打包到一個文件中,這個文件就是一個單純的依賴庫。這個依賴庫不會跟著你的業務代碼一起被重新打包,只有當依賴自身發生版本變化時才會重新打包。

用 DllPlugin 處理文件,要分兩步走:

  • 基於 dll 專屬的配置文件,打包 dll 庫
  • 基於 webpack.config.js 文件,打包業務代碼

以一個基於 React 的簡單項目為例,我們的 dll 的配置文件可以編寫如下:

const path = require('path')
const webpack = require('webpack')

module.exports = {
    entry: {
      // 依賴的庫數組
      vendor: [
        'prop-types',
        'babel-polyfill',
        'react',
        'react-dom',
        'react-router-dom',
      ]
    },
    output: {
      path: path.join(__dirname, 'dist'),
      filename: '[name].js',
      library: '[name]_[hash]',
    },
    plugins: [
      new webpack.DllPlugin({
        // DllPlugin的name屬性需要和libary保持一致
        name: '[name]_[hash]',
        path: path.join(__dirname, 'dist', '[name]-manifest.json'),
        // context需要和webpack.config.js保持一致
        context: __dirname,
      }),
    ],
}

編寫完成之後,運行這個配置文件,我們的 dist 文件夾裡會出現一個叫 vendor-manifest.json 的文件用於描述每個第三方庫對應的具體路徑。
隨後,我們只需在 webpack.config.js 裡針對 dll 稍作配置:

const path = require('path');
const webpack = require('webpack')
module.exports = {
  mode: 'production',
  // 編譯入口
  entry: {
    main: './src/index.js'
  },
  // 目標文件
  output: {
    path: path.join(__dirname, 'dist/'),
    filename: '[name].js'
  },
  // dll相關配置
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      // manifest就是我們第一步中打包出來的json文件
      manifest: require('./dist/vendor-manifest.json'),
    })
  ]
}

Happypack—— 將 loader 由單進程轉為多進程#

大家知道,Webpack 是單線程的,就算此刻存在多個任務,你也只能排隊一個接一個地等待處理。這是 Webpack 的缺點,好在我們的 CPU 是多核的,Happypack 會充分釋放 CPU 在多核並發方面的優勢,幫我們把任務分解給多個子進程去並發執行,大大提升打包效率。

HappyPack 的使用方法也非常簡單,只需要我們把對 loader 的配置轉移到 HappyPack 中去就好,我們可以手動告訴 HappyPack 我們需要多少個並發的進程:

const HappyPack = require('happypack')
// 手動創建進程池
const happyThreadPool =  HappyPack.ThreadPool({ size: os.cpus().length })

module.exports = {
  module: {
    rules: [
      ...
      {
        test: /\.js$/,
        // 問號後面的查詢參數指定了處理這類文件的HappyPack實例的名字
        loader: 'happypack/loader?id=happyBabel',
        ...
      },
    ],
  },
  plugins: [
    ...
    new HappyPack({
      // 這個HappyPack的“名字”就叫做happyBabel,和樓上的查詢參數遙相呼應
      id: 'happyBabel',
      // 指定進程池
      threadPool: happyThreadPool,
      loaders: ['babel-loader?cacheDirectory']
    })
  ],
}

構建結果體積壓縮#

文件結構可視化,找出導致體積過大的原因#

一個非常好用的包組成可視化工具 ——webpack-bundle-analyzer,配置方法和普通的 plugin 無異,它會以矩形樹圖的形式將包內各個模塊的大小和依賴關係呈現出來。在使用時,我們只需要將其以插件的形式引入:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}

刪除冗餘代碼#

基於 import/export 語法,Tree-Shaking 可以在編譯的過程中獲悉哪些模塊並沒有真正被使用,這些沒用的代碼,在最後打包的時候會被去除。Tree-Shaking 的針對性很強,它更適合用來處理模塊級別的冗餘代碼。至於粒度更細的冗餘代碼的去除,往往會被整合進 JS 或 CSS 的壓縮或分離過程中。

這裡我們以當下接受度較高的 UglifyJsPlugin 為例,看一下如何在壓縮過程中對碎片化的冗餘代碼(如 console 語句、註釋等)進行自動化刪除:

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
 plugins: [
   new UglifyJsPlugin({
     // 允許並發
     parallel: true,
     // 開啟緩存
     cache: true,
     compress: {
       // 刪除所有的console語句    
       drop_console: true,
       // 把使用多次的靜態值自動定義為變量
       reduce_vars: true,
     },
     output: {
       // 不保留註釋
       comment: false,
       // 使輸出的代碼儘可能緊湊
       beautify: false
     }
   })
 ]
}

這段手動引入 UglifyJsPlugin 的代碼其實是 webpack3 的用法,webpack4 現在已經默認使用 uglifyjs-webpack-plugin 對代碼做壓縮了 —— 在 webpack4 中,我們是通過配置 optimization.minimize 與 optimization.minimizer 來自定義壓縮相關的操作的。

按需加載#

  • 一次不加載完所有的文件內容,只加載此刻需要用到的那部分(會提前做拆分)
  • 當需要更多內容時,再對用到的內容進行即時加載

當我們不需要按需加載的時候,我們的代碼是這樣的:

import BugComponent from '../pages/BugComponent'
...
<Route path="/bug" component={BugComponent}>

為了開啟按需加載,我們要稍作改動。首先 webpack 的配置文件要走起來:

output: {
    path: path.join(__dirname, '/../dist'),
    filename: 'app.js',
    publicPath: defaultSettings.publicPath,
    // 指定 chunkFilename
    chunkFilename: '[name].[chunkhash:5].chunk.js',
},

路由處的代碼也要做一下配合:

const getComponent => (location, cb) {
  // 核心方法
  require.ensure([], (require) => {
    cb(null, require('../pages/BugComponent').default)
  }, 'bug')
},
...
<Route path="/bug" getComponent={getComponent}>

require.ensure(dependencies, callback, chunkName) 這是一個異步的方法,Webpack 在打包時,BugComponent 會被單獨打成一個文件,只有在我們跳轉 bug 這個路由的時候,這個異步方法的回調才會生效,才會真正地去獲取 BugComponent 的內容。這就是按需加載。所謂按需加載,根本上就是在正確的時機去觸發相應的回調。


載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。