帶你深度解鎖Webpack系列(優化篇)

帶你深度解鎖Webpack系列(基礎篇)帶你深度解鎖Webpack系列(進階篇),主要是講解了 Webpack 的配置,可是隨着項目愈來愈大,構建速度可能會愈來愈慢,構建出來的js的體積也愈來愈大,此時就須要對配置進行優化。css

文中羅列出了十多種優化方式,你們能夠結合本身的項目,選擇適當的方式進行優化。這些 Webpack 插件的源碼我大多也沒有看過,主要是結合 Webpack 官方文檔以及項目實踐,在驗證後輸出了本文,若是文中有錯誤的地方,歡迎在評論區指正。html

鑑於前端技術變動迅速,祭出本篇文章基於 Webpack 的版本號:前端

├── webpack@4.41.5 
└── webpack-cli@3.3.10

本文對應的項目地址(編寫本文時使用)供參考:https://github.com/YvetteLau/...node

量化

有時,咱們覺得的優化是負優化,這時,若是有一個量化的指標能夠看出先後對比,那將會是再好不過的一件事。react

speed-measure-webpack-plugin 插件能夠測量各個插件和loader所花費的時間,使用以後,構建時,會獲得相似下面這樣的信息:jquery

smp.jpeg
對比先後的信息,來肯定優化的效果。webpack

speed-measure-webpack-plugin 的使用很簡單,能夠直接用其來包裹 Webpack 的配置:git

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

const config = {
    //...webpack配置
}

module.exports = smp.wrap(config);

1.exclude/include

咱們能夠經過 excludeinclude 配置來確保轉譯儘量少的文件。顧名思義,exclude 指定要排除的文件,include 指定要包含的文件。github

exclude 的優先級高於 include,在 includeexclude 中使用絕對路徑數組,儘可能避免 exclude,更傾向於使用 includeweb

//webpack.config.js
const path = require('path');
module.exports = {
    //...
    module: {
        rules: [
            {
                test: /\.js[x]?$/,
                use: ['babel-loader'],
                include: [path.resolve(__dirname, 'src')]
            }
        ]
    },
}

下圖是我未配置 include 和配置了 include 的構建結果對比:

include:exclude.jpeg

2. cache-loader

在一些性能開銷較大的 loader 以前添加 cache-loader,將結果緩存中磁盤中。默認保存在 node_modueles/.cache/cache-loader 目錄下。

首先安裝依賴:

npm install cache-loader -D

cache-loader 的配置很簡單,放在其餘 loader 以前便可。修改Webpack 的配置以下:

module.exports = {
    //...
    
    module: {
        //個人項目中,babel-loader耗時比較長,因此我給它配置了`cache-loader`
        rules: [
            {
                test: /\.jsx?$/,
                use: ['cache-loader','babel-loader']
            }
        ]
    }
}

若是你跟我同樣,只打算給 babel-loader 配置 cache 的話,也能夠不使用 cache-loader,給 babel-loader 增長選項 cacheDirectory

cache-loader.jpeg

cacheDirectory:默認值爲 false。當有設置時,指定的目錄將用來緩存 loader 的執行結果。以後的 Webpack 構建,將會嘗試讀取緩存,來避免在每次執行時,可能產生的、高性能消耗的 Babel 從新編譯過程。設置空值或者 true 的話,使用默認緩存目錄:node_modules/.cache/babel-loader。開啓 babel-loader的緩存和配置 cache-loader,我比對了下,構建時間很接近。

3.happypack

因爲有大量文件須要解析和處理,構建是文件讀寫和計算密集型的操做,特別是當文件數量變多後,Webpack 構建慢的問題會顯得嚴重。文件讀寫和計算操做是沒法避免的,那能不能讓 Webpack 同一時刻處理多個任務,發揮多核 CPU 電腦的威力,以提高構建速度呢?

HappyPack 就能讓 Webpack 作到這點,它把任務分解給多個子進程去併發的執行,子進程處理完後再把結果發送給主進程。

首先須要安裝 happypack:

npm install happypack -D

修改配置文件:

const Happypack = require('happypack');
module.exports = {
    //...
    module: {
        rules: [
            {
                test: /\.js[x]?$/,
                use: 'Happypack/loader?id=js',
                include: [path.resolve(__dirname, 'src')]
            },
            {
                test: /\.css$/,
                use: 'Happypack/loader?id=css',
                include: [
                    path.resolve(__dirname, 'src'),
                    path.resolve(__dirname, 'node_modules', 'bootstrap', 'dist')
                ]
            },
            {
                test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2|.gexf)$/,
                use: 'Happypack/loader?id=file',
                include: [
                    path.resolve(__dirname, 'src'),
                    path.resolve(__dirname, 'public'),
                    path.resolve(__dirname, 'node_modules', 'bootstrap', 'dist')
                ]
            }
        ]
    },
    plugins: [
        new Happypack({
            id: 'js', //和rule中的id=js對應
            //將以前 rule 中的 loader 在此配置
            use: ['babel-loader'] //必須是數組
        }),
        new Happypack({
            id: 'css',//和rule中的id=css對應
            use: ['style-loader', 'css-loader','postcss-loader'],
        }),
        new Happypack({
            id: 'file', //和rule中的id=file對應
            use: [{
                loader: 'url-loader',
                options: {
                    limit: 10240 //10K
                }
            }],
        }),
    ]
}

happypack 默認開啓 CPU核數 - 1 個進程,固然,咱們也能夠傳遞 threadsHappypack

happypack.jpeg

說明:當 postcss-loader 配置在 Happypack 中,必需要在項目中建立 postcss.config.js

//postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer')()
    ]
}

不然,會拋出錯誤: Error: No PostCSS Config found

另外,當你的項目不是很複雜時,不須要配置 happypack,由於進程的分配和管理也須要時間,並不能有效提高構建速度,甚至會變慢。

4.thread-loader

除了使用 Happypack 外,咱們也可使用 thread-loader ,把 thread-loader 放置在其它 loader 以前,那麼放置在這個 loader 以後的 loader 就會在一個單獨的 worker 池中運行。

在 worker 池(worker pool)中運行的 loader 是受到限制的。例如:

  • 這些 loader 不能產生新的文件。
  • 這些 loader 不能使用定製的 loader API(也就是說,經過插件)。
  • 這些 loader 沒法獲取 webpack 的選項設置。

首先安裝依賴:

npm install thread-loader -D

修改配置:

module.exports = {
    module: {
        //個人項目中,babel-loader耗時比較長,因此我給它配置 thread-loader
        rules: [
            {
                test: /\.jsx?$/,
                use: ['thread-loader', 'cache-loader', 'babel-loader']
            }
        ]
    }
}

thread-loaderHappypack 我對比了一下,構建時間基本沒什麼差異。不過 thread-loader 配置起來爲簡單。

5.開啓 JS 多進程壓縮

雖然不少 webpack 優化的文章上會說起多進程壓縮的優化,不論是 webpack-parallel-uglify-plugin 或者是 uglifyjs-webpack-plugin 配置 parallel。不過這裏我要說一句,不必單獨安裝這些插件,它們並不會讓你的 Webpack 更快。

由於當前 Webpack 默認使用的是 TerserWebpackPlugin,默認開啓了多進程和緩存,構建時,你的項目中能夠看到 terser 的緩存文件 node_modules/.cache/terser-webpack-plugin

6.HardSourceWebpackPlugin

HardSourceWebpackPlugin 爲模塊提供中間緩存,緩存默認的存放路徑是: node_modules/.cache/hard-source

配置 hard-source-webpack-plugin,首次構建時間沒有太大變化,可是第二次開始,構建時間大約能夠節約 80%。

首先安裝依賴:

npm install hard-source-webpack-plugin -D

修改 webpack 的配置:

//webpack.config.js
var HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
    //...
    plugins: [
        new HardSourceWebpackPlugin()
    ]
}

用另一個比較大的項目測試了下,配置了 HardSourceWebpackPlugin,構建時間從 8S 左右降到了 2S 左右。

HardSourceWebpackPlugin文檔中 列出了一些你可能會遇到的問題以及如何解決,例如熱更新失效,或者某些配置不生效等。

7.noParse

若是一些第三方模塊沒有AMD/CommonJS規範版本,可使用 noParse 來標識這個模塊,這樣 webpack 會引入這些模塊,可是不進行轉化和解析,從而提高 webpack 的構建性能 ,例如:jquerylodash

noParse 屬性的值是一個正則表達式或者是一個 function

//webpack.config.js
module.exports = {
    //...
    module: {
        noParse: /jquery|lodash/
    }
}

我當前的 webpack-optimize 項目中,沒有使用 jquery 或者是 lodash

所以新建一個項目測試,只引入 jqueryloadsh,而後配置 noParse 和不配置 noParse,分別構建比對時間。

配置noParse 前,構建須要 2392ms。配置了 noParse 以後,構建須要 1613ms。 若是你使用到了不須要解析的第三方依賴,那麼配置 noParse 很顯然是必定會起到優化做用的。

8.resolve

resolve 配置 webpack 如何尋找模塊所對應的文件。假設咱們肯定模塊都從根目錄下的 node_modules 中查找,咱們能夠配置:

//webpack.config.js
const path = require('path');
module.exports = {
    //...
    resolve: {
        modules: [path.resolve(__dirname, 'node_modules')],
    }
}

須要記住的是,若是你配置了上述的 resolve.moudles ,可能會出現問題,例如,你的依賴中還存在 node_modules 目錄,那麼就會出現,對應的文件明明在,可是卻提示找不到。所以呢,我的不推薦配置這個。若是其餘同事不熟悉這個配置,遇到這個問題時,會摸不着頭腦。

另外,resolveextensions 配置,默認是 ['.js', '.json'],若是你要對它進行配置,記住將頻率最高的後綴放在第一位,而且控制列表的長度,以減小嚐試次數。

本項目較小,所以測試時,此處優化效果不明顯。

9.IgnorePlugin

webpack 的內置插件,做用是忽略第三方包指定目錄。

例如: moment (2.24.0版本) 會將全部本地化內容和核心功能一塊兒打包,咱們就可使用 IgnorePlugin 在打包時忽略本地化內容。

//webpack.config.js
module.exports = {
    //...
    plugins: [
        //忽略 moment 下的 ./locale 目錄
        new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
    ]
}

在使用的時候,若是咱們須要指定語言,那麼須要咱們手動的去引入語言包,例如,引入中文語言包:

import moment from 'moment';
import 'moment/locale/zh-cn';// 手動引入

index.js 中只引入 moment,打包出來的 bundle.js 大小爲 263KB,若是配置了 IgnorePlugin,單獨引入 moment/locale/zh-cn,構建出來的包大小爲 55KB

10.externals

咱們能夠將一些JS文件存儲在 CDN 上(減小 Webpack打包出來的 js 體積),在 index.html 中經過 <script> 標籤引入,如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="root">root</div>
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</body>
</html>

咱們但願在使用時,仍然能夠經過 import 的方式去引用(如 import $ from 'jquery'),而且但願 webpack 不會對其進行打包,此時就能夠配置 externals

//webpack.config.js
module.exports = {
    //...
    externals: {
        //jquery經過script引入以後,全局中即有了 jQuery 變量
        'jquery': 'jQuery'
    }
}

11.DllPlugin

有些時候,若是全部的JS文件都打成一個JS文件,會致使最終生成的JS文件很大,這個時候,咱們就要考慮拆分 bundles

DllPluginDLLReferencePlugin 能夠實現拆分 bundles,而且能夠大大提高構建速度,DllPluginDLLReferencePlugin 都是 webpack 的內置模塊。

咱們使用 DllPlugin 將不會頻繁更新的庫進行編譯,當這些依賴的版本沒有變化時,就不須要從新編譯。咱們新建一個 webpack 的配置文件,來專門用於編譯動態連接庫,例如名爲: webpack.config.dll.js,這裏咱們將 reactreact-dom 單獨打包成一個動態連接庫。

//webpack.config.dll.js
const webpack = require('webpack');
const path = require('path');

module.exports = {
    entry: {
        react: ['react', 'react-dom']
    },
    mode: 'production',
    output: {
        filename: '[name].dll.[hash:6].js',
        path: path.resolve(__dirname, 'dist', 'dll'),
        library: '[name]_dll' //暴露給外部使用
        //libraryTarget 指定如何暴露內容,缺省時就是 var
    },
    plugins: [
        new webpack.DllPlugin({
            //name和library一致
            name: '[name]_dll', 
            path: path.resolve(__dirname, 'dist', 'dll', 'manifest.json') //manifest.json的生成路徑
        })
    ]
}

package.jsonscripts 中增長:

{
    "scripts": {
        "dev": "NODE_ENV=development webpack-dev-server",
        "build": "NODE_ENV=production webpack",
        "build:dll": "webpack --config webpack.config.dll.js"
    },
}

執行 npm run build:all,能夠看到 dist 目錄以下,之因此將動態連接庫單獨放在 dll 目錄下,主要是爲了使用 CleanWebpackPlugin 更爲方便的過濾掉動態連接庫。

dist
└── dll
    ├── manifest.json
    └── react.dll.9dcd9d.js

manifest.json 用於讓 DLLReferencePlugin 映射到相關依賴上。

修改 webpack 的主配置文件: webpack.config.js 的配置:

//webpack.config.js
const webpack = require('webpack');
const path = require('path');
module.exports = {
    //...
    devServer: {
        contentBase: path.resolve(__dirname, 'dist')
    },
    plugins: [
        new webpack.DllReferencePlugin({
            manifest: path.resolve(__dirname, 'dist', 'dll', 'manifest.json')
        }),
        new CleanWebpackPlugin({
            cleanOnceBeforeBuildPatterns: ['**/*', '!dll', '!dll/**'] //不刪除dll目錄
        }),
        //...
    ]
}

使用 npm run build 構建,能夠看到 bundle.js 的體積大大減小。

修改 public/index.html 文件,在其中引入 react.dll.js

<script src="/dll/react.dll.9dcd9d.js"></script>
構建速度

DllPlugin.jpeg

包體積

dll-size.jpeg

12.抽離公共代碼

抽離公共代碼是對於多頁應用來講的,若是多個頁面引入了一些公共模塊,那麼能夠把這些公共的模塊抽離出來,單獨打包。公共代碼只須要下載一次就緩存起來了,避免了重複下載。

抽離公共代碼對於單頁應用和多頁應該在配置上沒有什麼區別,都是配置在 optimization.splitChunks 中。

//webpack.config.js
module.exports = {
    optimization: {
        splitChunks: {//分割代碼塊
            cacheGroups: {
                vendor: {
                    //第三方依賴
                    priority: 1, //設置優先級,首先抽離第三方模塊
                    name: 'vendor',
                    test: /node_modules/,
                    chunks: 'initial',
                    minSize: 0,
                    minChunks: 1 //最少引入了1次
                },
                //緩存組
                common: {
                    //公共模塊
                    chunks: 'initial',
                    name: 'common',
                    minSize: 100, //大小超過100個字節
                    minChunks: 3 //最少引入了3次
                }
            }
        }
    }
}

即便是單頁應用,一樣可使用這個配置,例如,打包出來的 bundle.js 體積過大,咱們能夠將一些依賴打包成動態連接庫,而後將剩下的第三方依賴拆出來。這樣能夠有效減少 bundle.js 的體積大小。固然,你還能夠繼續提取業務代碼的公共模塊,此處,由於我項目中源碼較少,因此沒有配置。

splitChunks.jpeg

runtimeChunk

runtimeChunk 的做用是將包含 chunk 映射關係的列表從 main.js 中抽離出來,在配置了 splitChunk 時,記得配置 runtimeChunk.

module.exports = {
    //...
    optimization: {
        runtimeChunk: {
            name: 'manifest'
        }
    }
}

最終構建出來的文件中會生成一個 manifest.js

藉助 webpack-bundle-analyzer 進一步優化

在作 webpack 構建優化的時候,vendor 打出來超過了1M,reactreact-dom 已經打包成了DLL。

所以須要藉助 webpack-bundle-analyzer 查看一下是哪些包的體積較大。

首先安裝依賴:

npm install webpack-bundle-analyzer -D

使用也很簡單,修改下咱們的配置:

//webpack.config.prod.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.config.base');
module.exports = merge(baseWebpackConfig, {
    //....
    plugins: [
        //...
        new BundleAnalyzerPlugin(),
    ]
})

npm run build 構建,會默認打開: http://127.0.0.1:8888/,能夠看到各個包的體積:

W1.jpeg

進一步對 vendor 進行拆分,將 vendor 拆分紅了4個(使用 splitChunks 進行拆分便可)。

module.exports = {
    optimization: {
    concatenateModules: false,
    splitChunks: {//分割代碼塊
      maxInitialRequests:6, //默認是5
      cacheGroups: {
        vendor: {
          //第三方依賴
          priority: 1,
          name: 'vendor',
          test: /node_modules/,
          chunks: 'initial',
          minSize: 100,
          minChunks: 1 //重複引入了幾回
        },
        'lottie-web': {
          name: "lottie-web", // 單獨將 react-lottie 拆包
          priority: 5, // 權重需大於`vendor`
          test: /[\/]node_modules[\/]lottie-web[\/]/,
          chunks: 'initial',
          minSize: 100,
          minChunks: 1 //重複引入了幾回
        },
        //...
      }
    },
  },
}

從新構建,結果以下所示:

W2.jpeg

13.webpack自身的優化

tree-shaking

若是使用ES6的import 語法,那麼在生產環境下,會自動移除沒有使用到的代碼。

//math.js
const add = (a, b) => {
    console.log('aaaaaa')
    return a + b;
}

const minus = (a, b) => {
    console.log('bbbbbb')
    return a - b;
}

export {
    add,
    minus
}
//index.js
import {add, minus} from './math';
add(2,3);

構建的最終代碼裏,minus 函數不會被打包進去。

scope hosting 做用域提高

變量提高,能夠減小一些變量聲明。在生產環境下,默認開啓。

另外,你們測試的時候注意一下,speed-measure-webpack-pluginHotModuleReplacementPlugin 不能同時使用,不然會報錯:

webpack 的配置部分到此基本就結束了,typescripteslintprettier,這裏沒有說起,你們能夠本身配置一下,也能夠參考我以前練手時配置的一個項目:https://github.com/YvetteLau/...

babel 配置的優化

若是你對 babel 還不太熟悉的話,那麼能夠閱讀這篇文章:不容錯過的 Babel7 知識

在不配置 @babel/plugin-transform-runtime 時,babel 會使用很小的輔助函數來實現相似 _createClass 等公共方法。默認狀況下,它將被注入(inject)到須要它的每一個文件中。可是這樣的結果就是致使構建出來的JS體積變大。

咱們也並不須要在每一個 js 中注入輔助函數,所以咱們可使用 @babel/plugin-transform-runtime@babel/plugin-transform-runtime 是一個能夠重複使用 Babel 注入的幫助程序,以節省代碼大小的插件。

所以咱們能夠在 .babelrc 中增長 @babel/plugin-transform-runtime 的配置。

{
    "presets": [],
    "plugins": [
        [
            "@babel/plugin-transform-runtime"
        ]
    ]
}

以上就是我目前爲止使用到的一些優化,若是你有更好的優化方式,歡迎在評論區留言,感謝閱讀。

最後

若是本文對你有幫助的話,給本文點個贊吧。

參考文檔:
相關文章
相關標籤/搜索