.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è)面