三. tree-shaking實(shí)踐
[圖片上傳中...(image-c050ec-1566907647861-15)]
webpack2 發(fā)布,宣布支持tree-shaking,webpack 3發(fā)布贰锁,支持作用域提升诅迷,生成的bundle文件更小教翩。 再?zèng)]有升級(jí)webpack之前逮刨,增幻想我們的性能又要大幅提升了秽晚,對(duì)升級(jí)充滿了期待瓦糟。實(shí)際上事實(shí)是這樣的
升級(jí)完之后,bundle文件大小并沒有大幅減少赴蝇,當(dāng)時(shí)有較大的心理落差菩浙,然后去研究了為什么效果不理想,
優(yōu)化還是要繼續(xù)的,雖然工具自帶的tree-shaking不能去除太多無用代碼劲蜻,在去除無用代碼這一方面也還是有可以做的事情陆淀。我們從三個(gè)方面做里一些優(yōu)化。
(1)對(duì)組件庫引用的優(yōu)化
先來看一個(gè)問題
當(dāng)我們使用組件庫的時(shí)候先嬉,import {Button} from 'element-ui'轧苫,相對(duì)于Vue.use(elementUI),已經(jīng)是具有性能意識(shí)疫蔓,是比較推薦的做法含懊,但如果我們寫成右邊的形式,具體到文件的引用衅胀,打包之后的區(qū)別是非常大的岔乔,以antd為例,右邊形式bundle體積減少約80%滚躯。
這個(gè)引用也屬于有副作用雏门,webpack不能把其他組件進(jìn)行tree-shaking。既然工具本身是做不了掸掏,那我們可以做工具把左邊代碼自動(dòng)改成右邊代碼這種形式茁影。這個(gè)工具antd庫本身也是提供的。我在antd的工具基礎(chǔ)上做了少量的修改阅束,不用任何配置,原生支持我們自己的組件庫茄唐, wui和 xcui 以及一些其他常用的庫
babel-plugin-import-fix 息裸,縮小引用范圍
[lin-xi/babel-plugin-import-fix?github.com[圖片上傳中...(image-7a6b94-1566907647859-0)]]
下面介紹一下原理
[圖片上傳中...(image-17dda3-1566907647861-11)]
這是一個(gè)babel的插件,babel通過核心babylon將ES6代碼轉(zhuǎn)換成AST抽象語法樹沪编,然后插件遍歷語法樹找出類似import {Button} from 'element-ui'這樣的語句呼盆,進(jìn)行轉(zhuǎn)換,最后重新生成代碼蚁廓。
babel-plugin-import-fix默認(rèn)支持antd访圃,element,meterial-UI相嵌,wui腿时,xcui和d3,只需要再.babelrc中配置插件本身就可以饭宾。
.babelrc
{
"presets": [
["es2015", { "modules": false }], "react"
],
"plugins": ["import-fix"]
}
[圖片上傳中...(image-16a40a-1566907647861-10)]
其實(shí)是想把所有常用的庫都默認(rèn)支持批糟,但很多常用的庫卻不支持縮小引用范圍。因?yàn)闆]有獨(dú)立輸出各個(gè)子模塊看铆,不能把引用修改為對(duì)單個(gè)子模塊的引用徽鼎。
(2)CSS Tree-shaking
我們前面所說的tree-shaking都是針對(duì)js文件,通過靜態(tài)分析,盡可能消除無用的代碼否淤,那對(duì)于css我們能做tree-shaking嗎悄但?
隨著CSS3,LESS石抡,SASS等各種css預(yù)處理語言的普及檐嚣,css文件在整個(gè)工程中占比是不可忽視的。隨著大項(xiàng)目功能的不停迭代汁雷,導(dǎo)致css中可能就存在著無用的代碼净嘀。我實(shí)現(xiàn)了一個(gè)webpack插件來解決這個(gè)問題,找出css代碼無用的代碼侠讯。
<u style="text-decoration: none; border-bottom: 1px dashed grey;">webpack-css-treeshaking-plugin挖藏,對(duì)css進(jìn)行tree-shaking</u>
[webpack-css-treeshaking-plugin?github.com](https://link.zhihu.com/?target=https%3A//github.com/lin-xi/
下面介紹一下原理
整體思路是這樣的,遍歷所有的css文件中的selector選擇器厢漩,然后去所有js代碼中匹配膜眠,如果選擇器沒有在代碼出現(xiàn)過,則認(rèn)為該選擇器是無用代碼溜嗜。
首先面臨的問題是宵膨,如何優(yōu)雅的遍歷所有的選擇器呢?難道要用正則表達(dá)式很苦逼的去匹配分割嗎炸宵?
babel是js世界的福星辟躏,其實(shí)css世界也有利器,那就是postCss土全。
PostCSS 提供了一個(gè)解析器捎琐,它能夠?qū)?CSS 解析成AST抽象語法樹。然后我們能寫各種插件裹匙,對(duì)抽象語法樹做處理瑞凑,最終生成新的css文件,以達(dá)到對(duì)css進(jìn)行精確修改的目的概页。
[圖片上傳中...(image-c9cd01-1566907647861-9)]
整體又是一個(gè)webpack的插件籽御,架構(gòu)圖如下:
[圖片上傳中...(image-cb1ee5-1566907647861-8)]
主要流程:
- 插件監(jiān)聽webapck編譯完成事件,webpack編譯完成之后惰匙,從compilation中找出所有的css文件和js文件
apply (compiler) {
compiler.plugin('after-emit', (compilation, callback) => {
let styleFiles = Object.keys(compilation.assets).filter(asset => {
return /\.css$/.test(asset)
})
let jsFiles = Object.keys(compilation.assets).filter(asset => {
return /\.(js|jsx)$/.test(asset)
})
....
}
- 將所有的css文件送至postCss處理技掏,找出無用代碼
let tasks = []
styleFiles.forEach((filename) => {
const source = compilation.assets[filename].source()
let listOpts = {
include: '',
source: jsContents, //傳入全部js文件
opts: this.options //插件配置選項(xiàng)
}
tasks.push(postcss(treeShakingPlugin(listOpts)).process(source).then(result => {
let css = result.toString() // postCss處理后的css AST
//替換webpack的編譯產(chǎn)物compilation
compilation.assets[filename] = {
source: () => css,
size: () => css.length
}
return result
}))
})
- postCss 遍歷,匹配项鬼,刪除過程
module.exports = postcss.plugin('list-selectors', function (options) {
// 從根節(jié)點(diǎn)開始遍歷
cssRoot.walkRules(function (rule) {
// Ignore keyframes, which can log e.g. 10%, 20% as selectors
if (rule.parent.type === 'atrule' && /keyframes/.test(rule.parent.name)) return
// 對(duì)每一個(gè)規(guī)則進(jìn)行處理
checkRule(rule).then(result => {
if (result.selectors.length === 0) {
// 選擇器全部被刪除
let log = ' ?? [' + rule.selector + '] shaked, [1]'
console.log(log)
if (config.remove) {
rule.remove()
}
} else {
// 選擇器被部分刪除
let shaked = rule.selectors.filter(item => {
return result.selectors.indexOf(item) === -1
})
if (shaked && shaked.length > 0) {
let log = ' ?? [' + shaked.join(' ') + '] shaked, [2]'
console.log(log)
}
if (config.remove) {
// 修改AST抽象語法樹
rule.selectors = result.selectors
}
}
})
})
checkRule 處理每一個(gè)規(guī)則核心代碼
let checkRule = (rule) => {
return new Promise(resolve => {
...
let secs = rule.selectors.filter(function (selector) {
let result = true
let processor = parser(function (selectors) {
for (let i = 0, len = selectors.nodes.length; i < len; i++) {
let node = selectors.nodes[i]
if (_.includes(['comment', 'combinator', 'pseudo'], node.type)) continue
for (let j = 0, len2 = node.nodes.length; j < len2; j++) {
let n = node.nodes[j]
if (!notCache[n.value]) {
switch (n.type) {
case 'tag':
// nothing
break
case 'id':
case 'class':
if (!classInJs(n.value)) {
// 調(diào)用classInJs判斷是否在JS中出現(xiàn)過
notCache[n.value] = true
result = false
break
}
break
default:
// nothing
break
}
} else {
result = false
break
}
}
}
})
...
})
...
})
}
可以看到其實(shí)我只處理里 id選擇器和class選擇器零截,id和class相對(duì)來說副作用小,引起樣式異常的可能性相對(duì)較小秃臣。
判斷css是否再js中出現(xiàn)過涧衙,是使用正則匹配哪工。
其實(shí),后續(xù)還可以繼續(xù)優(yōu)化弧哎,比如對(duì)tag類的選擇器雁比,可以配置是否再html,jsx撤嫩,template中出現(xiàn)過偎捎,如果出現(xiàn)過,沒有出現(xiàn)過也可以認(rèn)為是無用代碼序攘。
當(dāng)然茴她,插件能正常工作還是的有一些前提和約束。我們可以在代碼中動(dòng)態(tài)改變css程奠,比如再react和vue中丈牢,可以這么寫
[圖片上傳中...(image-2f260-1566907647861-7)]
這樣是比較推薦的方式,選擇器作為字符或變量名出現(xiàn)在代碼中瞄沙,下面這樣動(dòng)態(tài)生成選擇器的情況就會(huì)導(dǎo)致匹配失敗
render(){
this.stateClass = 'state-' + this.state == 2 ? 'open' : 'close'
return <div class={this.stateClass}></div>
}
其中這樣情況很容易避免
render(){
this.stateClass = this.state == 2 ? 'state-open' : 'state-close'
return <div class={this.stateClass}></div>
}
所以有一個(gè)好的編碼規(guī)范的約束己沛,插件能更好的工作。
(3)webpack bundle文件去重
如果webpack打包后的bundle文件中存在著相同的模塊距境,也屬于無用代碼的一種申尼。也應(yīng)該被去除掉
首先我們需要一個(gè)能對(duì)bundle文件定性分析的工具,能發(fā)現(xiàn)問題垫桂,能看出優(yōu)化效果师幕。
webpack-bundle-analyzer這個(gè)插件完全能滿足我們的需求,他能以圖形化的方式展示bundle中所有的模塊的構(gòu)成的各構(gòu)成的大小诬滩。
[圖片上傳中...(image-52ee70-1566907647861-6)]
其次霹粥,需求對(duì)通用模塊進(jìn)行提取,CommonsChunkPlugin是最被人熟知的用于提供通用模塊的插件碱呼。早期的時(shí)候蒙挑,我并不完全了解他的功能宗侦,并沒有發(fā)揮最大的功效愚臀。
下面介紹CommonsChunkPlugin的正確用法
自動(dòng)提取所有的node_moudles或者引用次數(shù)兩次以上的模塊
[圖片上傳中...(image-67d199-1566907647861-5)]
minChunks可以接受一個(gè)數(shù)值或者函數(shù),如果是函數(shù)矾利,可自定義打包規(guī)則
但使用上面記載的配置之后姑裂,并不能高枕無憂。因?yàn)檫@個(gè)配置只能提取所有entry打包后的文件中的通用模塊男旗。而現(xiàn)實(shí)是舶斧,有了提高性能,我們會(huì)按需加載察皇,通過webpack提供的import(...)方法茴厉,這種按需加載的文件并不會(huì)存在于entry之中泽台,所以按需加載的異步模塊中的通用模塊并沒有提取。
如何提取按需加載的異步模塊里的通用模塊呢矾缓?
[圖片上傳中...(image-d3ac04-1566907647861-4)]
配置另一個(gè)CommonsChunkPlugin怀酷,添加async屬性,async可以接受布爾值或字符串嗜闻。當(dāng)時(shí)字符串時(shí)蜕依,默認(rèn)是輸出文件的名稱。
names是所有異步模塊的名稱
這里還涉及一個(gè)給異步模塊命名的知識(shí)點(diǎn)琉雳。我是這樣做的:
const Edit = resolve => { import( /* webpackChunkName: "EditPage" */ './pages/Edit/Edit').then((mod) => { resolve(mod.default); }) };
const PublishPage = resolve => { import( /* webpackChunkName: "Publish" */ './pages/Publish/Publish').then((mod) => { resolve(mod); }) };
const Models = resolve => { import( /* webpackChunkName: "Models" */ './pages/Models/Models').then((mod) => { resolve(mod.default); }) };
const MediaUpload = resolve => { import( /* webpackChunkName: "MediaUpload" */ './pages/Media/MediaUpload').then((mod) => { resolve(mod); }) };
const RealTime = resolve => { import( /* webpackChunkName: "RealTime" */ './pages/RealTime/RealTime').then((mod) => { resolve(mod.default); }) };
沒錯(cuò)样眠,在import里添加注釋。/* webpackChunkName: "EditPage" */ 翠肘,雖然看著不舒服檐束,但是管用。
貼一個(gè)項(xiàng)目的優(yōu)化效果對(duì)比圖
[圖片上傳中...(image-a34f54-1566907647861-3)]
優(yōu)化效果還是比較明顯锯茄。
[圖片上傳中...(image-fbc24d-1566907647861-2)]
<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">優(yōu)化前bundle</figcaption>
[圖片上傳中...(image-fdd117-1566907647861-1)]
<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">優(yōu)化后bundle</figcaption>
最后思考一個(gè)問題:
不同entry模塊或按需加載的異步模塊需不需要提取通用模塊厢塘?
這個(gè)需要看場(chǎng)景了,比如模塊都是在線加載的肌幽,如果通用模塊提取粒度過小晚碾,會(huì)導(dǎo)致首頁首屏需要的文件變多,很多可能是首屏用不到的喂急,導(dǎo)致首屏過慢格嘁,二級(jí)或三級(jí)頁面加載會(huì)大幅提升。所以這個(gè)就需要根據(jù)業(yè)務(wù)場(chǎng)景做權(quán)衡廊移,控制通用模塊提取的粒度糕簿。
百度外賣的移動(dòng)端應(yīng)用場(chǎng)景是這樣的,我們所有的移動(dòng)端頁面都做了離線化的處理狡孔。離線之后懂诗,加載本地的js文件,與網(wǎng)絡(luò)無關(guān)苗膝,基本上可以忽略文件大小殃恒,所以更關(guān)注整個(gè)離線包的大小。離線包越小辱揭,耗費(fèi)用戶的流量就越小离唐,用戶體驗(yàn)更好,所以離線化的場(chǎng)景是非常適合最小粒提取通用模塊的问窃,即將所有entry模塊和異步加載模塊的引用大于2的模塊都提取亥鬓,這樣能獲得最小的輸出文件,最小的離線包域庇。