天天看點

19-webpack性能優化集錦

1.提高 Webpack 打包速度

(1)優化loader的檔案搜尋範圍

Babel 是編寫下一代 JavaScript 的編譯器

對于 Loader 來說,影響打包效率首當其沖必屬 Babel 了。因為 Babel 會将代碼轉為字元串生成 AST,然後對 AST 繼續進行轉變最後再生成新的代碼,項目越大,轉換代碼越多,效率就越低

首先我們可以優化 Loader 的檔案搜尋範圍,在使用loader時,我們可以指定哪些檔案不通過loader處理,或者指定哪些檔案通過loader處理。

module.exports = {
  module: {
    rules: [
      {        // js 檔案才使用 babel
        test: /\.js$/,
        use: ['babel-loader'],        // 隻處理src檔案夾下面的檔案
        include: path.resolve('src'),        // 不處理node_modules下面的檔案
        exclude: /node_modules/
      }
    ]
  }
}      

(2)将babel編譯過的檔案緩存起來

對于babel-loader,我們還可以将 Babel 編譯過的檔案緩存起來,下次隻需要編譯更改過的代碼檔案即可,這樣可以大幅度加快打包時間。

通過使用 cacheDirectory 選項,将 babel-loader 提速至少兩倍。這會将轉譯的結果緩存到檔案系統中。

{
  test: /\.js$/,
  use: 'babel-loader?cacheDirectory',
  include: [resolve('src'), resolve('test') ,resolve('node_modules/webpack-dev-server/client')] //緩存用戶端(浏覽器)這個檔案夾
}      

cache-loader:緩存其他loader

除了 babel-loader,如果我們想讓其他的 loader 的處理結果也緩存,該怎麼做呢?

答案是可以使用 cache-loader。在一些性能開銷較大的 loader 之前添加 cache-loader,以便将結果緩存到磁盤裡

安裝

npm install --save-dev cache-loader      
module.exports = {
  module: {
    rules: [
      {
     // js 檔案才使用 babel
        test: /\.js$/,
        use: ['cache-loader', ...loaders], //...loader是要指定的要緩存的loader的一個統稱
        include: path.resolve('src'), //前面babel-loader 時候,已經統一指定隻處理src下的檔案,這裡就不用單獨再寫了
      },
    ],
  },
};      

那這麼說的話,我給每個loder前面都加上cache-loader,然而凡事物極必反,儲存和讀取這些緩存檔案會有一些時間開銷,是以請隻對性能開銷較大的 loader 使用 cache-loader。關于這個cache-loader更詳細的使用方法請參照這裡cache-loader

 那上面說了,我可以給指定的loader做緩存,那麼我怎麼知道是哪些loader比較慢,需要給其做緩存呢?因為不能無腦的全部緩存,這樣儲存和讀取有時間開銷,我們要知己知彼才能對症下藥。是以我們需要一個分析速度的插件:

(3)分析各loader和plugin速度插件:speed-measure-webpack-plugin

// 安裝npm install --save-dev speed-measure-webpack-plugin// 使用方式const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
 
const smp = new SpeedMeasurePlugin();
 
const webpackConfig = smp.wrap({
  module: {},
  plugins: [    new MyPlugin(),    new MyOtherPlugin()
  ]
});      

運作時候如下圖所示

19-webpack性能優化集錦

但需要注意的是:HardSourceWebpackPlugin 和 speed-measure-webpack-plugin 不能一起使用。

二、使用多線程打包

1.HappyPack :就是能夠讓Webpack把打包任務分解給多個子線程去并發的執行,子線程處理完後再把結果發送給主線程。

在使用 Webpack 對項目進行建構時,webpack建構過程中的有兩個部分是直接影響建構效率的,一個是檔案的編譯,另一個則是檔案的分類打包。相較之下檔案的編譯更為耗時,而且在Node環境下檔案隻能一個一個去處理,是以這塊的優化需要解決。

由于 JavaScript 是單線程模型,要想發揮多核 CPU 的能力,隻能通過多程序去實作,而無法通過多線程實作。

而 Happypack 的作用就是将檔案解析任務分解成多個子程序(多個子程序也就意味着多個子線程,一個程序裡面有一個線程)并發執行。子程序處理完任務後再将結果發送給主程序。是以可以大大提升 Webpack 的項目構件速度

由于HappyPack 對file-loader、url-loader 支援的不友好,是以不建議對該loader使用。

module: {
  rules: [
    {
        test: /\.js$/,        // 把對 .js 檔案的處理轉交給 id 為 babel 的 HappyPack 執行個體
        use: ['happypack/loader?id=babel'],
        exclude: path.resolve(__dirname, 'node_modules'),
    },
    {
        test: /\.css$/,        // 把對 .css 檔案的處理轉交給 id 為 css 的 HappyPack 執行個體
        use: ['happypack/loader?id=css']
    }
  ]
},
plugins: [      new HappyPack({
        id: 'js', //ID是辨別符的意思,ID用來代理目前的happypack是用來處理一類特定的檔案的
        threads: 4, //你要開啟多少個子程序去處理這一類型的檔案
        loaders: [ 'babel-loader' ]
    }),    new HappyPack({
        id: 'css',
        threads: 2,
        loaders: [ 'style-loader', 'css-loader' ]
    })
]      

2.thread-loader:在worker 池(worker pool)中運作加載器loader。把thread-loader 放置在其他 loader 之前, 放置在這個 thread-loader 之後的 loader 就會在一個單獨的 worker 池(worker pool)中運作。

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve('src'),
        use: [
          {
              loader: "thread-loader",              // 有同樣配置的 loader 會共享一個 worker 池(worker pool)              options: {                  // 産生的 worker 的數量,預設是 cpu 的核心數
                  workers: 2,                  // 一個 worker 程序中并行執行工作的數量
                  // 預設為 20
                  workerParallelJobs: 50,                  // 額外的 node.js 參數
                  workerNodeArgs: ['--max-old-space-size', '1024'],                  // 閑置時定時删除 worker 程序
                  // 預設為 500ms
                  // 可以設定為無窮大, 這樣在監視模式(--watch)下可以保持 worker 持續存在
                  poolTimeout: 2000,                  // 池(pool)配置設定給 worker 的工作數量
                  // 預設為 200
                  // 降低這個數值會降低總體的效率,但是會提升工作分布更均一
                  poolParallelJobs: 50,                  // 池(pool)的名稱
                  // 可以修改名稱來建立其餘選項都一樣的池(pool)
                  name: "my-pool"
              }
          }, 
          {
              loader:'babel-loader'
          }
        ]
      }
    ]
  }
}      

同樣,thread-loader也不是越多越好,也請隻在耗時的loader 上使用。

 3.webpack-parallel-uglify-plugin :開啟多個子程序

在Webpack3 中,我們一般使用 UglifyJS 來壓縮代碼,但是這個是單線程運作的,也就是說多個js檔案需要被壓縮,它需要一個個檔案進行壓縮。是以說在正式環境打包壓縮代碼速度非常慢(因為壓縮JS代碼需要先把代碼解析成AST文法樹,再去應用各種規則分析和處理AST,導緻這個過程耗時非常大)。為了加快效率,我們可以使用 webpack-parallel-uglify-plugin 插件,該插件會開啟多個子程序,把對多個檔案壓縮的工作分别給多個子程序去完成,但是每個子程序還是通過UglifyJS去壓縮代碼。無非就是變成了并行處理該壓縮了,并行處理多個子任務,提高打包效率。來并行運作 UglifyJS,進而提高效率。

在 Webpack4 中,我們就不需要以上這些操作了,隻需要将 mode 設定為 production 就可以預設開啟以上功能。代碼壓縮也是我們必做的性能優化方案,當然我們不止可以壓縮JS 代碼,還可以壓縮HTML、CSS 代碼,并且在壓縮 JS 代碼的過程中,我們還可以通過配置實作比如删除 console.log 這類代碼的功能。

let ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = {
    module: {},
    plugins: [        new ParallelUglifyPlugin({
            workerCount:3,//開啟幾個子程序去并發的執行壓縮。預設是目前運作電腦的cPU核數減去1            uglifyJs:{
                output:{
                    beautify:false,//不需要格式化
                    comments:false,//不保留注釋                },
                compress:{
                    warnings:false,//在Uglify]s除沒有用到的代碼時不輸出警告
                    drop_console:true,//删除所有的console語句,可以相容ie浏覽器
                    collapse_vars:true,//内嵌定義了但是隻用到一次的變量
                    reduce_vars:true,//取出出現多次但是沒有定義成變量去引用的靜态值                }
            },
        })
    ]
}      

4.DllPlugin&DllReferencePlugin :将特定的類庫提前打包成動态連結庫

DllPlugin可以将特定的類庫提前打包成動态連結庫,在一個動态連結庫中可以包含給其他子產品調用的函數和資料,把基礎子產品獨立出來打包到單獨的動态連接配接庫裡,當需要導入的子產品在動态連接配接庫裡的時候,子產品不用再次被打包,而是去動态連接配接庫裡擷取。這種方式可以極大的減少打包類庫的次數,隻有當類庫更新版本才有需要重新打包,并且也實作了将公共代碼抽離成單獨檔案的優化方案。

這裡我們可以先将react、react-dom單獨打包成動态連結庫,首先建立一個新的webpack配置檔案:webpack.dll.js

19-webpack性能優化集錦

個人了解的dll基本過程:

1、第一次npm run的時候,把請求的内容存儲起來(存儲在映射表中)

2、再次請求時,先從映射表中找,看請求的内容是否有緩存,有則加載緩存(類似浏覽器的緩存政策,命中緩存),沒有就正常打包。

3、直接從緩存中讀取。

配置:

const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
module.exports = {    // 想統一打包的類庫
    entry:['react','react-dom'],
    output:{
        filename: '[name].dll.js',  //輸出的動态連結庫的檔案名稱,[name] 代表目前動态連結庫的名稱
        path:path.resolve(__dirname,'dll'),  // 輸出的檔案都放到 dll 目錄下
        library: '_dll_[name]',//存放動态連結庫的全局變量名稱,例如對應 react 來說就是 _dll_react    },
    plugins:[        new DllPlugin({            // 動态連結庫的全局變量名稱,需要和 output.library 中保持一緻
            // 該字段的值也就是輸出的 manifest.json 檔案 中 name 字段的值
            // 例如 react.manifest.json 中就有 "name": "_dll_react"
            name: '_dll_[name]',            // 描述動态連結庫的 manifest.json 檔案輸出時的檔案名稱
            path: path.join(__dirname, 'dll', '[name].manifest.json')
        })
    ]
}      

然後我們需要執行這個配置檔案生成依賴檔案:

webpack --config webpack.dll.js --mode development      

接下來我們需要使用 DllReferencePlugin 将依賴檔案引入項目中

const DllReferencePlugin = require('webpack/lib/DllReferencePlugin')
module.exports = {  // ...省略其他配置  plugins: [    new DllReferencePlugin({      // manifest 就是之前打包出來的 json 檔案
      manifest:path.join(__dirname, 'dll', 'react.manifest.json')
    })
  ]
}      

5. noParse:可以用于配置那些子產品檔案的内容不需要進行解析(即無依賴) 的第三方大型類庫(例如jquery,lodash)等,使用該屬性讓 Webpack不掃描該檔案,以提高整體的建構速度。

module.exports = {
    module: {
      noParse: /jquery|lodash/, // 正規表達式
      // 或者使用函數      noParse(content) {        return /jquery|lodash/.test(content)
      }
    }
}      

6.IgnorePlugin :IgnorePlugin用于忽略某些特定的子產品,讓webpack 不把這些指定的子產品打包進去。

module.exports = {  // ...省略其他配置  plugins: [    new webpack.IgnorePlugin(/^\.\/locale/,/moment$/)
  ]
}      

webpack.IgnorePlugin()參數中第一個參數是比對引入子產品路徑的正規表達式,第二個參數是比對子產品的對應上下文,即所在目錄名。

 三、值的注意的小點點

  • resolve.extensions:用來表明檔案字尾清單,預設查找順序是 ['.js', '.json'],如果你的導入檔案沒有添加字尾就會按照這個順序查找檔案。我們應該盡可能減少字尾清單長度,然後将出現頻率高的字尾排在前面
  • resolve.alias:可以通過别名的方式來映射一個路徑,能讓 Webpack 更快找到路徑
module.exports ={    // ...省略其他配置    resolve: {
        extensions: [".js",".jsx",".json",".css"],
        alias:{            "jquery":jquery
        }
    }
};      

四、減少Wwebpack打包後的檔案體積

1.image-webpack-loader 對圖檔進行壓縮和優化

image-webpack-loader這個loder可以幫助我們對打包後的圖檔進行壓縮和優化,例如降低圖檔分辨率,壓縮圖檔體積等。

module.exports ={    // ...省略其他配置    module: {
        rules: [
            {
                test: /\.(png|svg|jpg|gif|jpeg|ico)$/,
                use: [                    'file-loader',
                    {
                        loader: 'image-webpack-loader',
                        options: {
                            mozjpeg: {
                                progressive: true,
                                quality: 65
                            },
                            optipng: {
                                enabled: false,
                            },
                            pngquant: {
                                quality: '65-90',
                                speed: 4
                            },
                            gifsicle: {
                                interlaced: false,
                            },
                            webp: {
                                quality: 75
                            }
                        }
                    }
                ]
            }
        ]
    }
};      

2.删除無用的CSS樣式

有時候一些時間久遠的項目,可能會存在一些CSS樣式被疊代廢棄,需要将其剔除掉,此時就可以使用purgecss-webpack-plugin插件,該插件可以去除未使用的CSS,一般與 glob、glob-all 配合使用。

注意:此插件必須和CSS代碼抽離插件mini-css-extract-plugin配合使用。

例如我們有樣式檔案style.css

body{
    background: red
}
.class1{
    background: red
}      

這裡的.class1顯然是無用的,我們可以搜尋src目錄下的檔案,删除無用的樣式。

const glob = require('glob');
const PurgecssPlugin = require('purgecss-webpack-plugin');

module.exports ={    // ...    plugins: [        // 需要配合mini-css-extract-plugin插件
        new PurgecssPlugin({
            paths: glob.sync(`${path.join(__dirname, 'src')}/**/*`, 
                  {nodir: true}), // 不比對目錄,隻比對檔案            })
        }),
    ]
}      

3.以CDN方式加載資源

我們知道,一般常用的類庫都會釋出在CDN上,是以,我們可以在項目中以CDN的方式加載資源,這樣我們就不用對資源進行打包,可以大大減少打包後的檔案體積。

以CDN方式加載資源需要使用到add-asset-html-cdn-webpack-plugin插件。我們以CDN方式加載jquery為例:

const AddAssetHtmlCdnPlugin = require('add-asset-html-cdn-webpack-plugin')

module.exports ={    // ...    plugins: [        new AddAssetHtmlCdnPlugin(true,{            'jquery':'https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js'
        })
    ],    //在配置檔案中标注jquery是外部的,這樣打包時就不會将jquery進行打包了    externals:{      'jquery':'$'
    }
}      

4.搖樹:Tree Shaking

5.開啟Scope Hoisting

Scope Hoisting 可以讓 Webpack 打包出來的代碼檔案更小、運作的更快, 它又譯作 "作用域提升",是在 Webpack3 中新推出的功能。

由于最初的webpack轉換後的子產品會包裹上一層函數,import會轉換成require,因為函數會産生大量的作用域,運作時建立的函數作用域越多,記憶體開銷越大。而Scope Hoisting 會分析出子產品之間的依賴關系,盡可能的把打包出來的子產品合并到一個函數中去,然後适當地重命名一些變量以防止命名沖突。這個功能在webpack4中,當我們将mode設定為production時會自動開啟。

比如我們希望打包兩個檔案

let a = 1;
let b = 2;
let c = 3;
let d = a+b+c
export default d;// 引入dimport d from './d';
console.log(d)      

 最終打包後的結果會變成 console.log(6),這樣的打包方式生成的代碼明顯比之前的少多了,并且減少多個函數後記憶體占用也将減少。如果你希望在開發模式development中開啟這個功能,隻需要使用插件 webpack.optimize.ModuleConcatenationPlugin() 就可以了。

module.exports = {  // ...  plugins: [    // 開啟 Scope Hoisting
    new webpack.optimize.ModuleConcatenationPlugin(),
  ]
}      

 6.按需加載&動态加載

 大家在開發單頁面應用項目的時候,項目中都會存在十幾甚至更多的路由頁面。如果我們将這些頁面全部打包進一個檔案的話,雖然将多個請求合并了,但是同樣也加載了很多并不需要的代碼,耗費了更長的時間。那麼為了首頁能更快地呈現給使用者,我們肯定是希望首頁能加載的檔案體積越小越好,這時候我們就可以使用按需加載,将每個路由頁面單獨打包為一個檔案。在給單頁應用做按需加載優化時,一般采用以下原則:

  • 對網站功能進行劃分,每一類一個chunk
  • 對于首次打開頁面需要的功能直接加載,盡快展示給使用者,某些依賴大量代碼的功能點可以按需加載
  • 被分割出去的代碼需要一個按需加載的時機

動态加載目前并沒有原生支援,需要babel的插件:plugin-syntax-dynamic-import。安裝此插件并且在.babelrc中配置:

{  // 添加
  "plugins": ["transform-vue-jsx", "transform-runtime"],
  
}      

 例如如下示例:

index.js

let btn = document.createElement('button');
btn.innerHTML = '點選加載視訊';
btn.addEventListener('click',()=>{
    import(/* webpackChunkName: "video" */'./video').then(res=>{
        console.log(res.default);
    });
});
document.body.appendChild(btn);      
module.exports = {    // ...    output:{
      chunkFilename:'[name].min.js'
    }
}      

繼續閱讀