1. 程式人生 > 其它 >webpack之效能優化(webpack4)

webpack之效能優化(webpack4)

在講解效能優化的方案之前,我們需要了解一下webpack的整個工作流程,

方案一:減少模組解析

也就是省略了構建chunk依賴模組的這幾個步驟

如果沒有loader對該模組進行處理,該模組的原始碼就是最終打包結果的程式碼。不對某個模組進行解析,可以縮短構建時間

哪些模組不需要解析?

模組中無其他依賴

webpack配置

配置module.noParse,它是一個正則,被正則匹配到的模組不會解析

module.exports = {
    mode: "development",
    module: {
        noParse: /test/
    }
}

方案二:優化loader

1.對於某些庫,不使用loader

例如:babel-loader可以轉換ES6或更高版本的語法,可是有些庫本身就是用ES5語法書寫的,不需要轉換,使用babel-loader反而會浪費構建時間

通過module.rule.excludemodule.rule.include,排除或僅包含需要應用loader的場景,可以直接排除掉node_modules的所有包,也可以僅排除單獨的包

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: 
/node_modules/,// exclude: /lodash/ // // include: /src/, use: "babel-loader" } ] } }

2.利用cache-loader對模組進行快取

如果某個檔案內容不變,經過相同的loader解析後,解析後的結果也不變,所以可以將loader的解析結果儲存下來,讓後續的解析直接使用儲存的結果

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

大家可能會感到疑惑,明明loader是從後往前執行的,那麼cache-loader是怎麼拿到babel-loader的結果的呢?

其實,每個loader上,還有一個pitch的靜態方法

function loader(source){
  return `new source`
}

loader.pitch = function(filePath){
  // 可返回可不返回
  // 如果返回,返回原始碼
}

module.exports = loader;

loader真正執行的順序是這樣的:

loader1.pitch => loader2.pitch => loader3.pitch => loader3 => loader2 => loader1

因此,以['cache-loader', 'babel-loader']為例,

第一次打包:

  • 先呼叫cache-loader.pitch,發現無快取,往後執行,
  • 呼叫babel-loader.pitch,也發現無快取,往後執行,
  • 讀取當前需要處理的模組的程式碼
  • 呼叫babel-loader,返回修改成es5的程式碼
  • 呼叫cache-loader,返回babel-loader處理的結果程式碼並快取

第二次打包:

  • 先呼叫cache-loader.pitch,發現有快取,則返回原始碼
  • 直接返回上次處理好的原始碼,不會繼續往後走了

當然對於babel-loader,使用它本身的配置也是可以快取的

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use:  'babel-loader?cacheDirectory'
      },
    ],
  },
};

3.開啟thread-loader

它會把後續的loader放到執行緒池的執行緒中執行,以提高構建效率

由於後續的loader會放到新的執行緒中,所以,後續的loader不能:

  • 使用 webpack api 生成檔案 (loader上下文中的emitFile、emitError等api)
  • 無法使用自定義的 plugin api (某些外掛提供了自身的plugin和loader,plugin會向webpack注入新的api,loader中會使用)
  • 無法訪問 webpack的配置

注意,開啟和管理執行緒需要消耗時間,在小型專案中使用thread-loader反而會增加構建時間

方案三:熱替換

熱替換並不能降低構建時間(可能還會稍微增加),但可以減少程式碼改動到效果呈現的時間

// webpack配置
module.exports = {
  devServer:{
    hot:true // 開啟HMR
  }
}
// index.js

if(module.hot){ // 是否開啟了熱更新
  module.hot.accept() // 接受熱更新
}

方案四:動態連結庫

什麼情況下使用?

當打包出來的多個bundle.js檔案都有重複的第三方程式碼,會增加檔案的體積,不利於傳輸

打包的過程:

1.使用output.library配置公共模組的全域性變數名

// webpack.dll.config.js
module.exports = {
  mode: "production",
  entry: {
    jquery: ["jquery"],
    lodash: ["lodash"]
  },
  output: {
    filename: "dll/[name].js",
    library: "[name]"// 每個buldle暴露的全域性變數名
  }
};

打包結果

// dist/dll/lodash
var lodash=function(n){xxx}
// dist/dll/jquery
var jquery=function(n){xxx}

2.用DllPlugin建立資源清單(包含資訊:全域性變數名、node_modules對應包的路徑)

// webpack.dll.config.js
module.exports = {
  plugins: [
    new webpack.DllPlugin({
      path: path.resolve(__dirname, "dll", "[name].manifest.json"), //資源清單的儲存位置
      name: "[name]"//資源清單中,暴露的變數名
    })
  ]
};

打包生成的資源清單

// dll/lodash.manifest.json
{
  "name": "lodash",
  "content": {
    "./node_modules/lodash/lodash.js": {
      xxx
    }
  }
}
// dll/jquery.manifest.json

{
  "name": "jquery",
  "content": {
    "./node_modules/jquery/dist/jquery.js": {
      xxx
    }
  }
}

3.用DllReferencePlugin使用資源清單

在頁面中手動引入公共模組

<script src="./dll/jquery.js"></script>
<script src="./dll/lodash.js"></script>

重新設定clean-webpack-plugin

如果使用了外掛clean-webpack-plugin,為了避免它把公共模組清除,需要做出以下配置,webpack.config.js配置(注意不是和output.library、DllPlugin在同一個配置檔案中哦):

new CleanWebpackPlugin({
  // 要清除的檔案或目錄
  // 排除掉dll目錄本身和它裡面的檔案
  cleanOnceBeforeBuildPatterns: ["**/*", '!dll', '!dll/*']
})

使用DllReferencePlugin

找到對應的資源清單,根據暴露的變數名(output.library)匹配第三方庫在node_modules中的路徑,不需要將程式碼打包到bundle.js中,webpack.config.js:
    new webpack.DllReferencePlugin({
      manifest: require("./dll/jquery.manifest.json")
    }),
    new webpack.DllReferencePlugin({
      manifest: require("./dll/lodash.manifest.json")
    })

打包過程:首先要根據webpack.dll.config.js配置檔案打包一次,之後再根據webpack.config.js打包

最終打包結果的格式:

(function(modules){
  //...
})({
  // index.js檔案的打包結果並沒有變化
  "./src/index.js":
  function(module, exports, __webpack_require__){
    var $ = __webpack_require__("./node_modules/jquery/index.js")
    var _ = __webpack_require__("./node_modules/lodash/index.js")
    _.isArray($(".red"));
  },
  // 由於資源清單中存在,jquery的程式碼並不會出現在這裡
  "./node_modules/jquery/index.js":
  function(module, exports, __webpack_require__){
    module.exports = jquery;
  },
  // 由於資源清單中存在,lodash的程式碼並不會出現在這裡
  "./node_modules/lodash/index.js":
  function(module, exports, __webpack_require__){
    module.exports = lodash;
  }
})

優點:

  • 極大提升自身模組的打包速度
  • 極大的縮小了自身檔案體積
  • 有利於瀏覽器快取第三方庫的公共程式碼

缺點:

  • 使用非常繁瑣
  • 如果第三方庫中包含重複程式碼,則效果不太理想

方案五:抽離公共程式碼

有多個模組都引用了公共模組,當一個模組載入時,訪問了公共模組,並快取下來,另一個模組載入就可以直接使用快取的結果。

module.exports = {
  optimization: {
    splitChunks: {
      // 分包策略
      chunks: "all",
      cacheGroups: {
        // 公共模組
        common: {
          mixSize: 0,
          minChunks: 2, // 至少被幾個檔案引用
        },
        vendors: {
          test: /[\\/]node_modules[\\/]/, // 當匹配到相應模組時,將這些模組進行單獨打包
          priority: 1 // 快取組優先順序,優先順序越高,該策略越先進行處理,預設值為0
        },
      }
    }
  }
}

還可以抽離公共樣式,使用MiniCssExtractPlugin

module.exports = {
  optimization: {
    splitChunks: {
      chunks: "all",
      cacheGroups: {
        styles: {
          test: /\.css$/, // 匹配樣式模組
          minSize: 0, // 覆蓋預設的最小尺寸,這裡僅僅是作為測試
          minChunks: 2 // 覆蓋預設的最小chunk引用數
        }
      }
    }
  },
  module: {
    rules: [{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] }]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      filename: "[name].[hash:5].css",
      // chunkFilename是配置來自於分割chunk的檔名
      chunkFilename: "common.[hash:5].css" 
    })
  ]
}

通過cdn方式引入js、css檔案,將不怎麼需要更新的第三方庫脫離webpack打包,不被打入bundle中,從而減少打包時間,但又不影響運用第三方庫的方式,例如import方式等。

const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');

module.exports = {
    plugins: [
        new HtmlWebpackExternalsPlugin({
          externals: [{
            module: 'vue',
            entry: 'https://lib.baomitu.com/vue/2.6.12/vue.min.js',
            global: 'Vue'
          }]
        })
    ],
}

最後看到在dist/index.html中動態添加了如下程式碼:

<script type="text/javascript" src="https://lib.baomitu.com/vue/2.6.12/vue.min.js"></script>

方案六:程式碼壓縮

壓縮js和css程式碼:

const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
  optimization: {
    // 是否要啟用壓縮,預設情況下,生產環境會自動開啟
    minimize: true, 
    minimizer: [ // 壓縮時使用的外掛
    // 壓縮js檔案
      new TerserPlugin({
parallel: true // 開啟多執行緒壓縮
}),
// 壓縮css檔案 new OptimizeCSSAssetsPlugin() ], }, };

使用compression-webpack-plugin外掛對打包結果進行預壓縮,可以移除伺服器的壓縮時間

    new CmpressionWebpackPlugin({
      test: /\.js/,
      minRatio: 0.5
    })