Webpack 4進階--從前的日色變得慢 ,一下午只夠打一次包

從前的日色變得慢,車,馬,郵件都慢,一輩子只夠愛一我的 -- 《從前慢》css

近期在團隊項目裏把Webpack升級到4.4.1,過程當中發現現存的升級文檔十分有限,踩了很多坑,好在升級以後提高還算顯著,production場景下第三方依賴打包速度提高76%,development場景下本地服務首次啓動提高效果約46%,再次啓動提高效果上升至63%。這裏將此次升級過程當中的點滴分享出來,但願對你們有所幫助。html

理論部分

Webpack 4 發佈以後,議論最多的兩大特性,其一是零配置,其二是速度快(號稱提速上限98%)。聽起來十分美妙,在實地測試以前,首先從理論上分析一下可能性。react

零配置

一言以蔽之,約定優於配置。經過mode屬性將開發/生產(development/production)環境中經常使用的功能設置好默認值,用戶即來即用。webpack

打包速度快

Optimization

Webpack 4取消了四個經常使用的用於性能優化的plugin(UglifyjsWebpackPlugin,CommonsChunkPlugin,ModuleConcatenationPlugin,NoEmitOnErrorsPlugin),轉而提供了一個名爲optimization的配置項,用於接手以上四位的工做。 git

注:UglifyjsWebpackPlugin並不執行tree shaking操做,這裏爲了介紹sideEffects,故而將關係緊密的二者放在一塊兒介紹了

  1. Tree Shaking & Minimize

廢棄插件:UglifyjsWebpackPlugingithub

新增屬性:sideEffects,minimize等web

  • Tree Shaking

Tree shaking一直是一個美麗而高不可攀的話題。影響tree shaking的根本緣由在於side effects(反作用),其中最廣爲人知的一條side effect就是動態引入依賴的問題。得益於ES6的模塊化實現思路,全部的依賴必須位於文件頂部,靜態引入(然而import()的出現打破了這個規則),Webpack能夠在繪製依賴圖的時候進行靜態分析,從而將真正被引用的exports添加到bundle文件中,減小打包體積。然而不少熱度較高的第三方庫爲了考慮兼容性每每採用UMD實現,而其所支持的動態引入依賴的功能則致使真實的依賴圖可能要到運行時才能肯定,使得靜態分析難以發揮真正威力,tree shaking採用了保守策略,致使咱們發現沒有被用到的方法依然出如今了bundle文件中。typescript

一個好消息是許多第三方庫相繼推出了es版,配合tree-shaking食用,口感更佳,這也是官方號稱提速98%的重要前提之一(冷漠臉)。壞消息是ES6其實也提供import()方法支持動態引入依賴,因此如下寫法其實也是徹底行的通的。。。還記得那些年咱們追過的沈佳宜說過的話麼,「人生原本就有不少事情是徒勞無功的啊」。瀏覽器

if(Math.random() > 0.5) {
    import('./a.js').then(() => {
        ...
    })
} else {
    import('./b.js').then(() => {
        ...
    })
}
複製代碼

除此之外,爲了防止用戶不當心修改輸出元素的屬性,有些庫會將最終的輸出元素用Object.freeze方法包裹起來,這也屬於side effects之一,一樣也會對tree shaking產生影響。緩存

回到Webpack 4,官方提供了sideEffects屬性,經過將其設置爲false,能夠主動標識該類庫中的文件只執行簡單輸出,並無執行其餘操做,能夠放心shaking。除了能夠減少bundle文件的體積,同時也可以提高打包速度。爲了檢查side effects,Webpack須要在打包的時候將全部的文件執行一遍。而在設置sideEffects以後,則能夠跳過執行那些未被引用的文件,畢竟已經明確標識了「我是平民」。所以對於一些咱們本身開發的庫,設置sideEffects爲false大有裨益。

  • Minimize

Minimize屬性就沒啥可多說的了,混淆壓縮文件。

  1. Scope hoisting

廢棄插件:ModuleConcatenationPlugin

新增屬性:concatenateModules

//開啓前
[
    /* 0 */
    function(module, exports, require) {
        var module_a = require(1)
        console.log(module_a['default'])
    }
    
    /* 1 */
    function(module, exports, require) {
        exports['default'] = 'module A'
    }
]

//開啓後
[
    function(module, exports, require) {
        var module_a_defaultExport = 'module A'
        console.log(module_a_defaultExport)
    }
]
複製代碼

concatenateModules開啓以後,能夠看出bundle文件中的函數聲明變少了,於是能夠帶來的好處,其一,文件的體積比以前更小了,其二,運行代碼時建立的函數做用域變少了,開銷也隨之變少了。不過scope hoisting的效果一樣也依賴於靜態分析,無奈命不禁我。

  1. Code splitting

廢棄插件:CommonsChunkPlugin

新增屬性:splitChunks,runtimeChunk, occurrenceOrder等

  • splitChunks

splitChunks在Webpack 4裏被用於取代咱們熟悉CommonsChunkPlugin。讀到這裏不知道你有沒有發現其中的端倪,這是否意味着DllPlugin和CommonsChunkPlugin(splitChunks)能夠共存了呢?

在Webpack 4以前,二者並不能一塊兒使用,緣由有二

  • 一個相對沒那麼重要的緣由是DllPlugin服務的目標場景是develop環境,由於第三方依賴(輸出文件暫稱爲vendors)的變動頻率較低,故而在每次啓動本地服務或者rebuild的時候將第三方依賴從新打包一次其實是一種浪費。經過DllPlugin,將第三方依賴的打包過程從業務代碼的打包過程當中獨立出來,能夠大大縮短develop環境下的啓動時間。同時經過設置hash值,也能夠充分的利用瀏覽器對這部分文件的緩存,提高加載效率。而在對加載效率更爲苛刻的production環境,DllPlugin打包出的文件則稍顯笨重,不少重複的內容被屢次打包進了bundle文件。在這種場景下,CommonsChunkPlugin被視爲更好的選擇,由於咱們不須要爲打包時間操心過多,加載效率是咱們惟一須要關注的內容。因此在webpack的開發者看來,這二者如同「I have an apple,I have a pen,Ah~~ Apple pen」同樣,實際上並不存在什麼交集。
  • 所以也引出了兩者不兼容更爲重要的第二個緣由,沒人實現

這塊功能實際上經過CommonsChunkPlugin設置兩個entry point也能夠實現,一個做爲業務代碼的入口,一個做爲vendors的入口。不過存在兩個問題,第一個問題是,儘管vendors被單獨設置了entry point,可是在每次啓動本地服務的時候,儘管打包的結果不變,hash值不變,瀏覽器的緩存文件也被充分利用了,它的打包過程依然會執行,因此啓動時間並不會縮短,第二個問題是,許多人在使用CommonsChunkPlugin的時候並無注意到Webpack會將runtime一塊兒打包進vendors文件,因此每次啓動的時候,儘管你並無修改任何第三方依賴,可是vendors文件的hash值卻變了,致使瀏覽器緩存實際上並無被利用起來。要解決這個問題,須要配置CommonsChunkPlugin將runtime單獨打包成一個文件。

然而到了Webpack 4,在CommonsChunkPlugin變成splitChunks以後,出於某些未知的緣由,二者兼容性的問題被解決了。。。Happy coding。

  • runtimeChunk

runtimeChunk之因此被單獨設置爲一個配置項,應該就是爲了主動幫助用戶避免上文所述的問題吧。

  • occurrenceOrder

occurrenceOrder應用的場景是若是不手動設置chunk的名字,而採用默認值的話,Webpack將會用更短的名字去命名引用頻度更高的chunk。

  • noEmitOnErrors

廢棄插件:NoEmitOnErrorsPlugin

新增屬性:noEmitOnErrors

noEmitOnErrors在編譯出現錯誤時,用來跳過輸出階段。

New Plugin

Webpack 4同時實現了一套新的plugin機制,與性能相關的改進點是消除了對arguments的濫用。如同咱們推崇開發時定義類型,從而能夠避免JIT過程當中產生過多的重載函數,以及下降從新編譯的機率。

實踐部分

講了這麼多,最後分享一下個人實操經歷。Webpack 4爲用戶描繪的場景當然美好,然而帶來便利的同時也給開發者留下了很多麻煩。首當其衝的就是兼容性的問題,不少咱們經常使用的loader,plugin還沒有對此次升級作好準備,找到合適的替代工具以及積極改造自研的工具將成爲升級過程當中一場重要戰役。接下來我會針對在此次項目升級中我所遇到的兼容性問題以及最終採用的解決方案作一個總結,常規的Webpack 4配置能夠在官方demo 中找到答案。

  1. CommonsChunkPlugin + DllPlugin

Nothing special,主要仍是一個分類問題,如何識別存在公共依賴的第三方依賴,並將其分配到不一樣的entry中。例如antd和react都依賴了react,則應該將二者分配到不一樣的entry中。以及如何均勻的分配依賴到不一樣的entry中,使得打包以後的每一個entry大小相近。能夠說十分考驗一名配置工程師的功力和對源碼庫的瞭解程度。

  1. Ts-loader 由於awesome-typescript-loader(ATL)尚未合併支持Webpack 4的pr。因此ts-loader是ts愛好者們目前最好的選擇。曾經ATL之因此可以打敗ts-loader,成爲很多人的選擇,緣由有二,其一是ATL會新開一個獨立的進程執行類型檢查操做,所以不會影響編譯時間,其二是ts的編譯結果會被緩存,rebuild場景下能夠提速。目前ts-loader也已經支持這兩方面功能了,因此替換時並不須要擔憂。
module: {
  rule: {
    test: /\.tsx?$/,
    use: [
      'cache-loader',
      {
        loader: 'thread-loader',
        options: {
          workers: require('os').cpus().length - 1,
        }
      },
      {
        loader: 'ts-loader',
        options: {
          happyPackMode: true,
          transpileOnly: true
        }
      }
    ]
  }
}

plugins: [
  new ForkTsCheckerWebpackPlugin()
]
複製代碼
  • ForkTsCheckerWebpackPlugin用於新建進程執行類型檢查,爲此你須要關閉ts-loader自身的類型檢查功能,即設置transpileOnly爲true。
  • thread-loader容許新建一個worker進程去分擔一些昂貴的loader操做;cache-loader則能夠將loader的運行結果緩存在本地。然而二者同時也會帶來額外的開銷(進程管理,I/O操做),自行評估後使用。
  1. MiniCssExtractPlugin 經過名字不難猜出它的功能,因爲ExtractTextWebpackPlugin尚不支持Webpack 4,並且將來極可能被吸取爲配置項,Mini-css-extract-plugin能夠做爲過渡期的一個選擇。除了常規的css抽取合併功能外,它還會在合併時清理重複的css副本,而這也是ExtractTextWebpackPlugin還沒有實現的功能,因此理論上css的打包效果更優。
  2. InlineChunkWebpackPlugin(Webpack 4還沒有支持) 雖然Webpack 4還沒有支持這個插件,但仍是把它加在了這裏,只是由於它確實有用。上文說到經過配置runtimeChunk爲true,能夠將運行時打包成獨立的chunk,然而這個chunk體積很小,單獨佔用一個http請求稍顯浪費,inline顯然是更好的選擇。InlineChunkWebpackPlugin能夠幫助咱們將指定的chunk經過inline的形式寫入index.html文件。在Webpack 4尚不支持的狀況下,只好在http和ctrl + a&ctrl + c&ctrl + v中選擇一個更合適您口味的方法了。
  3. CleanWebpackPlugin 首先我要說明,這是一個玄學plugin,用或不用徹底取決於臉黑不黑,手髒不髒。用處就是能夠在打包前清理指定目錄的文件,譬如說舊的bundle文件。開始我也不信,後來的結果大家也看到了。

最後秀一下數據吧

在展現最終結果以前須要聲明的一點是,因爲升級Webpack的同時,還解決了諸多兼容性問題,因此最終結果的表現不管優劣,都不只僅是Webpack的功過,loader以及plugin替換帶來的性能影響一樣不可忽略。至於如何到達提速98%,若是全部依賴所有更新成爲es版本的話。。。

  1. DllPlugin + CommonsChunkPlugin對第三方依賴打包場景(production場景) Webpack 3.8.1的打包時長爲57411ms,Webpack 4的打包時長爲13959ms,提高效果約76%,詳情以下圖所示。

    webpack3.8.1
    webpack4.4.1

  2. 本地啓動(development場景) Webpack 3.8.1的啓動時長(僅包含業務代碼打包過程)爲42890ms,Webpack 4的首次啓動(cache文件還沒有產生)時長爲23017ms,Webpack 4的再次啓動(cache文件已經存在,並不是watch模式下的rebuild場景)時長爲15827ms,首次啓動提高效果約46%,再次啓動提高效果上升至63%,詳情以下圖所示。

    webpack3.8.1
    webpack4.4.1(首次啓動,無緩存)
    webpack4.4.1(非首次啓動,有緩存)

結束語

在不糾結到底是Webpack仍是替換loader&plugin的功勞,以及升級過程當中遭遇的懵逼,躁鬱,崩潰的狀況下,此次升級仍是爲項目帶來了正反饋。若是你也是一名追求極致開發體驗的配置工程師的話,此次Webpack升級仍是值得嘗試的。最後但願文章中的內容可以有所幫助。

相關文章
相關標籤/搜索