上一篇文章 Tree-Shaking性能優化實踐 - 原理篇 介紹了 tree-shaking 的原理,本文主要介紹 tree-shaking 的實踐
css
webpack2 發佈,宣佈支持tree-shaking,webpack 3發佈,支持做用域提高,生成的bundle文件更小。 再沒有升級webpack以前,增幻想咱們的性能又要大幅提高了,對升級充滿了期待。實際上事實是這樣的html
升級完以後,bundle文件大小並無大幅減小,當時有較大的心理落差,而後去研究了爲何效果不理想,緣由見 Tree-Shaking性能優化實踐 - 原理篇 。前端
優化仍是要繼續的,雖然工具自帶的tree-shaking不能去除太多無用代碼,在去除無用代碼這一方面也仍是有能夠作的事情。咱們從三個方面作裏一些優化。vue
先來看一個問題node
當咱們使用組件庫的時候,import {Button} from 'element-ui',相對於Vue.use(elementUI),已是具備性能意識,是比較推薦的作法,但若是咱們寫成右邊的形式,具體到文件的引用,打包以後的區別是很是大的,以antd爲例,右邊形式bundle體積減小約80%。react
這個引用也屬於有反作用,webpack不能把其餘組件進行tree-shaking。既然工具自己是作不了,那咱們能夠作工具把左邊代碼自動改爲右邊代碼這種形式。這個工具antd庫自己也是提供的。我在antd的工具基礎上作了少許的修改,不用任何配置,原生支持咱們本身的組件庫, wui 和 xcui 以及一些其餘經常使用的庫webpack
babel-plugin-import-fix ,縮小引用範圍git
下面介紹一下原理github
這是一個babel的插件,babel經過核心babylon將ES6代碼轉換成AST抽象語法樹,而後插件遍歷語法樹找出相似import {Button} from 'element-ui'這樣的語句,進行轉換,最後從新生成代碼。web
babel-plugin-import-fix默認支持antd,element,meterial-UI,wui,xcui和d3,只須要再.babelrc中配置插件自己就能夠。
.babelrc
{ "presets": [ ["es2015", { "modules": false }], "react" ], "plugins": ["import-fix"] } 複製代碼
實際上是想把全部經常使用的庫都默認支持,但不少經常使用的庫卻不支持縮小引用範圍。由於沒有獨立輸出各個子模塊,不能把引用修改成對單個子模塊的引用。
咱們前面所說的tree-shaking都是針對js文件,經過靜態分析,儘量消除無用的代碼,那對於css咱們能作tree-shaking嗎?
隨着CSS3,LESS,SASS等各類css預處理語言的普及,css文件在整個工程中佔比是不可忽視的。隨着大項目功能的不停迭代,致使css中可能就存在着無用的代碼。我實現了一個webpack插件來解決這個問題,找出css代碼無用的代碼。
webpack-css-treeshaking-plugin,對css進行tree-shaking
下面介紹一下原理
總體思路是這樣的,遍歷全部的css文件中的selector選擇器,而後去全部js代碼中匹配,若是選擇器沒有在代碼出現過,則認爲該選擇器是無用代碼。
首先面臨的問題是,如何優雅的遍歷全部的選擇器呢?難道要用正則表達式很苦逼的去匹配分割嗎?
babel是js世界的福星,其實css世界也有利器,那就是postCss。
PostCSS 提供了一個解析器,它可以將 CSS 解析成AST抽象語法樹。而後咱們能寫各類插件,對抽象語法樹作處理,最終生成新的css文件,以達到對css進行精確修改的目的。
總體又是一個webpack的插件,架構圖以下:
主要流程:
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) }) .... } 複製代碼
let tasks = [] styleFiles.forEach((filename) => { const source = compilation.assets[filename].source() let listOpts = { include: '', source: jsContents, //傳入所有js文件 opts: this.options //插件配置選項 } tasks.push(postcss(treeShakingPlugin(listOpts)).process(source).then(result => { let css = result.toString() // postCss處理後的css AST //替換webpack的編譯產物compilation compilation.assets[filename] = { source: () => css, size: () => css.length } return result })) }) 複製代碼
module.exports = postcss.plugin('list-selectors', function (options) { // 從根節點開始遍歷 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 // 對每個規則進行處理 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 處理每個規則核心代碼
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)) { // 調用classInJs判斷是否在JS中出現過 notCache[n.value] = true result = false break } break default: // nothing break } } else { result = false break } } } }) ... }) ... }) } 複製代碼
能夠看到其實我只處理裏 id選擇器和class選擇器,id和class相對來講反作用小,引發樣式異常的可能性相對較小。
判斷css是否再js中出現過,是使用正則匹配。
其實,後續還能夠繼續優化,好比對tag類的選擇器,能夠配置是否再html,jsx,template中出現過,若是出現過,沒有出現過也能夠認爲是無用代碼。
固然,插件能正常工做仍是的有一些前提和約束。咱們能夠在代碼中動態改變css,好比再react和vue中,能夠這麼寫
這樣是比較推薦的方式,選擇器做爲字符或變量名出如今代碼中,下面這樣動態生成選擇器的狀況就會致使匹配失敗
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> } 複製代碼
因此有一個好的編碼規範的約束,插件能更好的工做。
若是webpack打包後的bundle文件中存在着相同的模塊,也屬於無用代碼的一種。也應該被去除掉
首先咱們須要一個能對bundle文件定性分析的工具,能發現問題,能看出優化效果。
webpack-bundle-analyzer這個插件徹底能知足咱們的需求,他能以圖形化的方式展現bundle中全部的模塊的構成的各構成的大小。
其次,需求對通用模塊進行提取,CommonsChunkPlugin是最被人熟知的用於提供通用模塊的插件。早期的時候,我並不徹底瞭解他的功能,並無發揮最大的功效。
下面介紹CommonsChunkPlugin的正確用法
自動提取全部的node_moudles或者引用次數兩次以上的模塊
minChunks能夠接受一個數值或者函數,若是是函數,可自定義打包規則
但使用上面記載的配置以後,並不能高枕無憂。由於這個配置只能提取全部entry打包後的文件中的通用模塊。而現實是,有了提升性能,咱們會按需加載,經過webpack提供的import(...)方法,這種按需加載的文件並不會存在於entry之中,因此按需加載的異步模塊中的通用模塊並無提取。
如何提取按需加載的異步模塊裏的通用模塊呢?
配置另外一個CommonsChunkPlugin,添加async屬性,async能夠接受布爾值或字符串。當時字符串時,默認是輸出文件的名稱。
names是全部異步模塊的名稱
這裏還涉及一個給異步模塊命名的知識點。我是這樣作的:
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); }) }; 複製代碼
沒錯,在import裏添加註釋。/* webpackChunkName: "EditPage" */ ,雖然看着不舒服,可是管用。
貼一個項目的優化效果對比圖
優化效果仍是比較明顯。
最後思考一個問題:
不一樣entry模塊或按需加載的異步模塊需不須要提取通用模塊?
這個須要看場景了,好比模塊都是在線加載的,若是通用模塊提取粒度太小,會致使首頁首屏須要的文件變多,不少多是首屏用不到的,致使首屏過慢,二級或三級頁面加載會大幅提高。因此這個就須要根據業務場景作權衡,控制通用模塊提取的粒度。
百度外賣的移動端應用場景是這樣的,咱們全部的移動端頁面都作了離線化的處理。離線以後,加載本地的js文件,與網絡無關,基本上能夠忽略文件大小,因此更關注整個離線包的大小。離線包越小,耗費用戶的流量就越小,用戶體驗更好,因此離線化的場景是很是適合最小粒提取通用模塊的,即將全部entry模塊和異步加載模塊的引用大於2的模塊都提取,這樣能得到最小的輸出文件,最小的離線包。
1月20日,我將在掘金分享《百度外賣前端離線化實踐》,有興趣的能夠關注一下。
文本提到的插件都是開源的,連接彙總,歡迎交流,歡迎戳❤
lin-xi/babel-plugin-import-fix