webpack
的打包優化一直是個老生常談的話題,常規的無非就分塊、拆包、壓縮等。html
本文以我本身的經驗向你們分享如何經過一些分析工具、插件以及webpack
新版本中的一些新特性來顯著提高webpack
的打包速度和改善包體積,學會分析打包的瓶頸以及問題所在。前端
本文演示代碼,倉庫地址vue
webpack 有時候打包很慢,而咱們在項目中可能用了不少的 plugin
和 loader
,想知道究竟是哪一個環節慢,下面這個插件能夠計算 plugin
和 loader
的耗時。node
yarn add -D speed-measure-webpack-plugin
複製代碼
配置也很簡單,把 webpack
配置對象包裹起來便可:react
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin(); const webpackConfig = smp.wrap({ plugins: [ new MyPlugin(), new MyOtherPlugin() ] }); 複製代碼
來看下在項目中引入speed-measure-webpack-plugin
後的打包狀況: 從上圖能夠看出這個插件主要作了兩件事情:jquery
loader
和
plugin
的耗時狀況,咱們就能夠「對症下藥」了
打包後的體積優化是一個能夠着重優化的點,好比引入的一些第三方組件庫過大,這時就要考慮是否須要尋找替代品了。webpack
這裏採用的是webpack-bundle-analyzer
,也是我平時工做中用的最多的一款插件了。git
它能夠用交互式可縮放樹形圖顯示webpack
輸出文件的大小。用起來很是的方便。es6
首先安裝插件:github
yarn add -D webpack-bundle-analyzer
複製代碼
安裝完在webpack.config.js
中簡單的配置一下:
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = { plugins: [ new BundleAnalyzerPlugin({ // 能夠是`server`,`static`或`disabled`。 // 在`server`模式下,分析器將啓動HTTP服務器來顯示軟件包報告。 // 在「靜態」模式下,會生成帶有報告的單個HTML文件。 // 在`disabled`模式下,你可使用這個插件來將`generateStatsFile`設置爲`true`來生成Webpack Stats JSON文件。 analyzerMode: "server", // 將在「服務器」模式下使用的主機啓動HTTP服務器。 analyzerHost: "127.0.0.1", // 將在「服務器」模式下使用的端口啓動HTTP服務器。 analyzerPort: 8866, // 路徑捆綁,將在`static`模式下生成的報告文件。 // 相對於捆綁輸出目錄。 reportFilename: "report.html", // 模塊大小默認顯示在報告中。 // 應該是`stat`,`parsed`或者`gzip`中的一個。 // 有關更多信息,請參見「定義」一節。 defaultSizes: "parsed", // 在默認瀏覽器中自動打開報告 openAnalyzer: true, // 若是爲true,則Webpack Stats JSON文件將在bundle輸出目錄中生成 generateStatsFile: false, // 若是`generateStatsFile`爲`true`,將會生成Webpack Stats JSON文件的名字。 // 相對於捆綁輸出目錄。 statsFilename: "stats.json", // stats.toJson()方法的選項。 // 例如,您可使用`source:false`選項排除統計文件中模塊的來源。 // 在這裏查看更多選項:https: //github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21 statsOptions: null, logLevel: "info" ) ] } 複製代碼
而後在命令行工具中輸入npm run dev
,它默認會起一個端口號爲 8888 的本地服務器: 圖中的每一塊清晰的展現了組件、第三方庫的代碼體積。
有了它,咱們就能夠針對體積偏大的模塊進行相關優化了。
你們都知道 webpack
是運行在 node
環境中,而 node
是單線程的。webpack
的打包過程是 io
密集和計算密集型的操做,若是能同時 fork
多個進程並行處理各個任務,將會有效的縮短構建時間。
平時用的比較多的兩個是thread-loader
和HappyPack
。
先來看下thread-loader
吧,這個也是webpack4
官方所推薦的。
thread-loader
yarn add -D thread-loader
複製代碼
thread-loader
會將你的 loader
放置在一個 worker
池裏面運行,以達到多線程構建。
❝把這個
❞loader
放置在其餘loader
以前(以下面示例的位置), 放置在這個loader
以後的loader
就會在一個單獨的worker
池(worker pool
)中運行。
module.exports = {
module: { rules: [ { test: /\.js$/, include: path.resolve("src"), use: [ "thread-loader", // your expensive loader (e.g babel-loader) ] } ] } } 複製代碼
yarn add -D happypack
複製代碼
HappyPack
可讓 Webpack
同一時間處理多個任務,發揮多核 CPU
的能力,將任務分解給多個子進程去併發的執行,子進程處理完後,再把結果發送給主進程。經過多進程模型,來加速代碼構建。
// webpack.config.js
const HappyPack = require('happypack'); exports.module = { rules: [ { test: /.js$/, // 1) replace your original list of loaders with "happypack/loader": // loaders: [ 'babel-loader?presets[]=es2015' ], use: 'happypack/loader', include: [ /* ... */ ], exclude: [ /* ... */ ] } ] }; exports.plugins = [ // 2) create the plugin: new HappyPack({ // 3) re-add the loaders you replaced above in #1: loaders: [ 'babel-loader?presets[]=es2015' ] }) ]; 複製代碼
這裏有一點須要說明的是,HappyPack
的做者表示已再也不維護此項目,這個能夠在github
倉庫看到: 做者也是推薦使用webpack
官方提供的thread-loader
。
❝❞
thread-loader
和happypack
對於小型項目來講打包速度幾乎沒有影響,甚至可能會增長開銷,因此建議儘可能在大項目中採用。
一般咱們在開發環境,代碼構建時間比較快,而構建用於發佈到線上的代碼時會添加壓縮代碼這一流程,則會致使計算量大耗時多。
webpack
默認提供了UglifyJS
插件來壓縮JS
代碼,可是它使用的是單線程壓縮代碼,也就是說多個js
文件須要被壓縮,它須要一個個文件進行壓縮。因此說在正式環境打包壓縮代碼速度很是慢(由於壓縮JS
代碼須要先把代碼解析成用Object
抽象表示的AST
語法樹,再應用各類規則分析和處理AST
,致使這個過程耗時很是大)。
因此咱們要對壓縮代碼這一步驟進行優化,經常使用的作法就是多進程並行壓縮。
目前有三種主流的壓縮方案:
parallel-uglify-plugin
上面介紹的HappyPack
的思想是使用多個子進程去解析和編譯JS
,CSS
等,這樣就能夠並行處理多個子任務,多個子任務完成後,再將結果發到主進程中,有了這個思想後,ParallelUglifyPlugin
插件就產生了。
當webpack
有多個JS
文件須要輸出和壓縮時,原來會使用UglifyJS
去一個個壓縮而且輸出,而ParallelUglifyPlugin
插件則會開啓多個子進程,把對多個文件壓縮的工做分給多個子進程去完成,可是每一個子進程仍是經過UglifyJS
去壓縮代碼。並行壓縮能夠顯著的提高效率。
yarn add -D webpack-parallel-uglify-plugin
複製代碼
import ParallelUglifyPlugin from 'webpack-parallel-uglify-plugin';
module.exports = { plugins: [ new ParallelUglifyPlugin({ // Optional regex, or array of regex to match file against. Only matching files get minified. // Defaults to /.js$/, any file ending in .js. test, include, // Optional regex, or array of regex to include in minification. Only matching files get minified. exclude, // Optional regex, or array of regex to exclude from minification. Matching files are not minified. cacheDir, // Optional absolute path to use as a cache. If not provided, caching will not be used. workerCount, // Optional int. Number of workers to run uglify. Defaults to num of cpus - 1 or asset count (whichever is smaller) sourceMap, // Optional Boolean. This slows down the compilation. Defaults to false. uglifyJS: { // These pass straight through to uglify-js@3. // Cannot be used with uglifyES. // Defaults to {} if not neither uglifyJS or uglifyES are provided. // You should use this option if you need to ensure es5 support. uglify-js will produce an error message // if it comes across any es6 code that it can't parse. }, uglifyES: { // These pass straight through to uglify-es. // Cannot be used with uglifyJS. // uglify-es is a version of uglify that understands newer es6 syntax. You should use this option if the // files that you're minifying do not need to run in older browsers/versions of node. } }), ], }; 複製代碼
❝❞
webpack-parallel-uglify-plugin
已再也不維護,這裏不推薦使用
uglifyjs-webpack-plugin
yarn add -D uglifyjs-webpack-plugin
複製代碼
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = { plugins: [ new UglifyJsPlugin({ uglifyOptions: { warnings: false, parse: {}, compress: {}, ie8: false }, parallel: true }) ] }; 複製代碼
其實它和上面的parallel-uglify-plugin
相似,也可經過設置parallel: true
開啓多進程壓縮。
terser-webpack-plugin
不知道你有沒有發現:webpack4
已經默認支持 ES6
語法的壓縮。
而這離不開terser-webpack-plugin
。
yarn add -D terser-webpack-plugin
複製代碼
const TerserPlugin = require('terser-webpack-plugin');
module.exports = { optimization: { minimize: true, minimizer: [ new TerserPlugin({ parallel: 4, }), ], }, }; 複製代碼
在使用webpack
進行打包時候,對於依賴的第三方庫,好比vue
,vuex
等這些不會修改的依賴,咱們可讓它和咱們本身編寫的代碼分開打包,這樣作的好處是每次更改我本地代碼的文件的時候,webpack
只須要打包我項目自己的文件代碼,而不會再去編譯第三方庫。
那麼第三方庫在第一次打包的時候只打包一次,之後只要咱們不升級第三方包的時候,那麼webpack
就不會對這些庫去打包,這樣的能夠快速的提升打包的速度。其實也就是預編譯資源模塊
。
webpack
中,咱們能夠結合DllPlugin
和 DllReferencePlugin
插件來實現。
DllPlugin
是什麼?它能把第三方庫代碼分離開,而且每次文件更改的時候,它只會打包該項目自身的代碼。因此打包速度會更快。
DLLPlugin
插件是在一個額外獨立的webpack
設置中建立一個只有dll
的bundle
,也就是說咱們在項目根目錄下除了有webpack.config.js
,還會新建一個webpack.dll.js
文件。
webpack.dll.js
的做用是把全部的第三方庫依賴打包到一個bundle
的dll
文件裏面,還會生成一個名爲 manifest.json
文件。該manifest.json
的做用是用來讓 DllReferencePlugin
映射到相關的依賴上去的。
DllReferencePlugin
又是什麼?這個插件是在webpack.config.js
中使用的,該插件的做用是把剛剛在webpack.dll.js
中打包生成的dll
文件引用到須要的預編譯的依賴上來。
什麼意思呢?就是說在webpack.dll.js
中打包後好比會生成 vendor.dll.js
文件和vendor-manifest.json
文件,vendor.dll.js
文件包含了全部的第三方庫文件,vendor-manifest.json
文件會包含全部庫代碼的一個索引,當在使用webpack.config.js
文件打包DllReferencePlugin
插件的時候,會使用該DllReferencePlugin
插件讀取vendor-manifest.json
文件,看看是否有該第三方庫。
vendor-manifest.json
文件就是一個第三方庫的映射而已。
上面說了這麼多,主要是爲了方便你們對於預編譯資源模塊
和DllPlugin
和、DllReferencePlugin
插件做用的理解(我第一次使用看了很久才明白~~)
先來看下完成的項目目錄結構:
主要在兩塊配置,分別是webpack.dll.js
和webpack.config.js
(對應這裏我是webpack.base.js
)
webpack.dll.js
const path = require('path');
const webpack = require('webpack'); module.exports = { mode: 'production', entry: { vendors: ['lodash', 'jquery'], react: ['react', 'react-dom'] }, output: { filename: '[name].dll.js', path: path.resolve(__dirname, './dll'), library: '[name]' }, plugins: [ new webpack.DllPlugin({ name: '[name]', path: path.resolve(__dirname, './dll/[name].manifest.json') }) ] } 複製代碼
這裏我拆了兩部分:vendors
(存放了lodash
、jquery
等)和react
(存放了 react 相關的庫,react
、react-dom
等)
webpack.config.js
(對應我這裏就是webpack.base.js
)const path = require("path");
const fs = require('fs'); // ... const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin'); const webpack = require('webpack'); const plugins = [ // ... ]; const files = fs.readdirSync(path.resolve(__dirname, './dll')); files.forEach(file => { if(/.*\.dll.js/.test(file)) { plugins.push(new AddAssetHtmlWebpackPlugin({ filepath: path.resolve(__dirname, './dll', file) })) } if(/.*\.manifest.json/.test(file)) { plugins.push(new webpack.DllReferencePlugin({ manifest: path.resolve(__dirname, './dll', file) })) } }) module.exports = { entry: { main: "./src/index.js" }, module: { rules: [] }, plugins, output: { // publicPath: "./", path: path.resolve(__dirname, "dist") } } 複製代碼
這裏爲了演示省略了不少代碼,項目完整代碼在這裏
因爲上面我把第三方庫作了一個拆分,因此對應生成也就會是多個文件,這裏讀取了一下文件,作了一層遍歷。
最後在package.json
裏面再添加一條腳本就能夠了:
"scripts": {
"build:dll": "webpack --config ./webpack.dll.js", }, 複製代碼
運行yarn build:dll
就會生成本小節開頭貼的那張項目結構圖了~
通常來講,對於靜態資源,咱們都但願瀏覽器可以進行緩存,那樣之後進入頁面就能夠直接使用緩存資源,頁面打開速度會顯著加快,既提升了用戶的體驗也節省了寬帶資源。
固然瀏覽器緩存方法有不少種,這裏只簡單討論下在webpack
中如何利用緩存來提高二次構建速度。
在webpack
中利用緩存通常有如下幾種思路:
babel-loader
開啓緩存
cache-loader
hard-source-webpack-plugin
babel-loader
babel-loader
在執行的時候,可能會產生一些運行期間重複的公共文件,形成代碼體積冗餘,同時也會減慢編譯效率。
能夠加上cacheDirectory
參數開啓緩存:
{
test: /\.js$/, exclude: /node_modules/, use: [{ loader: "babel-loader", options: { cacheDirectory: true } }], }, 複製代碼
cache-loader
在一些性能開銷較大的 loader
以前添加此 loader
,以將結果緩存到磁盤裏。
yarn add -D cache-loader
複製代碼
cache-loader
的配置很簡單,放在其餘 loader
以前便可。修改Webpack
的配置以下:
// webpack.config.js
module.exports = { module: { rules: [ { test: /\.ext$/, use: [ 'cache-loader', ...loaders ], include: path.resolve('src') } ] } } 複製代碼
❝請注意,保存和讀取這些緩存文件會有一些時間開銷,因此請只對性能開銷較大的
❞loader
使用此loader
。
hard-source-webpack-plugin
HardSourceWebpackPlugin
爲模塊提供了中間緩存,緩存默認的存放路徑是: node_modules/.cache/hard-source
。
配置 hard-source-webpack-plugin
後,首次構建時間並不會有太大的變化,可是從第二次開始,構建時間大約能夠減小 80%
左右。
yarn add -D hard-source-webpack-plugin
複製代碼
// webpack.config.js
var HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); module.exports = { entry: // ... output: // ... plugins: [ new HardSourceWebpackPlugin() ] } 複製代碼
❝❞
webpack5
中會內置hard-source-webpack-plugin
。
有時候咱們的項目中會用到不少模塊,但有些模塊實際上是不須要被解析的。這時咱們就能夠經過縮小構建目標或者減小文件搜索範圍的方式來對構建作適當的優化。
主要是exclude
與 include
的使用:
// webpack.config.js
const path = require('path'); module.exports = { ... module: { rules: [ { test: /\.js$/, exclude: /node_modules/, // include: path.resolve('src'), use: ['babel-loader'] } ] } 複製代碼
這裏babel-loader
就會排除對node_modules
下對應 js
的解析,提高構建速度。
這個主要是resolve
相關的配置,用來設置模塊如何被解析。經過resolve
的配置,能夠幫助Webpack
快速查找依賴,也能夠替換對應的依賴。
resolve.modules
:告訴
webpack
解析模塊時應該搜索的目錄
resolve.mainFields
:當從
npm
包中導入模塊時(例如,
import * as React from 'react'
),此選項將決定在
package.json
中使用哪一個字段導入模塊。根據
webpack
配置中指定的
target
不一樣,默認值也會有所不一樣
resolve.mainFiles
:解析目錄時要使用的文件名,默認是
index
resolve.extensions
:文件擴展名
// webpack.config.js
const path = require('path'); module.exports = { ... resolve: { alias: { react: path.resolve(__dirname, './node_modules/react/umd/react.production.min.js') }, //直接指定react搜索模塊,不設置默認會一層層的搜尋 modules: [path.resolve(__dirname, 'node_modules')], //限定模塊路徑 extensions: ['.js'], //限定文件擴展名 mainFields: ['main'] //限定模塊入口文件名 複製代碼
介紹動態Polyfill
前,咱們先來看下什麼是babel-polyfill
。
babel
只負責語法轉換,好比將ES6
的語法轉換成ES5
。但若是有些對象、方法,瀏覽器自己不支持,好比:
Promise
、
WeakMap
等。
Array.from
、
Object.assign
等。
Array.prototype.includes
等。
此時,須要引入babel-polyfill
來模擬實現這些對象、方法。
這種通常也稱爲墊片
。
babel-polyfill
?使用也很是簡單,在webpack.config.js
文件做以下配置就能夠了:
module.exports = {
entry: ["@babel/polyfill", "./app/js"], }; 複製代碼
動態Polyfill
?babel-polyfill
因爲是一次性所有導入整個polyfill
,因此用起來很方便,但與此同時也帶來了一個大問題:文件很大,因此後續的方案都是針對這個問題作的優化。
來看下打包後babel-polyfill
的佔比: 佔比 29.6%,有點太大了!
介於上述緣由,動態Polyfill
服務誕生了。 經過一張圖來了解下Polyfill Service
的原理:
每次打開頁面,瀏覽器都會向Polyfill Service
發送請求,Polyfill Service
識別 User Agent
,下發不一樣的 Polyfill
,作到按需加載Polyfill
的效果。
動態Polyfill
服務?採用官方提供的服務地址便可:
//訪問url,根據User Agent 直接返回瀏覽器所需的 polyfills
https://polyfill.io/v3/polyfill.min.js 複製代碼
Scope Hoisting
🦁Scope Hoisting
?Scope hoisting
直譯過來就是「做用域提高」。熟悉 JavaScript
都應該知道「函數提高」和「變量提高」,JavaScript
會把函數和變量聲明提高到當前做用域的頂部。「做用域提高」也相似於此,webpack
會把引入的 js
文件「提高到」它的引入者頂部。
Scope Hoisting
可讓 Webpack
打包出來的代碼文件更小、運行的更快。
Scope Hoisting
要在 Webpack
中使用 Scope Hoisting
很是簡單,由於這是 Webpack
內置的功能,只須要配置一個插件,相關代碼以下:
// webpack.config.js
const webpack = require('webpack') module.exports = mode => { if (mode === 'production') { return {} } return { devtool: 'source-map', plugins: [new webpack.optimize.ModuleConcatenationPlugin()], } } 複製代碼
Scope Hoisting
後的對比讓咱們先來看看在沒有 Scope Hoisting
以前 Webpack
的打包方式。
假如如今有兩個文件分別是
constant.js
:
export default 'Hello,Jack-cool';
複製代碼
main.js
:
import str from './constant.js';
console.log(str); 複製代碼
以上源碼用 Webpack
打包後的部分代碼以下:
[
(function (module, __webpack_exports__, __webpack_require__) { var __WEBPACK_IMPORTED_MODULE_0__constant_js__ = __webpack_require__(1); console.log(__WEBPACK_IMPORTED_MODULE_0__constant_js__["a"]); }), (function (module, __webpack_exports__, __webpack_require__) { __webpack_exports__["a"] = ('Hello,Jack-cool'); }) ] 複製代碼
在開啓 Scope Hoisting
後,一樣的源碼輸出的部分代碼以下:
[
(function (module, __webpack_exports__, __webpack_require__) { var constant = ('Hello,Jack-cool'); console.log(constant); }) ] 複製代碼
從中能夠看出開啓 Scope Hoisting
後,函數申明由兩個變成了一個,constant.js
中定義的內容被直接注入到了 main.js
對應的模塊中。 這樣作的好處是:
Scope Hoisting
的實現原理其實很簡單:分析出模塊之間的依賴關係,儘量的把打散的模塊合併到一個函數中去,但前提是不能形成代碼冗餘。 所以只有那些被引用了一次的模塊才能被合併。
❝因爲
❞Scope Hoisting
須要分析出模塊之間的依賴關係,所以源碼必須採用ES6
模塊化語句,否則它將沒法生效。
極客時間 【玩轉 webpack】
1.若是以爲這篇文章還不錯,就幫忙點贊一下吧,讓更多的人也看到~
2.關注公衆號前端森林,按期爲你推送新鮮乾貨好文。
3.特殊階段,帶好口罩,作好我的防禦。
4.添加微信fs1263215592,拉你進技術交流羣一塊兒學習 🍻
本文使用 mdnice 排版