webpack構建優化清單

最近給項目進行webpack優化,嘗試過幾乎所有方法,一共26條,列舉在此。

優化webpack,首先明確優化目標:

  1. 構建速度: 開發環境項目的啟動速度,以及生產環境項目的打包速度。構建時間越短,速度越快越好。
  2. 增量構建速度:開發項目時,每修改一次代碼,webpack會對修改的部分進重新構建。增量構建時間越短,速度越快越好。
  3. 打包體積:開發環境webpack打包后,生成的文件體積。打包體積越小越好
  4. 加載體積: 除關注打包體積外,瀏覽器打開頁面,加載資源的體積也很重要。按需加載、緩存等技術可以減少加載體積。加載體積越小越好。

減少依賴包,縮小打包體積,構建速度會更快。使用壓縮,打包體積會減小,但構建速度則變慢。討論webpack的優化,速度體積需同時考慮,二者有時正相關,有時負相關,需要均衡速度和體積。

下文中每個優化建議,都會列出優化目標,影響明顯的會加粗。除速度和體積,還有: 分析, 即幫助分析webpack打包性能,呈現打包時間等信息;錯誤追蹤,webpack打包后的文件與源文件不同,需要特殊的處理以方便錯誤定位和調試。

webpack 在生產環境更關注打包體積,而開發環境更關注打包時間,生產環境和開發環境使用不同的策略,下文每一個優化建議都列出使用環境

webpack及相關打包工具在不斷跟新優化中,不同版本可能會有很大不同,請以官方文檔為準。本文以webpack4.42為準,與webpack3和webpack5區別較大的地方會標注版本變動

1. 量化打包速度

環境:開發,生產

目標:分析,構建速度,增量構建速度

對webpack進行優化,首先要測量webpack打包耗時,發現問題所在。

  1. 使用 speed-measure-webpack-plugin ,快速測量各插件和loader的耗時。
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的兼容性不好,一些插件可能會發生錯誤,需暫停發生錯誤的插件。

  1. 使用 profilingPlugin , 創建構建性能報告。 profilingPlugin 可以生成一份JSON文件,上傳到chrome devtool的performence面板, 可以看到詳細的編譯過程,尋找性能瓶頸:

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

    上傳到chrome,出現漂亮的時間線,可供分析:

    perfomrance.png

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

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

除調試webpack外,其他情況請開啟多進程。

2. 量化打包體積

環境:開發,生產

目標:分析,打包體積,加載體積

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

// npm i -D webpack-bundle-analyzer

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

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

chunck.png

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

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

線上一般按照 gzip 進行數據傳輸,所以選擇 gzip 作為衡量體積的標準。

3. 查看打包詳情

環境:開發,生產

目標:分析,構建速度,增量構建速度,打包體積,加載體積

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

  1. 配置 profile 選項,生成詳細打包報告。配置 profile 選項后,每次打包會生成一個json文件,里面有打包的詳細信息,也可通過 stats-webpack-plugin 進行更多設置:

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

    將產生的json文件上傳至https://webpack.github.io/analyse/ 官方分析工具,查看可讀的報告:

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

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

    每次打包后,在終端會顯示:

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

*   `none`: 沒有輸出
*   `errors-only` :只顯示錯誤信息,**使用此選項,可提高webpack編譯速度**
*   `minimal`: 顯示錯誤信息,或者新的輸出
*   `normal` : 標準輸出
*   `verbose` : 全部輸出,建議用 `profile` 選項輸出成文檔。

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

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

4. 使用最新版本

環境:開發,生產

目標: 構建速度,增量構建速度,打包體積,加載體積

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

npm-check -u

即查看哪些包有更新:

update.png

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

環境:開發,生產

目標:構建速度,增量構建速度

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

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

6. 設置正確的模式mode

環境:開發,生產

目標: 構建速度,增量構建速度,構建體積,加載體積

版本變動:webpack3

webpack提供開箱即用的配置環境,通過 mode 屬性告知webpack你的使用環境,webpack將自動開啟一系列的優化。

  • 生產環境,請設置 mode:'production'
  • 開發環境,請設置 mode: 'development'
module.exports = {
  //...
  mode: 'production' // 生產環境,也是默認值
  mode: 'development' // 開發環境
}

設置 mode 后,webpack自動將 process.env.NODE_ENV 設為相應的值(生產為 production , 開發為 develpment ),react等包根據此值進行優化,所以不要將process.env.NODE_ENV 改為其他值。

webpack3沒有 mode 選項,需要用 DefinePlugin手動設置process.env.NODE_ENV,明確生產或者開發環境 。

webpack自動啟用的優化,如無需配置,本文不再介紹。

7. 啟用多進程,并行編譯

環境:開發,生產

目標: 構建速度增量構建速度

webpack默認是單進程編譯,不利于發揮多核CPU的威力。可以采取些措施,使用多進程,這將大幅提高打包速度:

  • 對于 TerserWebpackPlugin 等插件,本身支持多進程,默認開啟,也可通過 parallel 選項進行個性化配置。

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

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

    // 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 放在單獨的池中運行,要求loader不能產生新的文件、不能使用插件、無法獲取webpack選項。

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

8. 使用持久緩存

環境:開發,生產

目標:構建速度,增量構建速度

版本變動:webpack5

webpack構建一次耗費很長時間,如果將構建的結果進行緩存,第一次構建的時間可能略有增加,但之后的構建時間將大幅度縮短,這種技術叫持久緩存。

  • 建議使用 hardSourcePlugin 進行全局緩存,開箱即用,無需配置,使用效果非常好,能減少50%以上的構建時間。hardSourcePlugin 在使用中可能存在一些問題,可能導致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 不為 falsehardSourcePlugin對TaresePlugin似乎沒有效果,需單獨處理。

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

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

9. 選擇合適的devTool

環境:開發,生產

目標:構建速度、增量構建速度

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

module.exports = {
  //...
  devTool: 'source-map' // 產生源碼品質的source-map,有暴露源碼風險,不建議使用
}

生產環境常用的值:

  • none :不使用devtool,速度最快,無法定位錯誤,是生成環境默認值
  • hidden-source-map :產生 source-map , 需要手動加載到瀏覽器devtool,或使用其他工具加載,才可查看源碼和追蹤錯誤,推薦使用
  • nosources-source-map :無法看到源碼,但能在瀏覽器devtoo看到錯誤棧,錯誤發生的準確位置、行號等信息。這是線上環境最簡單的方案,但暴露源碼結構。

開發環境常用的值:

  • eval: 源碼被放入 eval 函數中,缺少可讀性,只知道錯誤的大概位置,沒有錯誤行號、列號提示,構建速度最快,這是開發環境默認值
  • eval-cheap-source-map : 看到近似源碼的代碼,有錯誤行號提示,沒列號提示。較快的構建速度和增量構建速度。
  • eval-cheap-module-source-map : 能看到源碼,有行號錯誤提示,沒列號提示。較慢的構建速度和較快的增量構建速度,推薦此選項
  • eval-source-map : 能看到源碼,并有行號和列號的錯誤提示。很慢的構建速度和一般的增量構建速度。

devTool 還有其他配置值,需均衡考慮構建速度和錯誤定位,并防止源碼在生產環境中暴露。你可以用 SourceMapDevToolPlugin 代替 devTool 進行個性化配置。

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

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

環境:開發,生產

目標:構建速度,增量構建速度

一些loader,比如 babel-loader 轉換JS的過程是非常耗時的,如果能減少loader處理的文件數量,會大大減少編譯時間。

可以通過 test 正則匹配文件, include 指定要包含的文件, exclude 指定要排除的文件。優先級為: 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的執行順序,列表中較后的選項先執行,比如在上例中,先執行 stylus-loader

解析.style,再執行 css-loader解析CSS,最后執行 style-loader 將CSS內聯處理。

11. 忽略jquery等模塊的解析

環境:開發,生產

目標:構建速度、增量構建速度

jquery, lodash等是獨立的、不需要外部依賴即可運行的模塊。可以使用 module.noParse 避免對這些庫進行解析,以提高構建性能。配置方法如下:

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

module.noParse 配置后,將忽略相關模塊中 importreuqiredefine 的調用,請確保配置的模塊沒有其他依賴。

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

12. 使用 externals 引入公共CDN

環境:開發,生產

目標:構建速度、增量構建速度

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

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

當然,要在HTML代碼中引入jquery:

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

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

如果你的多個應用使用相同的依賴,可以將公共依賴分離,再用此種方式單獨部署使用。

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

環境: 開發、生產

目標:構建速度、增量構建速度

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

module.exports = {
  //...
  resolve: {
    modules: ['node_modules'],   // 僅在node_modules目錄尋找模塊
    extensions: ['.js', '.json'],   // import 'utils' 類似于 import 'utils.js' 
    mainFields: ['loader', 'main'], // 通過package.json 的loader、main 選項找到入口文件名
        mainFiles: ['index'], // 設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 :如果引入文件時不指定文件類型后綴,將通過此字段查找相應的文件類型。
  • resolve.symlinks :如果不使用 NPM link ,可將此設為 false
  • resolve.alias :設置別名,這將提高代碼的可讀性,并加快模塊的尋找速度。

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

未掌握 reslove 各選項含義前,請使用默認值, reslove 對webpack編譯的提速不明顯,但錯誤的配置可能導致一些bug,比如明明存在某個模塊但webpack無法找到該模塊。

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

環境:開發,生產

目標:構建體積

版本變動:webpack3

webpack4支持tree-shaking,可以刪除未使用的代碼,請使用ES module的導入方式,確保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將會從代碼中刪除。按下述導入方式,tree-shaking不會生效:

// common.js 同上

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

// common 依賴將被整個打包,無法實現tree-shaking

由于JS動態編譯的特性,tree-shaking的效果有限。對于很多依賴,webpack無法很好的執行tree-shaking。另外webpack3不支持tree-shaking。

在開發其他項目的依賴包時,可考慮在 package.js 中添加 sideEffects:false,其他項目在導入你的包時,會嘗試進行tree-shaking。

15. 使用TerserWebpackPlugin 最小化JS

環境:生產

目標:構建速度,打包體積

TerserWebpackPlugin 可以最小化JS,即壓縮js,并且支持最小化ES6,webpack4生產模式下自動啟用,你也可以進行一些配置:

  • 請確保 parallel 多進程開啟,提高打包速度。
  • 像loader一樣,通過 testincludeexclude 縮小壓縮文件范圍。也可以用 chunkFilter 排除一些chunk。
  • 非webpack5版本,請確保 cache 緩存開啟,webpack5中此選項失效。
  • 如果需要source-map,請配置 sourceMaptrue ,這將生成source-map。注意cheap-source-map 等包含cheap 的source-map 不會產生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, // 多進程,默認開啟
                cache: true, // 持久緩存,默認開啟
                sourceMap: true, // 支持產生source-map,默認不開啟
                terserOptions: {
                    //...  設置terser的壓縮選項,比如是否清除注釋,詳見官方文檔
                }
      }),
    ],
  },
};

不要使用 UglifyjsWebpackPlugin,此插件不支持ES6,并已經停止維護。

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

環境:生產

目標:打包體積

使用 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. 優化圖片

環境:生產

目標: 打包體積

圖片是網站重要的一部分,不僅體積大,而且數量多,占用大量帶寬,下面是優化圖片的一些方法:

  • 使用 url-loader 將小尺寸圖片進行內聯。可以配置 limit 選項,將小于此值的圖片轉換成base64 格式并存在JS代碼中,以節少請求數量。 file-loader 有和 url-loader 類似的功能,可以解析靜態文件,但不能設置 limit 屬性進行內聯。
  • 使用 svg-loader 將svg圖片進行內聯。此插件和 url-loader 類似,但SVG是文本,壓縮體積上更有效率。
  • 雪碧圖可將多張小圖片組裝合成一張大圖片,減少請求數量。由于雪碧圖配置較復雜,不推薦使用。可以利用 webpack-spritesmith 插件實現雪碧圖。
  • 使用 image-webpack-loader 壓縮圖片。它支持JPG,PNG,GIF和SVG等幾乎所有格式。 它不會將圖片內聯,需與url-loader等配合使用,即先壓縮圖片,再進行內聯。可以通過 enforce: 'pre' 確保在其他loader前執行。

配置示例如下:

// 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調用前,先行調用,避免重復代碼
            },
            // 內聯圖片
      {
                test: /\\.(jpe?g|png|gif)$/,
        loader: 'url-loader',
                options: {
          limit: 10 * 1024, // 小于10kb的會被內聯
        },
      }
            // 內聯svg
            {
        test: /\\.svg$/,
        loader: 'svg-url-loader',
        options: {
          limit: 10 * 1024, // 小于10kb的會被內聯
          noquotes: true,  // 刪除引號
                    iesafe: true, // 支持IE,但會增加體積
        },
      },
    ],
  },
};

18. 優化第三方依賴包

環境: 開發,生產

目標: 構建速度、增量構建速度、打包體積

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

Googel 在Github repo上收集了一些優化建議,其中包括babel-loard、loadsh、react、bootstrap等的優化建議。請在github上搜索“webpack-libs-optimizations”查看。

19. 對html-webpack-plugin的優化

環境:生產

目標:打包體積

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

// 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進行壓縮,所以需要安裝相關依賴
        }
    })
  ]
}

20. 優化Node核心庫的polyfill

環境:生產

目標:打包體積,加載體積

版本變動:webpack5

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

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

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

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

其中的 consoleglobalprocess 等為node核心庫中的相應功能,更多選項需查找 node 核心庫。配置的值的含義如下:

  • true :提供polyfill。
  • mock :提供 mock 實現預期接口,但功能很少或沒有。
  • empty :提供一個空對象。
  • false :不提供polyfill,如果使用相應的node庫,會觸發錯誤。

21. 分離代碼

環境:生產

目標:打包體積,加載體積

版本變動:webpack3,webpack5

webpack將各種資源打包成一個JS文件,合并請求,優化用戶體驗。現在的web應用體積越來越大,單一的js文件體積過大,需拆分成多個js文件,減少加載體積。這也是懶加載、長久緩存等技術使用的基礎。分離出的每一個js文件,稱之為chunk。

webpack3使用 CommonsCunksPlugn 配置復雜,webpack4提供開箱即用的 optimization.splitChunks 。生產環境自動啟用 optimization.splitChunks ,默認配置足以應對大多數情況,不需要專門配置。默認配置遵照的主要規則:

  • 主入口會打包成單獨的chunk。
  • 動態導入的每一個模塊,會打包成一個chunk。例如按路由分離3個頁面,將會打包出3個chunk。
  • 每個chunk中用到的node_modules依賴,會被打單獨打包成以 vendors~ 開頭的chunk
  • 如果兩個chunk中包含相同的內容,相同的內容會被單獨打包成一個共享chunk。

比如有個項目:

  • 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

如修改默認規則,請詳細閱讀官方文檔,并注意評估打包結果,防止負優化,例如:

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

webpack5對optimization.splitChunks 進行一些優化,配置略有不同。

22. 分離CSS

環境:生產

目標:打包體積,加載體積

版本變動:webpack3

在分離JS的基礎上,有時需分離CSS,以實現CSS的按需加載。在webpack3 時使用 ExtractTextWebpackPlugin , 在webpack4 時請使用 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' // 熱響應
            },
          },
          'css-loader',
        ],
      },
    ],
  },
};

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

23. 使用按需加載,懶加載,預加載

環境:生產

目標:加載體積

版本變動:webpack5

現在網頁應用功能越來越多,尺寸越來越大,我們可以只加載用戶用到的部分,而不加載其他部分以提高性能。比如只加載首屏需要的chunk,而其他部分的chunk暫時不加載。

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

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

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

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

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

24. 使用長久緩存

環境:生產

目標:加載體積

版本變動:webpack3,webpack5

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

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

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

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

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

    需要在服務器上設置緩存文件時長,比如通過Cache-Control

    // Server header
    Cache-Control: max-age=31536000      // 設置緩存一年時間
    
    

    有多種設置緩存的方法,請查看相關文章

  • 第二步,使用 recordsPath 配置項,方便調試

    在設置長久緩存時,我們需要知道webpack打包后的文件是如何劃分的,模塊id是否改變,recordsPath 配置項可以產生一份json文件,記錄每次打包后的結果。

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

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

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

    由于不同的系統、計算機使用的hash計算方式不同,在不同的計算機對相同的項目打包,產生的hash值可能不同,導致長久緩存失效。

  • 第四步,分離runtime

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

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

    通常每個chunk中都包含runtime信息,只要有一個chunk發生變化,runtime就會變化,進而使所有chunk發生變化。可以通過 optimization.runtimeChunk 將runtime分離為一個單獨的chunk,我們將將這個chunk稱為 runtime.js,有時也稱為manifest.js (比如vue-cli中)。

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

    分離的runtime文件,將作為一個獨立的js加載,瀏覽器的請求順序變為:

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

    可以看出多了次請求,使用 inline-manifest-webpack-plugin 將runtime內聯進html文件,瀏覽器的請求順序修改為:

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

    也可以使用webpack-manifest-plugin 實現同樣功能, inline-manifest-webpack-plugin 的配置示例:

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

    webpack會給每一個moudle分配一個id,id是按照順序計算的,如 0,1,2。如果你刪除了某個moudle,或者新增某個moudle,可能導致module id的順序發生變化,比如:

    moudleId   變動情況
    0          原來的0
    1          新增的module
    2          原來的1, id變化
    3          原來的2,id變化
    
    

    moudle id的變化會導致chunk文件發生變化,我們可以用 hashed-module-ids-plugin 或者用 optimization.moduleIds 來穩定moudle id:

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

經過復雜的配置,終于可以實現長久緩存。

25. 使用性能預算,控制體積

環境:生產

目標:打包體積

項目初期的體積較小,但隨項目發展,可能沒注意到,安裝的某個依賴,使項目打包體積急劇增加。使用性能預算,及時發現打包體積的增加。

配置 performance 即可啟用性能預算,生成環境自動開啟,默認配置如下:

module.exports = {
    // ...
    performance: {
          hints: 'warning', // 尺寸過大后警告, 
                            // 設為'errors'時體積過大將報錯
                            // 設為false時關閉性能預算
          maxEntrypointSize: 250000, // 最大入口體積
          maxAssetSize:250000 // 單個資源最大體積
    }
}

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

budgest.png

使用 bundlesize 工具,可以進行更靈活的配置,比如單獨設置某個文件的體積預算,同時支持自動化CI。

26. 其他優化建議

  • 并行運行多個webpack實例 。打包多個版本,比如打包國內版本應用、國際版本應用,可以使用 parallel-webpack 并行運行多個webpack實例。
  • 指定browserslist。指定合適的browserslist,babel將據此選擇核合適的降級工具,有助于代碼體積的減少和打包速度的提升。比如開發環境需要更快的打包速度,可以設為 'last 2 Chrome versions';生產環境需考慮兼容性,可設為 '> 1%, ie >= 11, not dead'
  • 考慮為不同的瀏覽器打包不同版本,可以為IE、現代瀏覽器分別打包,再根據瀏覽器的標識加載不同的版本。也可以根據瀏覽器的尺寸將圖片打包成不同的尺寸,對PC端瀏覽器返回大圖,而對移動端瀏覽器返回較小尺寸的圖片。
  • 如果擔心打包后的文件包含多個相同模塊,可以用 duplicate-package-checker-webpack-plugin 或者 bundle-duplicates-plugin 進行檢查。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380