Tree-Shaking性能優(yōu)化實(shí)踐 - 實(shí)踐2篇

三. 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ǔ)上做了少量的修改阅束,不用任何配置,原生支持我們自己的組件庫茄唐, wuixcui 以及一些其他常用的庫

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的模塊都提取亥鬓,這樣能獲得最小的輸出文件,最小的離線包域庇。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末嵌戈,一起剝皮案震驚了整個(gè)濱河市覆积,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌熟呛,老刑警劉巖技健,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異惰拱,居然都是意外死亡雌贱,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門偿短,熙熙樓的掌柜王于貴愁眉苦臉地迎上來欣孤,“玉大人,你說我怎么就攤上這事昔逗〗荡” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵勾怒,是天一觀的道長(zhǎng)婆排。 經(jīng)常有香客問我,道長(zhǎng)笔链,這世上最難降的妖魔是什么段只? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮鉴扫,結(jié)果婚禮上赞枕,老公的妹妹穿的比我還像新娘。我一直安慰自己坪创,他們只是感情好炕婶,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著莱预,像睡著了一般柠掂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上依沮,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天涯贞,我揣著相機(jī)與錄音,去河邊找鬼悉抵。 笑死肩狂,一個(gè)胖子當(dāng)著我的面吹牛摘完,可吹牛的內(nèi)容都是我干的姥饰。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼孝治,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼列粪!你這毒婦竟也來了审磁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤岂座,失蹤者是張志新(化名)和其女友劉穎态蒂,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體费什,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡方援,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年耸棒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡坚俗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出镣衡,到底是詐尸還是另有隱情生蚁,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布巡球,位于F島的核電站言沐,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏酣栈。R本人自食惡果不足惜险胰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望矿筝。 院中可真熱鬧鸯乃,春花似錦、人聲如沸跋涣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽陈辱。三九已至奖年,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間沛贪,已是汗流浹背陋守。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留利赋,地道東北人水评。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像媚送,于是被迫代替她去往敵國(guó)和親中燥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容