webpack構(gòu)建優(yōu)化清單

最近給項(xiàng)目進(jìn)行webpack優(yōu)化,嘗試過幾乎所有方法,一共26條,列舉在此。

優(yōu)化webpack,首先明確優(yōu)化目標(biāo):

  1. 構(gòu)建速度: 開發(fā)環(huán)境項(xiàng)目的啟動(dòng)速度,以及生產(chǎn)環(huán)境項(xiàng)目的打包速度。構(gòu)建時(shí)間越短,速度越快越好。
  2. 增量構(gòu)建速度:開發(fā)項(xiàng)目時(shí),每修改一次代碼,webpack會(huì)對修改的部分進(jìn)重新構(gòu)建。增量構(gòu)建時(shí)間越短,速度越快越好。
  3. 打包體積:開發(fā)環(huán)境webpack打包后,生成的文件體積。打包體積越小越好
  4. 加載體積: 除關(guān)注打包體積外,瀏覽器打開頁面,加載資源的體積也很重要。按需加載、緩存等技術(shù)可以減少加載體積。加載體積越小越好。

減少依賴包,縮小打包體積,構(gòu)建速度會(huì)更快。使用壓縮,打包體積會(huì)減小,但構(gòu)建速度則變慢。討論webpack的優(yōu)化,速度體積需同時(shí)考慮,二者有時(shí)正相關(guān),有時(shí)負(fù)相關(guān),需要均衡速度和體積。

下文中每個(gè)優(yōu)化建議,都會(huì)列出優(yōu)化目標(biāo),影響明顯的會(huì)加粗。除速度和體積,還有: 分析, 即幫助分析webpack打包性能,呈現(xiàn)打包時(shí)間等信息;錯(cuò)誤追蹤,webpack打包后的文件與源文件不同,需要特殊的處理以方便錯(cuò)誤定位和調(diào)試。

webpack 在生產(chǎn)環(huán)境更關(guān)注打包體積,而開發(fā)環(huán)境更關(guān)注打包時(shí)間,生產(chǎn)環(huán)境和開發(fā)環(huán)境使用不同的策略,下文每一個(gè)優(yōu)化建議都列出使用環(huán)境

webpack及相關(guān)打包工具在不斷跟新優(yōu)化中,不同版本可能會(huì)有很大不同,請以官方文檔為準(zhǔn)。本文以webpack4.42為準(zhǔn),與webpack3和webpack5區(qū)別較大的地方會(huì)標(biāo)注版本變動(dòng)。

1. 量化打包速度

環(huán)境:開發(fā),生產(chǎn)

目標(biāo):分析,構(gòu)建速度,增量構(gòu)建速度

對webpack進(jìn)行優(yōu)化,首先要測量webpack打包耗時(shí),發(fā)現(xiàn)問題所在。

  1. 使用 speed-measure-webpack-plugin ,快速測量各插件和loader的耗時(shí)。
speed.png

speed-measure-webpack-plugin使用非常簡單,直接包裹webpack配置即可

    // npm i -D speed-measure-webpack-plugin

    const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
    const smp = new SpeedMeasurePlugin()

    const webpackConfig = smp.wrap({
      // ... webpack config 
    })

speed-measure-webpack-plugin對webpack 4的兼容性不好,一些插件可能會(huì)發(fā)生錯(cuò)誤,需暫停發(fā)生錯(cuò)誤的插件。

  1. 使用 profilingPlugin , 創(chuàng)建構(gòu)建性能報(bào)告。 profilingPlugin 可以生成一份JSON文件,上傳到chrome devtool的performence面板, 可以看到詳細(xì)的編譯過程,尋找性能瓶頸:

    const webpack = require('webpack')
    
    module.exports = {
       //...
         plugins: [
                new webpack.debug.ProfilingPlugin(
            {
                        // 產(chǎn)生出的文件位置,相對于根目錄
                        outputPath: 'stats/profileEvents.json' 
                    })
        ] 
    }
    
    

    上傳到chrome,出現(xiàn)漂亮的時(shí)間線,可供分析:

    perfomrance.png

webpack多進(jìn)程運(yùn)行時(shí),插件和loader的運(yùn)行時(shí)間較難觀察,比如各插件的運(yùn)行時(shí)間相加會(huì)大于總時(shí)間,loader的先后順序可能不對。使用單進(jìn)程可以保證有效的測試數(shù)據(jù)。測速時(shí)請將 parallelism 配置項(xiàng)設(shè)為 false (默認(rèn)為 true ),確保webpack單線程運(yùn)行:

module.exports = {
   //...
   parallelism: false
}

除調(diào)試webpack外,其他情況請開啟多進(jìn)程。

2. 量化打包體積

環(huán)境:開發(fā),生產(chǎn)

目標(biāo):分析,打包體積,加載體積

可以通過 webpack-bundle-analyzer 查看打包后的體積。探究某個(gè)依賴是否過大,是否存在重復(fù)的依賴,是否有功能類似的依賴,是否進(jìn)行有效的代碼分離。

// npm i -D webpack-bundle-analyzer

const BundleAnalyzerPlugin = 
    require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
   //...
     plugins: [
            new BundleAnalyzerPlugin({
            defaultSizes: 'gzip' // 設(shè)置默認(rèn)尺寸為'gzip'
        })
    )
    ] 
}

chunck.png

webpack-bundle-analyzer 顯示的尺寸有三種:

  • stat: 未經(jīng)Uglify、Terser 等插件最小化的體積。最小化js就是我們常說的壓縮js,最小化(minify)是指去除注釋、簡化變量名等操作壓縮體積,而壓縮是通過gzip等算法壓縮體積。
  • parsed: 經(jīng)Uglify、Terser 等插件最小化后的體積。
  • gzip: 經(jīng)過gzip壓縮后的尺寸。

線上一般按照 gzip 進(jìn)行數(shù)據(jù)傳輸,所以選擇 gzip 作為衡量體積的標(biāo)準(zhǔn)。

3. 查看打包詳情

環(huán)境:開發(fā),生產(chǎn)

目標(biāo):分析,構(gòu)建速度,增量構(gòu)建速度,打包體積,加載體積

除查看打包體積和時(shí)間,有時(shí)需要更多打包信息,比如模塊的依賴關(guān)系,可以用如下方式:

  1. 配置 profile 選項(xiàng),生成詳細(xì)打包報(bào)告。配置 profile 選項(xiàng)后,每次打包會(huì)生成一個(gè)json文件,里面有打包的詳細(xì)信息,也可通過 stats-webpack-plugin 進(jìn)行更多設(shè)置:

    // npm i -D stats-webpack-plugin
    
    const StatsPlugin = require('stats-webpack-plugin')
    
    module.exports = {
      //...
      profile: true // 簡單的配置,將在打包文件輸出報(bào)告
        pulgins: [    // 更靈活的配置
            new StatsPlugin(
            '../../stats/stats.json', {
            exclude: [/node_modules/], 
        })
        ] 
    }
    
    

    將產(chǎn)生的json文件上傳至https://webpack.github.io/analyse/ 官方分析工具,查看可讀的報(bào)告:

stat.png
  1. 打包后,在terminal終端中顯示打包詳情。可以通過 stats 選項(xiàng)配置terminal中的輸出結(jié)果,如:

    module.exports = {
      //...
      stats: 'verbose'  // 將在terminal中呈現(xiàn)全部輸出 
    };
    
    

    每次打包后,在終端會(huì)顯示:

budgest2.png
`stats` 的可選址值有:

*   `none`: 沒有輸出
*   `errors-only` :只顯示錯(cuò)誤信息,**使用此選項(xiàng),可提高webpack編譯速度**
*   `minimal`: 顯示錯(cuò)誤信息,或者新的輸出
*   `normal` : 標(biāo)準(zhǔn)輸出
*   `verbose` : 全部輸出,建議用 `profile` 選項(xiàng)輸出成文檔。

可以個(gè)性化配置輸出信息。使用Node.js API 時(shí)此配置無效,使用 `webpack-dev-server` 時(shí),請將此選項(xiàng)需放入 `devServer` 中。。

Jarvis、webpack-dashboard、webpack visualizer等非官方工具也可提供打包信息,幫你分析webpack編譯。

4. 使用最新版本

環(huán)境:開發(fā),生產(chǎn)

目標(biāo): 構(gòu)建速度,增量構(gòu)建速度,打包體積,加載體積

工欲善其事,必先利其器。使用最新的版本,保證最快的速度。請檢查node、 webpack、插件、 loader等的版本號(hào),使用最新版本。推薦一款工具 npm-check, 只要在項(xiàng)目根目錄運(yùn)行:

npm-check -u

即查看哪些包有更新:

update.png

5. 刪除不必要的插件、loader等工具

環(huán)境:開發(fā),生產(chǎn)

目標(biāo):構(gòu)建速度,增量構(gòu)建速度

任何插件和loader的啟用都占用時(shí)間,使用最少的插件和loader可以減少webpack的打包時(shí)間。

  • 一些工具,只對生產(chǎn)階段有效,比如壓縮、CSS分離、hash,請?jiān)陂_發(fā)環(huán)境刪除。
  • 一些工具,比如 progress-bar-webpack-plugin進(jìn)度提示條,請衡量對你是否真的有用,每增加一個(gè)插件都會(huì)讓打包更慢。
  • 對于speed-measure-webpack-plugin,webpack-bundle-analyzer 這類量化插件,只在調(diào)試webpack時(shí)使用,在其他時(shí)候請刪除。
  • 在webpack打包時(shí),會(huì)在terminal中輸出moudle、chunk等打包信息,生成這些信息會(huì)消耗一定的時(shí)間。建議設(shè)置為 stats: 'errors-only' 或者 stats: 'minimal' 。

6. 設(shè)置正確的模式mode

環(huán)境:開發(fā),生產(chǎn)

目標(biāo): 構(gòu)建速度,增量構(gòu)建速度,構(gòu)建體積,加載體積

版本變動(dòng):webpack3

webpack提供開箱即用的配置環(huán)境,通過 mode 屬性告知webpack你的使用環(huán)境,webpack將自動(dòng)開啟一系列的優(yōu)化。

  • 生產(chǎn)環(huán)境,請?jiān)O(shè)置 mode:'production'
  • 開發(fā)環(huán)境,請?jiān)O(shè)置 mode: 'development'
module.exports = {
  //...
  mode: 'production' // 生產(chǎn)環(huán)境,也是默認(rèn)值
  mode: 'development' // 開發(fā)環(huán)境
}

設(shè)置 mode 后,webpack自動(dòng)將 process.env.NODE_ENV 設(shè)為相應(yīng)的值(生產(chǎn)為 production , 開發(fā)為 develpment ),react等包根據(jù)此值進(jìn)行優(yōu)化,所以不要將process.env.NODE_ENV 改為其他值。

webpack3沒有 mode 選項(xiàng),需要用 DefinePlugin手動(dòng)設(shè)置process.env.NODE_ENV,明確生產(chǎn)或者開發(fā)環(huán)境 。

webpack自動(dòng)啟用的優(yōu)化,如無需配置,本文不再介紹。

7. 啟用多進(jìn)程,并行編譯

環(huán)境:開發(fā),生產(chǎn)

目標(biāo): 構(gòu)建速度增量構(gòu)建速度

webpack默認(rèn)是單進(jìn)程編譯,不利于發(fā)揮多核CPU的威力??梢圆扇⌒┐胧?,使用多進(jìn)程,這將大幅提高打包速度:

  • 對于 TerserWebpackPlugin 等插件,本身支持多進(jìn)程,默認(rèn)開啟,也可通過 parallel 選項(xiàng)進(jìn)行個(gè)性化配置。

  • 對于其他loader,可以使用 happyPack 或者 thread-loader 進(jìn)行并行編譯。這兩個(gè)功能類似,對打包速度的提升也類似。對于babel-loader 尤其需要并行編譯,提高打包速度。

    happyPack 使用的人較多,使用限制較少, 但其配置較復(fù)雜,例如:

    // npm i -D happypack
    
    const HappyPack = require('happypack')
    
    module.exports = {
        // ...
        module: {
            loaders: [{
                test: /\\.js$/,
                loader: 'babel-loader',
                happy: { id: 'js' }
            }]
        },
        plugins: [
            new HappyPack({ id: 'js' })
        ]
    }
    
    

    thread-loader 有些使用限制,但配置非常簡單:

    // npm i -D threa-loader
    
    module.exports = {
        // ...
      module: {
        rules: [
          {
              test: /\\.jsx?$/,
              use: ['thread-loader', 'babel-loader']
          }
        ]
      }
    }
    
    

    thread-loader 將其后的loader 放在單獨(dú)的池中運(yùn)行,要求loader不能產(chǎn)生新的文件、不能使用插件、無法獲取webpack選項(xiàng)。

開啟多進(jìn)程會(huì)消耗大量時(shí)間,各進(jìn)程之間的通信也非常耗時(shí)。請僅對長耗時(shí)的loader啟用。進(jìn)程數(shù)過多會(huì)導(dǎo)致打包變慢,進(jìn)程的默認(rèn)數(shù)通常是CPU數(shù)-1,即 os.cpus().length - 1 。

8. 使用持久緩存

環(huán)境:開發(fā),生產(chǎn)

目標(biāo):構(gòu)建速度,增量構(gòu)建速度

版本變動(dòng):webpack5

webpack構(gòu)建一次耗費(fèi)很長時(shí)間,如果將構(gòu)建的結(jié)果進(jìn)行緩存,第一次構(gòu)建的時(shí)間可能略有增加,但之后的構(gòu)建時(shí)間將大幅度縮短,這種技術(shù)叫持久緩存。

  • 建議使用 hardSourcePlugin 進(jìn)行全局緩存,開箱即用,無需配置,使用效果非常好,能減少50%以上的構(gòu)建時(shí)間。hardSourcePlugin 在使用中可能存在一些問題,可能導(dǎo)致CSS分離失效,如有問題請查閱官方文檔。使用hardSourcePlugin 后,不需再使用cache-loader 或者 DLL plugin 等緩存工具。hardSourcePlugin 配置示例:

    // npm i -D hard-source-webpack-plugin
    const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
    
    module.exports = {
        // ...
        plugins: [
            new HardSourceWebpackPlugin ()
        ]
    }
    
    
  • 對于 TerserWebpackPlugin ,請確保緩存開啟,即 cache 不為 false 。 hardSourcePlugin對TaresePlugin似乎沒有效果,需單獨(dú)處理。

  • 對于webpack5,請直接配置 cache: { type: "filesystem” } ,不再需要 hardSourcePlugin 等緩存工具,TarserPlugin 自帶的緩存特性也會(huì)失效,無需開啟。

另外一種緩存技術(shù)叫做”長期緩存" (long-term caching), 是指瀏覽器緩存一些資源,可以更快的打開頁面,需與“持久緩存”相區(qū)別,后文有更多介紹。

9. 選擇合適的devTool

環(huán)境:開發(fā),生產(chǎn)

目標(biāo):構(gòu)建速度、增量構(gòu)建速度

webpack打包后的代碼是經(jīng)過壓縮處理,缺少可讀性。devTool可以控制代碼的輸出品質(zhì),添加 source-map ,增加代碼可讀性,方便錯(cuò)誤定位。注意代碼輸出品質(zhì)的提高,會(huì)使打包速度變慢,需均衡選擇。

module.exports = {
  //...
  devTool: 'source-map' // 產(chǎn)生源碼品質(zhì)的source-map,有暴露源碼風(fēng)險(xiǎn),不建議使用
}

生產(chǎn)環(huán)境常用的值:

  • none :不使用devtool,速度最快,無法定位錯(cuò)誤,是生成環(huán)境默認(rèn)值
  • hidden-source-map :產(chǎn)生 source-map , 需要手動(dòng)加載到瀏覽器devtool,或使用其他工具加載,才可查看源碼和追蹤錯(cuò)誤,推薦使用。
  • nosources-source-map :無法看到源碼,但能在瀏覽器devtoo看到錯(cuò)誤棧,錯(cuò)誤發(fā)生的準(zhǔn)確位置、行號(hào)等信息。這是線上環(huán)境最簡單的方案,但暴露源碼結(jié)構(gòu)。

開發(fā)環(huán)境常用的值:

  • eval: 源碼被放入 eval 函數(shù)中,缺少可讀性,只知道錯(cuò)誤的大概位置,沒有錯(cuò)誤行號(hào)、列號(hào)提示,構(gòu)建速度最快,這是開發(fā)環(huán)境默認(rèn)值
  • eval-cheap-source-map : 看到近似源碼的代碼,有錯(cuò)誤行號(hào)提示,沒列號(hào)提示。較快的構(gòu)建速度和增量構(gòu)建速度。
  • eval-cheap-module-source-map : 能看到源碼,有行號(hào)錯(cuò)誤提示,沒列號(hào)提示。較慢的構(gòu)建速度和較快的增量構(gòu)建速度,推薦此選項(xiàng)。
  • eval-source-map : 能看到源碼,并有行號(hào)和列號(hào)的錯(cuò)誤提示。很慢的構(gòu)建速度和一般的增量構(gòu)建速度。

devTool 還有其他配置值,需均衡考慮構(gòu)建速度和錯(cuò)誤定位,并防止源碼在生產(chǎn)環(huán)境中暴露。你可以用 SourceMapDevToolPlugin 代替 devTool 進(jìn)行個(gè)性化配置。

如未能生成 source-map,檢查一些插件或者loader是否配置 sourceMap: true ,這包括postcss-loader、resolve-url-loader、TeserWebpackPlugin、OptimizeCSSAssetsPlugin 等。

10. 合理配置loader的解析范圍

環(huán)境:開發(fā),生產(chǎn)

目標(biāo):構(gòu)建速度,增量構(gòu)建速度

一些loader,比如 babel-loader 轉(zhuǎn)換JS的過程是非常耗時(shí)的,如果能減少loader處理的文件數(shù)量,會(huì)大大減少編譯時(shí)間。

可以通過 test 正則匹配文件, include 指定要包含的文件, exclude 指定要排除的文件。優(yōu)先級(jí)為: exclude > include > test ,例如:

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\\.styl$/,
        include: [
          path.resolve(__dirname, 'app/styles'),
          path.resolve(__dirname, 'vendor/styles')
        ],
                // 在app/styles 中排除掉 app/styles/common
        exclude: [path.resolve(__dirname, 'app/styles/commen')],
                use: ['style-loader', 'css-loader', 'stylus-loader']
      }
    ]
  }
}

順便提下loader的執(zhí)行順序,列表中較后的選項(xiàng)先執(zhí)行,比如在上例中,先執(zhí)行 stylus-loader

解析.style,再執(zhí)行 css-loader解析CSS,最后執(zhí)行 style-loader 將CSS內(nèi)聯(lián)處理。

11. 忽略jquery等模塊的解析

環(huán)境:開發(fā),生產(chǎn)

目標(biāo):構(gòu)建速度、增量構(gòu)建速度

jquery, lodash等是獨(dú)立的、不需要外部依賴即可運(yùn)行的模塊??梢允褂?module.noParse 避免對這些庫進(jìn)行解析,以提高構(gòu)建性能。配置方法如下:

module.exports = {
  //...
  module: {
    noParse: /jquery|lodash/,
  }
};

module.noParse 配置后,將忽略相關(guān)模塊中 import 、 reuqiredefine 的調(diào)用,請確保配置的模塊沒有其他依賴。

module.noParse 只是不對jquery等模塊進(jìn)行解析(如通過babel-loader轉(zhuǎn)換js文件),打包后的文件中仍包含jquery的代碼。 module.noParse 的優(yōu)先級(jí)高于上文中對loader的includetest配置。

12. 使用 externals 引入公共CDN

環(huán)境:開發(fā),生產(chǎn)

目標(biāo):構(gòu)建速度、增量構(gòu)建速度

可以利用公共CDN服務(wù)加載包,而不用webpack打包。如下配置,將 jquery 變量掛載在 windows 變量上:

module.exports = {
  //...
  externals: {
    'jquery': 'jQuery'
  }
};

當(dāng)然,要在HTML代碼中引入jquery:

<script src="<http://libs.baidu.com/jquery/2.0.0/jquery.min.js>"></script>

webpack打包后的文件中將不包含jquery的代碼。

如果你的多個(gè)應(yīng)用使用相同的依賴,可以將公共依賴分離,再用此種方式單獨(dú)部署使用。

13. 合理配置 reslove 字段,縮小webpack尋找模塊的范圍

環(huán)境: 開發(fā)、生產(chǎn)

目標(biāo):構(gòu)建速度、增量構(gòu)建速度

resolve 配置webpack如何查找模塊,例如 import 'vue'resolve 告訴webpack在哪里找到vue。更精準(zhǔn)的配置將減小webpack搜索模塊的時(shí)間。

module.exports = {
  //...
  resolve: {
    modules: ['node_modules'],   // 僅在node_modules目錄尋找模塊
    extensions: ['.js', '.json'],   // import 'utils' 類似于 import 'utils.js' 
    mainFields: ['loader', 'main'], // 通過package.json 的loader、main 選項(xiàng)找到入口文件名
        mainFiles: ['index'], // 設(shè)index為入口文件名
        symlinks: false,      // 忽略npm link
    alias: { 
            // import 'Pages' 將等同于 import 'src/pages'
            Pages: 'src/pages',  
            // webpack直接確定react模塊位置,不需要層層查找 
      react: patch.resolve(__dirname, './node_modules/react/dist/react.min.js') 

        }
    }
};

  • resolve.modules :告訴webpack搜索模塊的范圍。
  • resolve.mainFieldsresolve.mainFiles :告訴webpack模塊的入口文件。
  • resolve.extensions :如果引入文件時(shí)不指定文件類型后綴,將通過此字段查找相應(yīng)的文件類型。
  • resolve.symlinks :如果不使用 NPM link ,可將此設(shè)為 false 。
  • resolve.alias :設(shè)置別名,這將提高代碼的可讀性,并加快模塊的尋找速度。

如果選項(xiàng)是列表的,請盡量減少列表項(xiàng),并將高頻值寫在前面, 如 extensions: ['.js', '.json']js 在前。

未掌握 reslove 各選項(xiàng)含義前,請使用默認(rèn)值, reslove 對webpack編譯的提速不明顯,但錯(cuò)誤的配置可能導(dǎo)致一些bug,比如明明存在某個(gè)模塊但webpack無法找到該模塊。

14. 使用ES module,保證tree-shaking刪除無用代碼

環(huán)境:開發(fā),生產(chǎn)

目標(biāo):構(gòu)建體積

版本變動(dòng):webpack3

webpack4支持tree-shaking,可以刪除未使用的代碼,請使用ES module的導(dǎo)入方式,確保tree-shaking的有效,例如:

// common.js
const run = () => {console.log('run')}
const fly = () => {console.log('fly')}

export {run, fly}

// index.js
import {run, fly} from './common'
run()

如此,fly將會(huì)從代碼中刪除。按下述導(dǎo)入方式,tree-shaking不會(huì)生效:

// common.js 同上

// index.js
import common from './common'
common.run()

// common 依賴將被整個(gè)打包,無法實(shí)現(xiàn)tree-shaking

由于JS動(dòng)態(tài)編譯的特性,tree-shaking的效果有限。對于很多依賴,webpack無法很好的執(zhí)行tree-shaking。另外webpack3不支持tree-shaking。

在開發(fā)其他項(xiàng)目的依賴包時(shí),可考慮在 package.js 中添加 sideEffects:false,其他項(xiàng)目在導(dǎo)入你的包時(shí),會(huì)嘗試進(jìn)行tree-shaking。

15. 使用TerserWebpackPlugin 最小化JS

環(huán)境:生產(chǎn)

目標(biāo):構(gòu)建速度,打包體積

TerserWebpackPlugin 可以最小化JS,即壓縮js,并且支持最小化ES6,webpack4生產(chǎn)模式下自動(dòng)啟用,你也可以進(jìn)行一些配置:

  • 請確保 parallel 多進(jìn)程開啟,提高打包速度。
  • 像loader一樣,通過 test ,include ,exclude 縮小壓縮文件范圍。也可以用 chunkFilter 排除一些chunk。
  • 非webpack5版本,請確保 cache 緩存開啟,webpack5中此選項(xiàng)失效。
  • 如果需要source-map,請配置 sourceMaptrue ,這將生成source-map。注意cheap-source-map 等包含cheap 的source-map 不會(huì)產(chǎn)生source-map。請使用hidden-source-map 或者nosources-source-map

配置示例如下:

const TerserPlugin = require('terser-webpack-plugin')

module.exports = {
  optimization: {
        minimize: true,
    minimizer: [
      new TerserPlugin({
        exclude: /\\/excludes/,
                chunkFilter: (chunk) => {
                    return chunk.name !== 'vendor' // 排除名為 `vendor` 的chunk
        },
                parallel: true, // 多進(jìn)程,默認(rèn)開啟
                cache: true, // 持久緩存,默認(rèn)開啟
                sourceMap: true, // 支持產(chǎn)生source-map,默認(rèn)不開啟
                terserOptions: {
                    //...  設(shè)置terser的壓縮選項(xiàng),比如是否清除注釋,詳見官方文檔
                }
      }),
    ],
  },
};

不要使用 UglifyjsWebpackPlugin,此插件不支持ES6,并已經(jīng)停止維護(hù)。

16. 使用 optimize-css-assets-webpack-plugin 最小化CSS

環(huán)境:生產(chǎn)

目標(biāo):打包體積

使用 optimize-css-assets-webpack-plugin 最小化CSS,減少CSS代碼的體積。webpack4 中不包含此插件,需先安裝。

// npm i -D optimize-css-assets-webpack-plugin

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
  optimization: {
    minimizer: [
      new OptimizeCSSAssetsPlugin({})
    ],
  },
};

17. 優(yōu)化圖片

環(huán)境:生產(chǎn)

目標(biāo): 打包體積

圖片是網(wǎng)站重要的一部分,不僅體積大,而且數(shù)量多,占用大量帶寬,下面是優(yōu)化圖片的一些方法:

  • 使用 url-loader 將小尺寸圖片進(jìn)行內(nèi)聯(lián)??梢耘渲?limit 選項(xiàng),將小于此值的圖片轉(zhuǎn)換成base64 格式并存在JS代碼中,以節(jié)少請求數(shù)量。 file-loader 有和 url-loader 類似的功能,可以解析靜態(tài)文件,但不能設(shè)置 limit 屬性進(jìn)行內(nèi)聯(lián)。
  • 使用 svg-loader 將svg圖片進(jìn)行內(nèi)聯(lián)。此插件和 url-loader 類似,但SVG是文本,壓縮體積上更有效率。
  • 雪碧圖可將多張小圖片組裝合成一張大圖片,減少請求數(shù)量。由于雪碧圖配置較復(fù)雜,不推薦使用??梢岳?webpack-spritesmith 插件實(shí)現(xiàn)雪碧圖。
  • 使用 image-webpack-loader 壓縮圖片。它支持JPG,PNG,GIF和SVG等幾乎所有格式。 它不會(huì)將圖片內(nèi)聯(lián),需與url-loader等配合使用,即先壓縮圖片,再進(jìn)行內(nèi)聯(lián)??梢酝ㄟ^ enforce: 'pre' 確保在其他loader前執(zhí)行。

配置示例如下:

// npm i -D image-webpack-loader
// npm i -D url-loader
// npm i -D svg-url-loader

module.exports = {
  module: {
    rules: [
            // 壓縮圖片
      {
                test: /\\.(jpe?g|png|gif|svg)$/,
                loader: 'image-webpack-loader',
                enforce: 'pre', // 在其他loader調(diào)用前,先行調(diào)用,避免重復(fù)代碼
            },
            // 內(nèi)聯(lián)圖片
      {
                test: /\\.(jpe?g|png|gif)$/,
        loader: 'url-loader',
                options: {
          limit: 10 * 1024, // 小于10kb的會(huì)被內(nèi)聯(lián)
        },
      }
            // 內(nèi)聯(lián)svg
            {
        test: /\\.svg$/,
        loader: 'svg-url-loader',
        options: {
          limit: 10 * 1024, // 小于10kb的會(huì)被內(nèi)聯(lián)
          noquotes: true,  // 刪除引號(hào)
                    iesafe: true, // 支持IE,但會(huì)增加體積
        },
      },
    ],
  },
};

18. 優(yōu)化第三方依賴包

環(huán)境: 開發(fā),生產(chǎn)

目標(biāo): 構(gòu)建速度、增量構(gòu)建速度、打包體積

幾乎一半以上的JS體積來源于第三方依賴包,如果能優(yōu)化依賴包體積,會(huì)大幅減少構(gòu)建體積。我們通常只用到包的幾個(gè)方法,但卻引入包的全部內(nèi)容。比如,我們只在中文環(huán)境使用Moment.js,卻引入了Moment.js的各國語言包,使得項(xiàng)目臃腫。

Googel 在Github repo上收集了一些優(yōu)化建議,其中包括babel-loard、loadsh、react、bootstrap等的優(yōu)化建議。請?jiān)趃ithub上搜索“webpack-libs-optimizations”查看。

19. 對html-webpack-plugin的優(yōu)化

環(huán)境:生產(chǎn)

目標(biāo):打包體積

hteml-webpack-plguin 可以很方便的創(chuàng)建入口html文件,但它對自定義模板默認(rèn)不開啟壓縮的,請配置 minify 選項(xiàng),進(jìn)行最小化代碼

// npm i -D html-webpack-plugin
// npm i -D uglifyjs-webpack-plugin  壓縮js

const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')

module.exports = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
        // 使用'ejs'后綴防止模板文件被html—loader 解析
        template: path.join(__dirname, 'index.ejs'),
        filename: 'index.html',
        minify: { // 開啟最小化操作
            collapseWhitespace: true,
            removeComments: true,
            removeRedundantAttributes: true,
            removeScriptTypeAttributes: true,
            removeStyleLinkTypeAttributes: true,
            useShortDoctype: true,
            minifyCSS: true,
            minifyJS: true, // 使用uglifyjs進(jìn)行壓縮,所以需要安裝相關(guān)依賴
        }
    })
  ]
}

20. 優(yōu)化Node核心庫的polyfill

環(huán)境:生產(chǎn)

目標(biāo):打包體積,加載體積

版本變動(dòng):webpack5

webpack打包時(shí)會(huì)自動(dòng)加入Node環(huán)境的polyfill,所以在瀏覽器中也可使用Node核心庫,比如 process.env.node_env ,這會(huì)略微增加打包的體積。如果不需要瀏覽器使用Node的核心庫,請配置:

module.exports = {
  // ...
    node: false
}

你也可以對每一個(gè)核心庫進(jìn)行精確配置,以下是webpack4的默認(rèn)配置值(webpack5有差別):

module.exports = {
  // ...
    node: {
      console: false,
      global: true,
      process: true,
      __filename: "mock",
      __dirname: "mock",
      Buffer: true,
      setImmediate: true
    }
}

其中的 console ,globalprocess 等為node核心庫中的相應(yīng)功能,更多選項(xiàng)需查找 node 核心庫。配置的值的含義如下:

  • true :提供polyfill。
  • mock :提供 mock 實(shí)現(xiàn)預(yù)期接口,但功能很少或沒有。
  • empty :提供一個(gè)空對象。
  • false :不提供polyfill,如果使用相應(yīng)的node庫,會(huì)觸發(fā)錯(cuò)誤。

21. 分離代碼

環(huán)境:生產(chǎn)

目標(biāo):打包體積,加載體積

版本變動(dòng):webpack3,webpack5

webpack將各種資源打包成一個(gè)JS文件,合并請求,優(yōu)化用戶體驗(yàn)?,F(xiàn)在的web應(yīng)用體積越來越大,單一的js文件體積過大,需拆分成多個(gè)js文件,減少加載體積。這也是懶加載、長久緩存等技術(shù)使用的基礎(chǔ)。分離出的每一個(gè)js文件,稱之為chunk。

webpack3使用 CommonsCunksPlugn 配置復(fù)雜,webpack4提供開箱即用的 optimization.splitChunks 。生產(chǎn)環(huán)境自動(dòng)啟用 optimization.splitChunks ,默認(rèn)配置足以應(yīng)對大多數(shù)情況,不需要專門配置。默認(rèn)配置遵照的主要規(guī)則:

  • 主入口會(huì)打包成單獨(dú)的chunk。
  • 動(dòng)態(tài)導(dǎo)入的每一個(gè)模塊,會(huì)打包成一個(gè)chunk。例如按路由分離3個(gè)頁面,將會(huì)打包出3個(gè)chunk。
  • 每個(gè)chunk中用到的node_modules依賴,會(huì)被打單獨(dú)打包成以 vendors~ 開頭的chunk
  • 如果兩個(gè)chunk中包含相同的內(nèi)容,相同的內(nèi)容會(huì)被單獨(dú)打包成一個(gè)共享chunk。

比如有個(gè)項(xiàng)目:

  • index 入口頁,用到react,index組件
  • about 介紹頁,用到react,about組件,common組件
  • detail 詳情頁,用到angular,detail組件,common組件

將打包成:

  • index.js: react, index組件
  • about~detail.js: common組件
  • about.js: about組件
  • detail.js: detail組件
  • vendors~detail.js: angular

如修改默認(rèn)規(guī)則,請?jiān)敿?xì)閱讀官方文檔,并注意評(píng)估打包結(jié)果,防止負(fù)優(yōu)化,例如:

module.exports = {
  //...
  optimization: {
    splitChunks: {
            //... 一些配置
        }
  }
}

webpack5對optimization.splitChunks 進(jìn)行一些優(yōu)化,配置略有不同。

22. 分離CSS

環(huán)境:生產(chǎn)

目標(biāo):打包體積,加載體積

版本變動(dòng):webpack3

在分離JS的基礎(chǔ)上,有時(shí)需分離CSS,以實(shí)現(xiàn)CSS的按需加載。在webpack3 時(shí)使用 ExtractTextWebpackPlugin , 在webpack4 時(shí)請使用 mini-css-extract-plugin :

// npm i -D mini-css-extract-plugin

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  plugins: [
        new MiniCssExtractPlugin({
            filename: '[hash].css',
            chunkFilename: '[contenthash].css' // 注意使用contenthash
        })
    ],
  module: {
    rules: [
      {
        test: /\\.css$/i,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              esModule: true,
                            hmr: process.env.NODE_ENV === 'development' // 熱響應(yīng)
            },
          },
          'css-loader',
        ],
      },
    ],
  },
};

CC將分離成一個(gè)個(gè)chunk。在開發(fā)階段可以考慮不分離CSS,以提高打包速度。

23. 使用按需加載,懶加載,預(yù)加載

環(huán)境:生產(chǎn)

目標(biāo):加載體積

版本變動(dòng):webpack5

現(xiàn)在網(wǎng)頁應(yīng)用功能越來越多,尺寸越來越大,我們可以只加載用戶用到的部分,而不加載其他部分以提高性能。比如只加載首屏需要的chunk,而其他部分的chunk暫時(shí)不加載。

我們可以使用上文所說的 split-chunk 分離代碼,并通過動(dòng)態(tài)導(dǎo)入實(shí)現(xiàn)按需加載/懶加載。Vue,React等都提供動(dòng)態(tài)導(dǎo)入組件的方案,也可以用 function import() 實(shí)現(xiàn)懶加載,比如:

import(/* webpackChunkName: "lodash" */ 'lodash')

webpack5使用更加智能的chunk命名方式,不需要諸如 /* webpackChunkName: "lodash" */ 的代碼。

對于分離的代碼,我們也可在空閑時(shí)間,將其預(yù)先加載,以提高用戶體驗(yàn),比如:

import(/* webpackPrefetch: true */ 'lodash')

24. 使用長久緩存

環(huán)境:生產(chǎn)

目標(biāo):加載體積

版本變動(dòng):webpack3,webpack5

瀏覽器打開應(yīng)用時(shí)會(huì)下載資源,這會(huì)占用大量的時(shí)間。如果將一部分資源在瀏覽器中緩存,下次打開頁面時(shí)不需要重下載,可以降低首屏渲染時(shí)間。

我們在修改項(xiàng)目時(shí),通常只修改一部份,讓瀏覽器僅更新修改部分即可,其余資源使用緩存。webpack打包后,會(huì)給每一個(gè)靜態(tài)資源添加hash后綴,如果修改該資源,hash會(huì)發(fā)生改變,瀏覽器會(huì)重新加載該資源。如果該資源沒被修改,hash會(huì)保持不變,瀏覽器會(huì)使用緩存,這種技術(shù)叫做“長久緩存”(long cache)。需與前文提到的"持續(xù)緩存”相區(qū)別,持續(xù)緩存是用于提高打包速度的。

webpack5對長久緩存進(jìn)行大量優(yōu)化,配置更為方便。下面的步驟限于webpack4版本,webpack3也需要類似復(fù)雜的配置,但略有不同。

長久緩存依靠 splitChunk 代碼分離,請參考上文。

  • 第一步,告知瀏覽器緩存文件

    需要在服務(wù)器上設(shè)置緩存文件時(shí)長,比如通過Cache-Control

    // Server header
    Cache-Control: max-age=31536000      // 設(shè)置緩存一年時(shí)間
    
    

    有多種設(shè)置緩存的方法,請查看相關(guān)文章

  • 第二步,使用 recordsPath 配置項(xiàng),方便調(diào)試

    在設(shè)置長久緩存時(shí),我們需要知道webpack打包后的文件是如何劃分的,模塊id是否改變,recordsPath 配置項(xiàng)可以產(chǎn)生一份json文件,記錄每次打包后的結(jié)果。

    const path = require('path')
    
    module.exports = {
      // ...
        recordsPath: path.join(
            process.cwd(),  // npm run build 在terminal中執(zhí)行時(shí)的目錄路徑
            'records.json'  // 輸出的文件名
        )
    }
    
    
  • 第三步,為文件添加hash

    需要為打包后的文件設(shè)置hash后綴,相當(dāng)于給該文件添加一個(gè)版本號(hào),告知瀏覽器該文件是否更新,比如設(shè)置 [name].[hash].js。hash值有三種:

    • [hash]: webpack每次編譯都會(huì)產(chǎn)生一個(gè)唯一的hash值,每次編譯hash都會(huì)改變
    • [chunkhash]: webpack每次編譯,對每一個(gè)chunk(每一個(gè)js文件)生成不同的hash值,如果修改chunk,會(huì)重新計(jì)算chunkhash。如果沒修改chunk,chunkhash不會(huì)變。對JS資源我們通常添加[chunkhash] 后綴。
    • [contenthash]:webpack根據(jù)輸出的文件內(nèi)容,計(jì)算并生成contenthash,每次編譯后都會(huì)根據(jù)內(nèi)容生成contenthash,如果改文件內(nèi)容不變,生成的contenthash是相同的。通常對圖片、字體、樣式等資源添加[contenthash] 后綴。
    module.exports = {
        // ...
        output: {
            chunkFilename: '[chunkhash].js' // 為chunk添加chunkhash
        },
        module: {
            rules: [
                {
                    test: /\\.ttf/,
                    loader: 'file-loaser',
                    options: { name: '[contenthash].ext'} //為字體添加contenthash 
                }
    
            ]
        }
    }
    
    

    由于不同的系統(tǒng)、計(jì)算機(jī)使用的hash計(jì)算方式不同,在不同的計(jì)算機(jī)對相同的項(xiàng)目打包,產(chǎn)生的hash值可能不同,導(dǎo)致長久緩存失效。

  • 第四步,分離runtime

    runtime記錄所有的資源文件名和路徑,以幫助瀏覽器找到最新版本的chunk,比如:

    {
        "0":"js/0.840dc3db.js",
        "common":"js/common.50055e90.js",
        "detail":"js/detail.dd333b62.js"
        //...
    }
    
    

    通常每個(gè)chunk中都包含runtime信息,只要有一個(gè)chunk發(fā)生變化,runtime就會(huì)變化,進(jìn)而使所有chunk發(fā)生變化??梢酝ㄟ^ optimization.runtimeChunk 將runtime分離為一個(gè)單獨(dú)的chunk,我們將將這個(gè)chunk稱為 runtime.js,有時(shí)也稱為manifest.js (比如vue-cli中)。

    module.exports = {
      // ...
        optimization: {
            runtimeChunk: {
            name: 'runtime'   // 分離runtime chunk,并命名為 'runtime'
        },
        }
    }
    
    
  • 第五步,內(nèi)聯(lián)runtime文件

    分離的runtime文件,將作為一個(gè)獨(dú)立的js加載,瀏覽器的請求順序變?yōu)椋?/p>

    index.html → runtime.js → 首屏chunk。

    可以看出多了次請求,使用 inline-manifest-webpack-plugin 將runtime內(nèi)聯(lián)進(jìn)html文件,瀏覽器的請求順序修改為:

    index.html (包含runtime的代碼) → 首屏chunk

    也可以使用webpack-manifest-plugin 實(shí)現(xiàn)同樣功能, inline-manifest-webpack-plugin 的配置示例:

    // npm i -D inline-manifest-webpack-plugin
    
    const InlineManifestWebpackPlugin 
        = require('inline-manifest-webpack-plugin')
    
    module.exports = {
      // ...
      plugins: [
        new InlineManifestWebpackPlugin(),
      ],
    };
    
    
  • 第六步,穩(wěn)定moudle id

    webpack會(huì)給每一個(gè)moudle分配一個(gè)id,id是按照順序計(jì)算的,如 0,1,2。如果你刪除了某個(gè)moudle,或者新增某個(gè)moudle,可能導(dǎo)致module id的順序發(fā)生變化,比如:

    moudleId   變動(dòng)情況
    0          原來的0
    1          新增的module
    2          原來的1, id變化
    3          原來的2,id變化
    
    

    moudle id的變化會(huì)導(dǎo)致chunk文件發(fā)生變化,我們可以用 hashed-module-ids-plugin 或者用 optimization.moduleIds 來穩(wěn)定moudle id:

    const webpack = require('webpack')
    
    module.exports = {
      // ...
      plugins: [
        new webpack.HashedModuleIdsPlugin(),
      ]
    }
    
    

經(jīng)過復(fù)雜的配置,終于可以實(shí)現(xiàn)長久緩存。

25. 使用性能預(yù)算,控制體積

環(huán)境:生產(chǎn)

目標(biāo):打包體積

項(xiàng)目初期的體積較小,但隨項(xiàng)目發(fā)展,可能沒注意到,安裝的某個(gè)依賴,使項(xiàng)目打包體積急劇增加。使用性能預(yù)算,及時(shí)發(fā)現(xiàn)打包體積的增加。

配置 performance 即可啟用性能預(yù)算,生成環(huán)境自動(dòng)開啟,默認(rèn)配置如下:

module.exports = {
    // ...
    performance: {
          hints: 'warning', // 尺寸過大后警告, 
                            // 設(shè)為'errors'時(shí)體積過大將報(bào)錯(cuò)
                            // 設(shè)為false時(shí)關(guān)閉性能預(yù)算
          maxEntrypointSize: 250000, // 最大入口體積
          maxAssetSize:250000 // 單個(gè)資源最大體積
    }
}

打包后,如果有體積過大,將顯示:

budgest.png

使用 bundlesize 工具,可以進(jìn)行更靈活的配置,比如單獨(dú)設(shè)置某個(gè)文件的體積預(yù)算,同時(shí)支持自動(dòng)化CI。

26. 其他優(yōu)化建議

  • 并行運(yùn)行多個(gè)webpack實(shí)例 。打包多個(gè)版本,比如打包國內(nèi)版本應(yīng)用、國際版本應(yīng)用,可以使用 parallel-webpack 并行運(yùn)行多個(gè)webpack實(shí)例。
  • 指定browserslist。指定合適的browserslist,babel將據(jù)此選擇核合適的降級(jí)工具,有助于代碼體積的減少和打包速度的提升。比如開發(fā)環(huán)境需要更快的打包速度,可以設(shè)為 'last 2 Chrome versions';生產(chǎn)環(huán)境需考慮兼容性,可設(shè)為 '> 1%, ie >= 11, not dead' 。
  • 考慮為不同的瀏覽器打包不同版本,可以為IE、現(xiàn)代瀏覽器分別打包,再根據(jù)瀏覽器的標(biāo)識(shí)加載不同的版本。也可以根據(jù)瀏覽器的尺寸將圖片打包成不同的尺寸,對PC端瀏覽器返回大圖,而對移動(dòng)端瀏覽器返回較小尺寸的圖片。
  • 如果擔(dān)心打包后的文件包含多個(gè)相同模塊,可以用 duplicate-package-checker-webpack-plugin 或者 bundle-duplicates-plugin 進(jìn)行檢查。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容