看完這篇你一定懂 --- Webpack4 打包優化

前面的話

Webpack升級4之後,小柒踩過的很多坑,這篇文章總結Webpack4的一些新特性,以及常見的優化方式。

Webpack4 新特性

  • 不再強制需要webpack.config.js配置文件。默認入口爲./src/index.js,默認輸出./dist目錄,輸出文件main.js

  • Webpack不再能單獨使用,4.x版本的很多命令都移動到了webpack-cli中。所以必須安裝webpackwebpack-cli

  • 開箱即用 WebAssembly,webpack4提供了wasm的支持,現在可以引入和導出任何一個 Webassembly 的模塊,也可以寫一個loader來引入C++、C和Rust。(注:WebAssembly 模塊只能在異步chunks中使用)

  • 增加模式區分,在配置文件中使用mode選項來配置相應的模式:
    development:開發模式。打包默認不壓縮代碼,默認開啓代碼調試
    production:生產模式,線上使用。打包壓縮代碼,不開啓代碼調試

  • 一些插件由optimeztion配置代替。
    比如:

    • CommonsChunkPlugin廢棄,由optimization.splitChunksoptimization.runtimeChunk代替,前者拆分代碼,後者提取runtime代碼
    • optimize.UglifyJsPlugin 廢棄,由 optimization.minimize 替代,生產環境默認開啓。
  • 升級到 webpack4 後,mini-css-extract-plugin 替代 extract-text-webpack-plugin 成爲css分離首選。

量化

speed-measure-webpack-paligin插件可以測量各個插件和loader所花費的時間。
使用如下:

 const SpeedMeasurePlugin = require('speed-measure-webpack-palugin');
 const smp = new SpeedMeasurePlugin();
 const config = {
 	// ...webpack配置
}
module.exports = smp.wrap(config);

在這裏插入圖片描述

從打包時間上優化

從哪幾個方面優化
  • 減少搜索依賴的時間
  • 減少loader解析時間
  • 減少js文件壓縮時間: 將所有解析完的代碼,打包到一個文件中,爲了減小白屏時間,webpack爲對其進行優化。js壓縮是發佈編譯的最後階段,通常webpack需要卡好一會,因爲壓縮js代碼要將代碼解析成AST,再根據複雜的規則去分析和處理AST,最後將AST還原爲js。
  • 減少二次打包時間:當我們更改項目中的文件是,就要重新打包。有些文件其實不需要再次打包,所以要減少二次打包時間。
1、減少搜索依賴的時間
  • 優化loader配置
    由於loader轉換文件比較耗時,所以讓儘可能少的文件被loader處理。使用testincludeexclude三個配置項來命中loader要應用的文件。
  • 優化resolve.modules配置
    resolve.modules用於配置Webpack去哪些目錄下尋找第三方模塊。resolve.modules的默認值是['node_modules'],意思是先從當前目錄下的./node_modules目錄下去找我們想要的模塊,如果沒有就去上級目錄…/node_modules,再沒有就去…/…/node_modules中找,以此類推。
  • 優化resolve.mainFields配置
    用於配置第三方模板使用哪個入口文件。
  • 優化resolve.alias配置
    通過別名來把原來的導入路徑映射成爲一個新的路徑,減少解析時間
  • 優化reslove.extensions配置
    在導入語句沒有文件後綴時,Webpack會根據resolve.extension自動帶上後綴後去嘗試詢問文件是否存在。
  • 優化module.noParse配置
    可以讓Webpack忽略對部分模塊沒有采用模塊化的文件的遞歸解析處理。比如:jQuery、ChartJS它們龐大有沒有采用模塊化標準,讓Webpack去解析這些文件耗時又沒有意義。

詳細配置:根據你的項目去選擇

// 編譯代碼的基礎配置
module.exports = {
  // ...
  module: {
    // 項目中使用的 jquery 並沒有採用模塊化標準,webpack 忽略它
    noParse: /jquery/,
    rules: [
      {
        // 這裏編譯 js、jsx
        // 注意:如果項目源碼中沒有 jsx 文件就不要寫 /\.jsx?$/,提升正則表達式性能
        test: /\.(js|jsx)$/,
        // babel-loader 支持緩存轉換出的結果,通過 cacheDirectory 選項開啓
        use: ['babel-loader?cacheDirectory'],
        // 排除 node_modules 目錄下的文件
        // node_modules 目錄下的文件都是採用的 ES5 語法,沒必要再通過 Babel 去轉換
        exclude: /node_modules/,
      },
    ]
  },
  resolve: {
    // 設置模塊導入規則,import/require時會直接在這些目錄找文件
    // 可以指明存放第三方模塊的絕對路徑,以減少尋找
    modules: [
      path.resolve(`${project}/client/components`), 
      path.resolve('h5_commonr/components'), 
      'node_modules'
    ],
    // import導入時省略後綴
    // 注意:儘可能的減少後綴嘗試的可能性
    extensions: ['.js', '.jsx', '.react.js', '.css', '.json'],
    // import導入時別名,減少耗時的遞歸解析操作
    alias: {
      '@compontents': path.resolve(`${project}/compontents`),
    }
  },
};
2、 減少loader的解析時間(開啓多進程)

webpack是單線程模式,只能一個一個文件去處理,當打包文件比較大時,打包時間就會比較長。

  • HappyPack(Webpack 3)

    原理:將loader的解析交給多個進程並行去處理,發揮CPU多核的能力,從而減少構建時間。
    安裝依賴: npm install happypack -D
    使用如下:

    // 比如對js文件的處理:
    // 引入happypack
    const HappyPack = require('happypack'); 
    // 引入系統操作模塊
    const os = require('os');
    // 構造共享進程池,根據系統內核數量,指定進程池的個數,也可以是其他數量
    const happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length}); 
    const createHappyPlugin = (id, loaders) => new HappyPack({
      id: id, // 唯一標識
      loaders: loaders, // 使用的loader
      // 其他選項
      threadPool:  happyThreadPool,  //使用共享進程池中的子進程去處理任務
      verbose: true // 是否允許HappyPack輸出日誌,默認爲true
    })
    module.exports = {
     // ... 其他選項
     module: {
        rules: [
            // ... 其他文件loader配置
            {
              test: /\.js$/,
              // 對js文件的處理交給`id`爲babel的HappyPack實例
              use: ['happypack/loader?id=babel'],
              // node_modules目錄下的文件都是es5語法,不用通過babel轉換
              exclude: /node_modules/,
            },
            
          ]
      },
      plugins: [
        // ...其他插件
        createHappyPlugin('babel',[{
          loader: 'babel-loader',
          options: {
            babelrc: true,
            cacheDirectory: true // 啓用緩存
          }
        }])
      ]
    }
    

    注意:只在解析時間長的loader上使用,項目小也不必使用。並且happypack不推薦使用,現在已經不再維護它

  • thread-loader(Webpack4 推薦)

    使用比較簡單,將這個thread-loader放在其他loader之前就可以了。

    原理: 也是開啓多進程來並行loader的解析,被放置了thread-loader的loader就會在單獨的worker池中運行,一個worker就是一個node進程。每個單獨的進程處理時間限制在600ms。

    安裝依賴: npm install thread-loader -D

    使用如下:

    module.exports = {
      // ...
      module: {
        rules: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            // 創建一個 js worker 池
            use: [ 
              'thread-loader',
              'babel-loader'
            ] 
          },
          {
            test: /\.s?css$/,
            exclude: /node_modules/,
            // 創建一個 css worker 池
            use: [
              'style-loader',
              'thread-loader',
              {
                loader: 'css-loader',
                options: {
                  modules: true,
                  localIdentName: '[name]__[local]--[hash:base64:5]',
                  importLoaders: 1
                }
              },
            ]
          }
          // ...
        ]
        // ...
    
3、減少js壓縮時間

發佈到到線上的代碼,一般都會壓縮js代碼。

  • UglifyJsPlugin (Webpack 3)

    UglifyJsPlugin是webpack3內置的的插件,使用時引入就好,是單進程的。

    原理: 配置在壓縮過程中使用的規則。

    使用如下,給出最優化的代碼配置:

    const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
    
    module.exports = {
     // ...
     plugins: [
      new UglifyJsPlugin({
          compress: {
            warnings: false,// 在UglifyJsPlugin刪除沒有用到的代碼時,不是輸出警告
            drop_console: true,// 刪除所有的'console'語句
            collapse_var: true,// 內嵌已定義,但是隻用了一次的變量
            reduce_var: true,// 提取出現了多次但沒有定義成變量去引用的靜態值。
          },
          output:{
            // 最緊湊的輸出
            beautify: false,
            // 刪除所有的註釋
            comments: false
          }
        })
     ]
    

    此外,Webpack提供了更簡單的方法來接入UglifyJsPlugin,直接在啓動Webpack時,帶上--optimize-minimize參數。即webpack --optimize-minimize

  • webpack-parallel-uglify-plugin (Webpack 3)

    看名字都知道,並行的UglifyPlugin嘛,就是利用了多進程

    原理:開啓多個子進程,對多個文件的壓縮工作分配給多個子進程去完成,每個子進程其實還是通過UgilifyJS去壓縮代碼,但是並行執行。子進程處理完後再把結果發送給主進程。

    安裝依賴: npm install webpack-parallel-uglify-plugin -D

    使用如下:

    const ParallelUglifyJsPlugin = require('webpack-parallel-uglify-plugin');
    
    	
    	module.exports = {
    	 // ...
    	 plugins: [
    	   new ParallelUglifyJsPlugin({
    	   	 cacheDir: './cache' // 緩存壓縮後的結果
    	      // 傳遞給UglifyJS的參數
    	      uglifyJS: {
    	        compress: {
    	          warnings: false,// 在UglifyJsPlugin刪除沒有用到的代碼時,不是輸出警告
    	          drop_console: true,// 刪除所有的'console'語句
    	          collapse_var: true,// 內嵌已定義,但是隻用了一次的變量
    	          reduce_var: true,// 提取出現了多次但沒有定義成變量去引用的靜態值。
    	        },
    	        output:{
    	          // 最緊湊的輸出
    	          beautify: false,
    	          // 刪除所有的註釋
    	          comments: false
    	        }
    	      }
    	})
     ]
    

    上面的ugilifyJS參數是用於壓縮ES5代碼是時的配置,也可以是ugilifyES:用於壓縮ES6代碼。

  • terser-webpack-plugin(Webpack 4)

    terser:簡要的。

    官方定義:用於ES6+的js解析器、壓縮工具。爲何選擇terser:不再維護UglifyES,並且UgilifyJS不支持ES6+。terser是UglifyES的一個分支,主要保留了與UglifyJS和UglifyES的API和CLI兼容性。

    原理:開啓多進程

    安裝依賴: npm install terser-webpack-plugin -D
    使用如下:

    module.exports = {
      optimization: {
        minimizer: [
          new TerserPlugin({
            parallel: true,
          }),
        ],
      },
    };
    
4、 減少二次打包時間

合理利用緩存,來減少二次打包時間。比如cache-loader HardSourceWebpackPluginbabel-loader的cacheDirectory標誌。但所有的緩存方法都有啓動開銷。二次打包節約時間,初次打包很慢

  • cache-loader

    cache-loader的配置很簡單,只要放在其他loader之前即可。如果只是下babel-loader配置cache,使用babel-loadercacheDirectory

    安裝依賴npm install cache-loader -D

    module.exports = {
      module: {
        rules: [
          {
            test: /\.ext$/,
            use: ['cache-loader', ...loaders],
            include: path.resolve('src'),
          },
        ],
      },
    };
    
  • hard-source-webpack-plugin

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

    使用如下:

    const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
    // ...
    plugins: [
        new HardSourceWebpackPlugin()
      ],
    

    非首次使用HardSourceWebpackPlugin打包的時間,明顯比沒有使用要快:
    在這裏插入圖片描述

從打包體積上優化

  • Tree Shaking (依賴需要採用ES6模塊化語法的代碼)

    可以去除無用代碼。

    要求:必須使用ES6模塊化語法,使用babel時,要關閉其模塊轉換功能,修改.babelrc文件或者在 webpack.config.js 配置文件中將modules設置爲false。

    // .babelrc
    {
      "presets": [
        ["env",
          {
            "modules": false // 保留es6的模塊化語法
          }
        ]
      ]
    }
    

    Webpack4下的配置:

    • 首先,你必須處於生產模式。Webpack 只有在壓縮代碼的時候會 tree-shaking。
    • 其次,開啓優化選項 “usedExports” 。這樣Webpack就看可以提示你哪些是用不上的代碼
    • 最後,你需要使用一個支持刪除死代碼的壓縮器。上面提到過terser-webpack-plugin

    注意:生產環境下默認開啓

    const config = {
     mode: 'production', // 生產模式
     optimization: {
     usedExports: true, // 
     minimizer: [
      new TerserPlugin({...})
     ]
     }
    };
    

    還需注意的一點:

    package.json文件中,有一個特殊的屬性sideEffects,它有三個可能值:

    • true: 是默認值,意味着所有的文件都不能進行tree-shaking

    • false: 表示所有的文件都可以進行tree-shaking

    • [...]: 表示在數組中的文件,是不可以進行tree-shaking的,其他文件都可以.

      // 所有文件都有副作用,全都不可 tree-shaking
      {
       "sideEffects": true
      }
      // 沒有文件有副作用,全都可以 tree-shaking
      {
       "sideEffects": false
      }
      // 只有這些文件有副作用,所有其他文件都可以 tree-shaking,但會保留這些文件
      {
       "sideEffects": [
       "./src/file1.js",
       "./src/file2.js"
       ]
      }
      
  • Scope Hoisting (依賴採用ES6模塊化語法的代碼)

    可以讓Webpack 打包出來的代碼更小。

    原理:分析模塊之間的依賴關係,儘可能將被打散的模塊合併到一個函數中,但前提是不能造成代碼冗餘。因此只有那些被引用了一次的模塊才能被合併。

    使用如下:

    const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
    
    module.exports = {
      // ... 
     resolve: {
        // 針對Npm中的第三方模塊優先採用jsnext:main中指向ES6模塊化語法的文件
        mainFields:['jsnext:main', 'browser', 'main']
      },
      plugins: [
      // 開啓Scope Hoisting
        new ModuleConcatenationPlugin()
       ],
       // ...
    

    注意:生產環境下默認開啓。

最後

還有使用DllPluginDllReferencePlugin分離基礎模塊(vue-router、vuex等)、使用optimization.splitChunksoptimization.runtimeChunk分離公共代碼。這兩部分內容放到下篇總結。

參考文章:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章