深入淺出Webpack 摘要 優(yōu)化

.babelrc 文件
{
    "plugins": [
        [
            "transform-runtime",
            {
                "polyfill": false
            }
        ]
    ],
    "presets": [
        [
            "es2015",
            {
                "modules": false
            }
        ],
        "stage-2",
        "react"
    ]
}

以上配置文件里的transform-runtime 對(duì)應(yīng)的插件全名叫作babel-plugin-transform-runtime ,即在前面加上了babel-plugin- 。要讓Babel 正常運(yùn)行,我們必須先安裝這個(gè)插件,是Babel 官方提供的一個(gè)插件,作用是減少冗余的代碼。Babel 在將ES6 代碼轉(zhuǎn)換成ES5 代碼時(shí),通常需要一些由ES5 編寫(xiě)的輔助函數(shù)來(lái)完成新語(yǔ)法的實(shí)現(xiàn),例如在轉(zhuǎn)換class extent 語(yǔ)法時(shí)會(huì)在轉(zhuǎn)換后的ES5 代碼里注入extent 輔助函數(shù)用于實(shí)現(xiàn)繼承。

這會(huì)導(dǎo)致每個(gè)使用class extent 語(yǔ)法的文件都被注入重復(fù)的extent輔助函數(shù)代碼,babel-plugin-transform-runtime 的作用在于將原本注入JavaScript 文件里的輔助函數(shù)替換成一條導(dǎo)入語(yǔ)句,這樣能減小Babel 編譯出來(lái)的代碼的文件大小。

var extent= require ('babel-runtime/helpers/extent');

由于babel-plugin-transform-runtime 注入了require ('babel-runtime/helpers/extent')語(yǔ)句到編譯后的代碼里,需要安裝babel-runtime
依賴到我們的項(xiàng)目后,代碼才能正常運(yùn)行。也就是說(shuō)babel-plugin-transform-runtime和babel-runtime 需要配套使用,在使用babel-plugin-transform- runtime后一定需要使用babel-runtime

presets 屬性告訴Babel 要轉(zhuǎn)換的源碼使用了哪些新的語(yǔ)法特性,一個(gè)Presets對(duì)一組新語(yǔ)法的特性提供了支持,多個(gè)Presets 可以疊加

縮小文件的搜索范圍

在導(dǎo)入模塊時(shí)

  • 根據(jù)導(dǎo)入語(yǔ)句去尋找對(duì)應(yīng)的要導(dǎo)入的文件。例如require('react')導(dǎo)入語(yǔ)句對(duì)應(yīng)的文件是./node_modules/react/react.js, require('./util')對(duì)應(yīng)的文件是./util.js

  • 根據(jù)找到的要導(dǎo)入的文件的后綴,使用配置中的Loader 去處理文件。

優(yōu)化Loader 配置

由于Loader 對(duì)文件的轉(zhuǎn)換操作很耗時(shí),所以需要讓盡可能少的文件被Loader 處理??梢酝ㄟ^(guò)test 、include 、exclude 三個(gè)配置項(xiàng)來(lái)命中Loader 要應(yīng)用規(guī)則的文件。為了盡可能少地讓文件被Loader 處理,可以通過(guò)include 去命中只有哪些文件需要被處理。

優(yōu)化resolve.modules 配置

resolve.modules 的默認(rèn)值是['node_modules'],含義是先去當(dāng)前目錄的./node_modules目錄下去找我們想找的模塊,如果沒(méi)找到, 就去上一級(jí)目錄../node_modules中找,再?zèng)]有就去../../node_modules 中找,以此類推, 這和Node.js 的模塊尋找機(jī)制很相似。

可以配置resolve.modules指明存放第三方模塊的絕對(duì)路徑,以減少尋找

modules: [path.resolve(__dirname ,'node_modules')]

優(yōu)化resolve.alias 配置

該配置項(xiàng)通過(guò)別名來(lái)將原導(dǎo)入路徑映射成一個(gè)新的導(dǎo)入路徑。

在默認(rèn)情況下, Webpack 會(huì)從入口文件./node_modules/react/react.js 開(kāi)始遞歸解析和處理依賴的幾十個(gè)文件,這會(huì)是一個(gè)很耗時(shí)的操作。通過(guò)配置resolve.alias,可以讓W(xué)ebpack 在處理React 庫(kù)時(shí),直接使用單獨(dú)、完整的react.min.js 文件,從而跳過(guò)耗時(shí)的遞歸解析操作。

alias: {
    'react': path.resolve(__dirname, './node_modules/react/dist/react.min.js')
}

// 大多數(shù)庫(kù)被發(fā)布到Npm 倉(cāng)庫(kù)中時(shí)都會(huì)包含打包好的完整文件,對(duì)于這些庫(kù),也可以對(duì)它們配置alias

但是,對(duì)某些庫(kù)使用本優(yōu)化方法后,會(huì)影響到后面要講的使用Tree-Sharking 去除無(wú)效代碼的優(yōu)化,因?yàn)檫@樣引入進(jìn)來(lái)的是打包好的完整文件,其中有部分代碼在我們的項(xiàng)目中可能永遠(yuǎn)用不上。一般對(duì)整體性比較強(qiáng)的庫(kù)采用本方法優(yōu)化,因?yàn)橥暾募械拇a是一個(gè)整體,每一行都是不可或缺的。但是對(duì)于一些工具類的庫(kù)如lodash(https://github.com/lodash/lodash) ,我們的項(xiàng)目中可能只用到了其中幾個(gè)工具函數(shù),就不能使用本方法去優(yōu)化了,因?yàn)檫@會(huì)導(dǎo)致在我們的輸出代碼中包含很多永遠(yuǎn)不會(huì)被執(zhí)行的代碼

優(yōu)化resolve.extensions 配置

在導(dǎo)入語(yǔ)句沒(méi)帶文件后綴時(shí), Webpack 會(huì)在自動(dòng)帶上后綴后去嘗試詢問(wèn)文件是否存在。如果這個(gè)列表越長(zhǎng),或者正確的后綴越往后,就會(huì)造成嘗試的次數(shù)越多,所以resolve .extensions 的配置也會(huì)影響到構(gòu)建的性能。在配置resolve.extensions時(shí)需要遵守以下幾點(diǎn),以做到盡可能地優(yōu)化構(gòu)建性能。

  • 后綴嘗試列表要盡可能小,不要將項(xiàng)目中不可能存在的情況寫(xiě)到后綴嘗試列表中。 頻率出現(xiàn)最高的文件后綴要優(yōu)先放在最前面,以做到盡快退出尋找過(guò)程。

  • 在源碼中寫(xiě)導(dǎo)入語(yǔ)句時(shí),要盡可能帶上后綴, 從而可以避免尋找過(guò)程

優(yōu)化module. noParse 配置

module.noParse 配置項(xiàng)可以讓W(xué)ebpack 忽略對(duì)部分沒(méi)采用模塊化的文件的遞歸解析處理,這樣做的好處是能提高構(gòu)建性能。

如jQuery、ChartJS龐大又沒(méi)有采用模塊化標(biāo)準(zhǔn),讓W(xué)ebpack 解析這些文件既耗時(shí)又沒(méi)有意義。

還有就是優(yōu)化resolve.alias 配置時(shí),單獨(dú)、完整的react.min.js 文件沒(méi)有采用模塊化,也通過(guò)配置module.noParse 忽略對(duì)react.min.js 文件的遞歸解析處理

module.exports = {
    module: {
        noParse: [/react\.min\.js$/, /jquery|chartjs/ ]
    }
}

注意,被忽略的文件里不應(yīng)該包含 import、require、define 等模塊化 語(yǔ)句,不然會(huì)導(dǎo)致在構(gòu)建出的代碼中包含無(wú)法在瀏覽器環(huán)境下執(zhí)行的模塊化語(yǔ)句。

Tree-Shaking

TreeShaking可以用來(lái)剔除JavaScript中用不上的死代碼。它依賴靜態(tài)的ES6模塊化語(yǔ)法,例如通過(guò)import和export導(dǎo)入、導(dǎo)出。

因?yàn)镋S6模塊化語(yǔ)法是靜態(tài)的(在導(dǎo)入、導(dǎo)出語(yǔ)句中的路徑必須是靜態(tài)的字符串,而且不能放入其他代碼塊中),這讓W(xué)ebpack可以簡(jiǎn)單地分析出哪些export的被import了。如果采用了ES5中的模塊化,例如module.export={...}、require(x+y)、if(x){require('./util')},則Webpack無(wú)法分析出可以剔除哪些代碼

為了將采用'ES6'模塊化的代碼提交給'Webpack,需要配置'Babel'以讓其保留'ES6'模塊化語(yǔ)句。修改'.babelrc'文件如下:

{
  "presets": [
    [
      "env",
      {
        "modules": false
        // "modules": false 的含義是關(guān)閉Babel的模塊轉(zhuǎn)換功能,保留原本的ES6模塊化語(yǔ)法。
      }
    ]
  ]
}

此時(shí)重新運(yùn)行webpack(攜帶參數(shù) --display-used-exports),會(huì)發(fā)現(xiàn)webpack能夠正確分析出哪些代碼被使用了,但是輸出的bundle.js文件中,哪些沒(méi)被使用的代碼依然存在。

要剔除用不上的代碼, 則還得經(jīng)過(guò) UglifyJS 處理一遍??墒褂肬glifyJSPlugin插件,也可以在啟動(dòng)webpack的時(shí)候帶上參數(shù) --optimize-minimize。

綜上,啟動(dòng)webpack時(shí)候使用命令: webpack --display-used-exports --optimize-minimize,就可以發(fā)現(xiàn)Tree-Shaking生效了,用不上的代碼被剔除了。

注意點(diǎn)

在項(xiàng)目中使用大量的第三方庫(kù)時(shí),我們會(huì)發(fā)現(xiàn)TreeShaking似乎不生效了,原因是大部分Npm中的代碼都采用了CommonJS語(yǔ)法,這導(dǎo)致TreeShaking無(wú)法正常工作而降級(jí)處理。但幸運(yùn)的是,有些庫(kù)考慮到了這一點(diǎn),這些庫(kù)在發(fā)布到Npm上時(shí)會(huì)同時(shí)提供兩份代碼,一份采用CommonJS模塊化語(yǔ)法,一份采用ES6模塊化語(yǔ)法。并且在package.json文件中分別指出這兩份代碼的入口。

{
  "main":"lib/index.j5",//指明采用 CommonJS模塊化的代碼入口
  "jsnext:main": "es/index.js" //指明采用 ES6模塊化的代碼入口
}

配置webpack中的 resolve.mainFields,用于配置采用哪個(gè)字段作為模塊的入口描述。

module.exports = {
  ...,
  resolve: {
    mainFields: ['jsnext:main','browser','main']
    // 優(yōu)先使用'jsnext:main'作為入口
  }
}

雖然并不是每個(gè)Npm中的第三方模塊都會(huì)提供ES6模塊化語(yǔ)法的代碼,但對(duì)于己提供了的代碼要盡量?jī)?yōu)化,目前越來(lái)越多的 Npm 中的第三方模塊都考慮到了 Tree Shaking, 并對(duì)其提供了支持。

抽取公共代碼

大型網(wǎng)站通常由多個(gè)頁(yè)面組成,每個(gè)頁(yè)面都是一個(gè)獨(dú)立的單頁(yè)應(yīng)用。但由于所有頁(yè)面都采用同樣的技術(shù)枝及同一套樣式代碼,就導(dǎo)致這些頁(yè)面之間有很多相同的代碼。如果每個(gè)頁(yè)面的代碼都將這些公共的部分包含進(jìn)去,則會(huì)造成以下問(wèn)題:

  • 相同的資源被重復(fù)加載,浪費(fèi)用戶的流量和服務(wù)器的成本。
  • 每個(gè)頁(yè)面需要加載的資源太大,導(dǎo)致網(wǎng)頁(yè)首屏加載緩慢, 影響用戶體驗(yàn)。

提取公共代碼就可以解決這些問(wèn)題。

如何提取公共代碼

  • 根據(jù)網(wǎng)站所使用的技術(shù)棧,找出網(wǎng)站的所有頁(yè)面都需要用到的基礎(chǔ)庫(kù),以采用React技術(shù)枝的網(wǎng)站為例,所有頁(yè)面都會(huì)依賴react、react-dom等庫(kù),將它們提取到一個(gè)單獨(dú)的文件base.js中,該文件包含了所有網(wǎng)頁(yè)的基礎(chǔ)運(yùn)行環(huán)境。

  • 在剔除了各個(gè)頁(yè)面中被base.js包含的部分代碼后,再找出所有頁(yè)面都依賴的公共部分的代碼,將它們提取出來(lái)并放到common.js中。

  • 再為每個(gè)網(wǎng)頁(yè)都生成一個(gè)單獨(dú)的文件,在這個(gè)文件中不再包含base.js和common.js中包含的部分,而只包含各個(gè)頁(yè)面單獨(dú)需要的部分代碼。

配置webpack

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');

module.exports = {
  entry: {
    base: './base.js',
    a: './a.js',
    b: './b.js',
  },
  plugin: [
    new CommonsChunkPlugin({
      // 從 哪些chunk中抽取
      chunks: ['a', 'b'],
      // 抽取的公共部分形成一個(gè)新的chunk
      name: 'common'
    }),

    // 為了從 common 中提取出 base 也包含的部分
    new CommonsChunkPlugin({
      // 從 common 和 base 兩個(gè)現(xiàn)成的 Chunk 中提取公共的部分
      // 因?yàn)閏ommon中必定會(huì)包含基礎(chǔ)庫(kù)的代碼,這兩個(gè)chunk抽取之后,common就會(huì)變小,其中就會(huì)沒(méi)有基礎(chǔ)庫(kù)的代碼了
      chunks: ['common', 'base'],
      // 把公共的部分放到 base 中
      name: 'base'
    }),
  ]
}

// base.js
// 在base.js中引入基礎(chǔ)庫(kù),然后作為入口生成單獨(dú)的chunk
// 所有頁(yè)面都依賴的基礎(chǔ)庫(kù)
import 'react';
import 'react-dom';
// 所有頁(yè)面都使用的樣式
import './base.css';

打包后生成a.js 、b.js 、common.js 、base.js 四個(gè)文件,同時(shí)a,b頁(yè)面不僅要引入自己?jiǎn)为?dú)的js文件,也有引入common.js和base.js。

<!-- 基礎(chǔ)庫(kù)代碼 需要按順序引入-->
<script src="base.js"></script>
<!-- a,b公共的代碼 -->
<script src="common.js"></script>
<!-- a單獨(dú)的代碼 -->
<script src="a.js"></script>

<!-- 這樣先訪問(wèn)a 在訪問(wèn)b的時(shí)候,就只需請(qǐng)求b.js了 -->
<!-- common 和 base 已經(jīng)被緩存 -->

minChunks

CommonsChunkPlugin提供了一個(gè)選項(xiàng)minChunks,表示文件要被提取出來(lái)時(shí)需要在指定的Chunks中出現(xiàn)的最小次數(shù)。假如minChunks=2、chunks=['a','b','c','d'],則任何一個(gè)文件只要在['a','b','c','d']中兩個(gè)以上的Chunk中都出現(xiàn)過(guò),這個(gè)文件就會(huì)被提取出來(lái)。我們可以根據(jù)自己的需求去調(diào)整minChunks的值,minChunks越小,被提取到common.js中的文件就會(huì)越多,但這也會(huì)導(dǎo)致部分頁(yè)面加載的不相關(guān)的資源越多:minChunks越大,被提取到common.js中的文件就會(huì)越少,但這會(huì)導(dǎo)致common.js變小、效果變?nèi)酢?/p>

根據(jù)各個(gè)頁(yè)面之間的相關(guān)性選取其中的部分頁(yè)面時(shí),可用CommonsChunkPlugin提取這部分被選出的頁(yè)面的公共部分,而不是提取所有頁(yè)面的公共部分,而且這樣的操作可以疊加多次。這樣做的效果會(huì)很好,但缺點(diǎn)是配置復(fù)雜,需要根據(jù)頁(yè)面之間的關(guān)系去思考如何配置,該方法并不通用。

分割代碼以按需加載

Webpack 內(nèi)置了強(qiáng)大的分割代碼的功能去實(shí)現(xiàn)按需加載,實(shí)現(xiàn)起來(lái)非常簡(jiǎn)單。舉個(gè)例子,現(xiàn)在需要做這樣一個(gè)進(jìn)行了按需加載優(yōu)化的網(wǎng)頁(yè)。

  • 網(wǎng)頁(yè)首次加載時(shí)只加載main.js文件,網(wǎng)頁(yè)會(huì)展示一個(gè)按鈕,在main.js文件中只包含監(jiān)聽(tīng)按鈕事件和加載按需加載的代碼。

  • 在按鈕被單擊時(shí)才去加載被分割出去的show.js文件,在加載成功后再執(zhí)行show.js里的函數(shù)。

// main.js

window.document.getElementById('btn').addEventListener('click', function () {
  // 當(dāng)按鈕被點(diǎn)擊后才去加載 show.js 文件,文件加載成功后執(zhí)行文件導(dǎo)出的函數(shù)
  import(/* webpackChunkName: "show" */ './show').then((show) => {
    show('Webpack');
  })
});

其中最關(guān)鍵的一句是: import(/* webpackChunkName: "show" */ './show')

Webpack內(nèi)置了對(duì)import(*)語(yǔ)句的支持,當(dāng)Webpack遇到了類似的語(yǔ)句時(shí)會(huì)這樣,

  • 以./show.js為入口重新生成一個(gè)Chunk;

  • 當(dāng)代碼執(zhí)行到import 所在的語(yǔ)句時(shí)才去加載由Chunk 對(duì)應(yīng)生成的文件:

  • import 返回一個(gè)Promise ,當(dāng)文件加載成功時(shí)可以在Promise 的then 方法中獲取 show.js 導(dǎo)出的內(nèi)容。

/* webpackChunkName :'show' */的含義是為動(dòng)態(tài)生成的Chunk 賦予一個(gè)名稱,以方便我們追蹤和調(diào)試代碼。如果不指定動(dòng)態(tài)生成的Chunk 的名稱,則其默認(rèn)的名稱將會(huì)[id].js ,是在Webpack 3 中引入的新特性,在Webpack 3 之前是無(wú)法為動(dòng)態(tài)生成的Chunk 賦予名稱的。

對(duì)應(yīng)的即是output.chunkFilename配置項(xiàng),專門指定動(dòng)態(tài)生成的chunk在輸出時(shí)的文件名稱。

module.exports = {
    entry: './main.js'
    output: {
        filename: '[name].js',
        chunkFilename: '[name].js'
        // 如果沒(méi)有這一行, 則分割出的代碼的文件名稱將會(huì)是[id].js 
        // 這個(gè)對(duì)應(yīng)的name 對(duì)應(yīng)的是 /* webpackChunkName :'show' */ 中的show
    }
}

Scope Hoisting

Scope Hoisting 作用域提升, webpack3中提出來(lái)的

Scope Hoisting 的實(shí)現(xiàn)原理其實(shí)很簡(jiǎn)單:分析模塊之間的依賴關(guān)系,盡可能將被打散的模塊合并到一個(gè)函數(shù)中,但前提是不能造成代碼元余。因此只有那些被引用了一次的模塊才能被合井。

假如現(xiàn)在有兩個(gè)文件

// util.js

export default 'hello, webpack'

// main.js

import str from './util.js'
console.log(str) ;

webpack打包之后的部分代碼

[
    (function(module, __webpack_exports__, __webpack_require__){
        var __WEBPACK_IMPORTED_MODULE_0_util_js__ = __webpack_require__(1);
        console.log(__WEBPACK_IMPORTED_MODULE_0_util_js__["a"]);
    }),

    (function(module, __webpack_exports__, __webpack_require__){
        __webpack_exports__["a"] = ('hello, webpack')
    }),
]

在開(kāi)啟 Scope Hoisting 之后,同樣的源碼輸出部分代碼如下。

[
    (function(module, __webpack_exports__, __webpack_require__){
        var util = ('hello, webpack');
        console.log(util);
    }),
]

好處:函數(shù)申明由兩個(gè)變成了一個(gè), util. js 中定義的內(nèi)容被直接注入main.js 對(duì)應(yīng)的模塊中。這樣使代碼體積更小,代碼在運(yùn)行時(shí)創(chuàng)建的函數(shù)作用域也會(huì)變少,所以內(nèi)存開(kāi)銷也會(huì)表小。

由于Scope Hoisting 需要分析模塊之間的依賴關(guān)系,因此源碼必須采用ES6 模塊化語(yǔ)句(所以跟上面講到的Tree-Shaking要有類似的配置),不然它將無(wú)法生效。

const ModuleConcatenationPlugin = require ('webpack/lib/optimize/ModuleConcatenationPlugin');

module.exports = {
  ...,
  resolve: {
    mainFields: ['jsnext:main','browser','main']
    // 優(yōu)先使用'jsnext:main'作為入口
  },
  plugins: [
    //  開(kāi)啟 Scope Hoisting
    new ModuleConcatenationPlugin();
  ]
}
輸出分析工具

在使用webpack時(shí)帶上兩個(gè)參數(shù)

webpack --profile --json > stats.json

profile 記錄構(gòu)建中的耗時(shí)信息
json 會(huì)輸出一個(gè)json文件,這個(gè)文件包含所有構(gòu)建相關(guān)的信息

webpack --profile --json會(huì)輸出字符串形式的json,webpack --profile --json > stats.json ,最后添加的部分是
UNIX/Linux 系統(tǒng)中的管道命令,將輸出的內(nèi)容通過(guò)管道輸出到stats.json文件中。

http://webpack.github.io/analyse

官網(wǎng)提供的在線web應(yīng)用,將生產(chǎn)的json文件上傳上去就能看到可視化的打包分析。需要注意的是,json文件要?jiǎng)h除前面部分無(wú)效的內(nèi)容。

webpack-bundle-analyzer

可視化分析工具,一個(gè)插件

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}
// 構(gòu)建完成自動(dòng)打開(kāi)一個(gè)分析頁(yè)面
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 目錄第1章 webpack簡(jiǎn)介 11.1 webpack是什么? 11.2 官網(wǎng)地址 21.3 為什么使用 web...
    lemonzoey閱讀 1,821評(píng)論 0 1
  • 全局安裝webpack 全局安裝webpack會(huì)有個(gè)問(wèn)題,就是當(dāng)你有兩個(gè)項(xiàng)目依賴于不同版本的webpack,就會(huì)有...
    説好的妹紙呢閱讀 1,959評(píng)論 0 11
  • /*生成環(huán)境配置文件:不需要一些dev-tools,dev-server和jshint校驗(yàn)等。和開(kāi)發(fā)有關(guān)的東西刪掉...
    David_Sam閱讀 1,132評(píng)論 0 1
  • 中午吃飯的路上 同事說(shuō)今天咱倆來(lái)了剛好一個(gè)月了 對(duì)啊 一月二十五號(hào)來(lái)入職報(bào)道的 雖然隔了春節(jié) 還是覺(jué)得上班蠻久了 ...
    cindy幸福在路上閱讀 369評(píng)論 0 0
  • 行動(dòng)營(yíng)的報(bào)名表上我曾經(jīng)這樣寫(xiě)過(guò),我進(jìn)入行動(dòng)營(yíng)想看看有趣的靈魂是怎樣生活的。 8月14日,早上起來(lái)看到群里孫小米大大...
    zhangdini閱讀 920評(píng)論 7 10

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