Webpack優化——將你的構建效率提速翻倍

0. 背景

隨着構建體系不斷完善、構建體驗不斷優化,webpack 已經逐漸成爲了前端構建體系的一大霸主,對於工做中的真正意義上的前端工程項目,webpack 已經成爲了咱們前端構建技術選型的不二選擇,包括 create-react-app 以及 vue-cli 等等業內常見的腳手架工具的構建體系,也都是基於 webpack 進行了上層封裝。但隨着業務代碼不斷增長,項目深度不斷延伸,咱們的構建時長也會所以不斷增長。漸漸的,總會有人拋出這樣的結論:webpack 構建太慢了、太「重」了。就以筆者本次近期爲團隊優化的項目爲例,以下圖所示,咱們能夠看到,隨着項目的不斷堆砌以及一些不正確的引用,團隊內的項目單次構建時長已經達到了40s,這就形成了工程師若是須要重啓 devServer 或者執行 build,都會形成很很差的體驗。css

而通過優化後,二次啓動的時長能接近8s。那是什麼樣的神仙操做能有如此效果呢?不着急,咱們一步步往下看,只要你跟着個人步驟,或許只須要一個晚上,你也能將大家的團隊項目的構建體系作出進一步優化。html

不過在正文開始以前,首先須要提早說明一點,本次文章介紹的構建效率提高手段是基於 webpack4 進行的,對於使用老版本的項目,如何從老版本升級到 webpack4 的流程我就不作過多介紹了,由於不管是掘金仍是各類論壇上你都能搜到太多優質的文章了,因此對於大部分的基礎知識,好比 webpack-dev-server 相關配置,還有一些常見的 plugin,在本文就不會較多說起。而對於那些持續跟進 webpack 版本的同窗,我相信大家也知道現階段 webpack5 也已經呼之欲出了,下圖是官方給出的里程碑進度,趁着目前只更新到64%,筆者趕忙先發一波軟文,或許到了5的時代,筆者今天所介紹的優化方式,或許都已經被集成到 webpack 自身的體系中了,誰讓它一每天都在不斷變好呢😊。前端

這一年來,前先後後忙於畢業和工做,在掘金裏的角色逐漸從一名做者轉變爲讀者。現在工做和生活也基本趨於穩定,筆者也但願能像曾經那樣,將本身的工做學習積澱,與你們悉數分享。做爲筆者回歸掘金的開篇之做,但願在看完這篇文章後,可以讓你們在工做中,對於現在前端而言不可或缺的構建體系,有新的認知以及更爲大膽的嘗試。固然,若是在這過程當中遇到了問題,我也特別歡迎能和你們一塊交流、一塊學習、一塊進步。vue

閒話很少說,接下來就進入我們本次的正題。node

本文將以筆者在實踐中解決問題的思路爲索引,逐步帶着你們以剖析問題 -> 發現問題 -> 解決問題的流程去了解對構建體系進行優化的整個過程。react

1. 構建打點

要作優化,咱們確定得知道要從哪裏作優化對吧。那在咱們的一次構建流程中,是什麼拉低了咱們的構建效率呢?咱們有什麼方法能夠將它們測量出來呢?webpack

要解決這兩個問題,咱們須要用到一款工具:speed-measure-webpack-plugin,它可以測量出在你的構建過程當中,每個 Loader 和 Plugin 的執行時長,官方給出的效果圖是下面這樣:git

而它的使用方法也一樣簡單,以下方示例代碼所示,只須要在你導出 Webpack 配置時,爲你的原始配置包一層 smp.wrap 就能夠了,接下來執行構建,你就能在 console 面板看到如它 demo 所示的各種型的模塊的執行時長。github

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
 
const smp = new SpeedMeasurePlugin();
 
module.exports = smp.wrap(YourWebpackConfig);
複製代碼

小貼士:因爲 speed-measure-webpack-plugin 對於 webpack 的升級還不夠完善,目前(就筆者書寫本文的時候)還存在一個 BUG,就是沒法與你本身編寫的掛載在 html-webpack-plugin 提供的 hooks 上的自定義 Plugin (add-asset-html-webpack-plugin 就是此類)共存,所以,在你須要打點以前,若是存在這類 Plugin,請先移除,不然會產生如我這篇 issue 所提到的問題。web

能夠斷言的是,大部分的執行時長應該都是消耗在編譯 JS、CSS 的 Loader 以及對這兩類代碼執行壓縮操做的 Plugin 上,若是你的執行結果和我所說的同樣,請不要吝嗇你的手指,爲個人文章點個贊吧😁。

爲何會這樣呢?由於在對咱們的代碼進行編譯或者壓縮的過程當中,都須要執行這樣的一個流程:編譯器(這裏能夠指 webpack)須要將咱們寫下的字符串代碼轉化成 AST(語法分析樹),就是以下圖所示的一個樹形對象:

顯而易見,編譯器確定不能用正則去顯式替換字符串來實現這樣一個複雜的編譯流程,而編譯器須要作的就是遍歷這棵樹,找到正確的節點並替換成編譯後的值,過程就像下圖這樣:

這部分知識我在以前的一篇文章 Webpack揭祕——走向高階前端的必經之路 中曾詳細介紹過,若是你有興趣瞭解,能夠翻閱噢~

你們必定還記得曾經在學習《數據結構與算法》或者是面試時候,被樹形結構的各類算法虐待千百遍的日子吧,你必定也還記得深度優先遍歷和廣度優先遍歷的實現思路對吧。可想而知,之因此構建時長會集中消耗在代碼的編譯或壓縮過程當中,正是由於它們須要去遍歷樹以替換字符或者說轉換語法,所以都須要經歷"轉化 AST -> 遍歷樹 -> 轉化回代碼"這樣一個過程,你說,它的時長能不長嘛。

2. 優化策略

既然咱們已經找到了拉低咱們構建速率的「罪魁禍首」,接下來咱們就點對點逐個擊破了!這裏,我就直接開門見山了,既然咱們都知道構建耗時的緣由,天然就能得出針對性的方略。因此咱們會從四個大方向入手:緩存、多核、抽離以及拆分,你如今看到這四個詞或許腦海裏又能浮現出了一些熟悉的思路,這很棒,這樣的話你對我接下來將介紹的手段必定就能更快理解。

2.1. 緩存

咱們每次的項目變動,確定不會把全部文件都重寫一遍,可是每次執行構建卻會把全部的文件都重複編譯一遍,這樣的重複工做是否能夠被緩存下來呢,就像瀏覽器加載資源同樣?答案確定是能夠的,其實大部分 Loader 都提供了 cache 配置項,好比在 babel-loader 中,能夠經過設置 cacheDirectory 來開啓緩存,這樣,babel-loader 就會將每次的編譯結果寫進硬盤文件(默認是在項目根目錄下的node_modules/.cache/babel-loader目錄內,固然你也能夠自定義)。

但若是 loader 不支持緩存呢?咱們也有方法。接下來介紹一款神器:cache-loader ,它所作的事情很簡單,就是 babel-loader 開啓 cache 後作的事情,將 loader 的編譯結果寫入硬盤緩存,再次構建若是文件沒有發生變化則會直接拉取緩存。而使用它的方法很簡單,正如官方 demo 所示,只須要把它卸載在代價高昂的 loader 的最前面便可:

module.exports = {
  module: {
    rules: [
      {
        test: /\.ext$/,
        use: ['cache-loader', ...loaders],
        include: path.resolve('src'),
      },
    ],
  },
};
複製代碼

小貼士cache-loader 默認將緩存存放的路徑是項目根目錄下的 .cache-loader 目錄內,咱們習慣將它配置到項目根目錄下的 node_modules/.cache 目錄下,與 babel-loader 等其餘 Plugin 或者 Loader 緩存存放在一塊

同理,一樣對於構建流程形成效率瓶頸的代碼壓縮階段,也能夠經過緩存解決大部分問題,以 uglifyjs-webpack-plugin 這款對於咱們最經常使用的 Plugin 爲例,它就提供了以下配置:

module.exports = {
  optimization: {
    minimizer: [
      new UglifyJsPlugin({
        cache: true,
        parallel: true,
      }),
    ],
  },
};
複製代碼

咱們能夠經過開啓 cache 配置開啓咱們的緩存功能,也能夠經過開啓 parallel 開啓多核編譯功能,這也是咱們下一章節立刻就會講到的知識。而另外一款咱們比較經常使用於壓縮 CSS 的插件—— optimize-css-assets-webpack-plugin,目前我還未找到有對緩存和多核編譯的相關支持,若是讀者在這塊領域有本身的沉澱,歡迎在評論區提出批正。

小貼士:目前而言筆者暫不建議將緩存邏輯集成到 CI 流程中,由於目前還仍會出現更新依賴後依舊命中緩存的狀況,這顯然是個 BUG,在開發機上咱們能夠手動刪除緩存解決問題,但在編譯機上過程就要麻煩的多。爲了保證每次 CI 結果的純淨度,這裏建議在 CI 過程當中仍是不要開啓緩存功能。

2.2. 多核

這裏的優化手段你們確定已經想到了,天然是咱們的 happypack。這彷佛已是一個老生常談的話題了,從3時代開始,happypack 就已經成爲了衆多 webpack 工程項目接入多核編譯的不二選擇,幾乎全部的人,在提到 webpack 效率優化時,怎麼樣也會說出 happypack 這個詞語。因此,在前端社區繁榮的今天,從 happypack 出現的那時候起,就有許多優秀的質量文如雨後春筍般層出不窮。因此今天在這裏,對於 happypack 我就不作過多細節上的介紹了,想必你們對它也再熟悉不過了,我就帶着你們簡單回顧一下它的使用方法吧。

const HappyPack = require('happypack')
const os = require('os')
// 開闢一個線程池
// 拿到系統CPU的最大核數,happypack 將編譯工做灌滿全部線程
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })

module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: 'happypack/loader?id=js',
      },
    ],
  },
  plugins: [
    new HappyPack({
      id: 'js',
      threadPool: happyThreadPool,
      loaders: [
        {
          loader: 'babel-loader',
        },
      ],
    }),
  ],
}
複製代碼

因此配置起來邏輯其實很簡單,就是用 happypack 提供的 Plugin 爲你的 Loaders 作一層包裝就行了,向外暴露一個id ,而在你的 module.rules 裏,就不須要寫loader了,直接引用這個 id 便可,因此趕忙用 happypack 對那些你測出來的代價比較昂貴的 loaders 們作一層多核編譯的包裝吧。

而對於一些編譯代價昂貴的 webpack 插件,通常都會提供 parallel 這樣的配置項供你開啓多核編譯,所以,只要你善於去它的官網發現,必定會有意想不到的收穫噢~

PS:這裏須要特別說起一個在 production 模式下容易遇到的坑,由於有個特殊的角色出現了 —— mini-css-extract-plugin,坑在哪呢?有兩點(這也是筆者在書寫本文時還未解決的問題):

  1. MiniCssExtractPlugin 沒法與 happypack 共存,若是用 happypack 對 MiniCssExtractPlugin 進行包裹,就會觸發這個問題:github.com/amireh/happ…
  2. MiniCssExtractPlugin 必須置於 cache-loader 執行以後,不然沒法生效,參考issue:github.com/webpack-con…

因此最後,在 production 模式下的 CSS Rule 配置就變成了下面這樣:

module.exports = {
    ...,
    module: {
        rules: [
            ...,
            {
                test: /\.css$/
                exclude: /node_modules/,
                use: [
                    _mode === 'development' ? 'style-loader' : MiniCssExtractPlugin.loader,
                    'happypack/loader?id=css'
                ]
            }
        ]
    },
    plugins: [
        new HappyPack({
          id: 'css',
          threadPool: happyThreadPool,
          loaders: [
            'cache-loader',
            'css-loader',
            'postcss-loader',
          ],
        }),
    ],
}
複製代碼

2.3. 抽離

對於一些不常變動的靜態依賴,好比咱們項目中常見的 React 全家桶,亦或是用到的一些工具庫,好比 lodash 等等,咱們不但願這些依賴被集成進每一次構建邏輯中,由於它們真的太少時候會被變動了,因此每次的構建的輸入輸出都應該是相同的。所以,咱們會設法將這些靜態依賴從每一次的構建邏輯中抽離出去,以提高咱們每次構建的構建效率。常見的方案有兩種,一種是使用 webpack-dll-plugin 的方式,在首次構建時候就將這些靜態依賴單獨打包,後續只須要引用這個早就被打好的靜態依賴包便可,有點相似「預編譯」的概念;另外一種,也是業內常見的 Externals的方式,咱們將這些不須要打包的靜態資源從構建邏輯中剔除出去,而使用 CDN 的方式,去引用它們。

那對於這兩種方式,咱們又該如何選擇呢?

2.3.1.webpack-dll-plugin 與 Externals 的抉擇

團隊早期的項目腳手架使用的是 webpack-dll-plugin 進行靜態資源抽離,之因此這麼作的緣由是由於原先也是使用的 Externals,可是因爲公司早期 CDN 服務並不成熟,項目使用了線上開源的 CDN 卻由於服務不穩定致使了團隊項目出現問題的狀況,因此在一次迭代中統一替換成了 webpack-dll-plugin,但隨着公司創建起了成熟的 CDN 服務後,團隊的腳手架卻由於各類緣由遲遲沒再更新。

而我,是堅決的 Externals 的支持着,這不是心之所向,先讓咱們來細數 webpack-dll-plugin 的三宗原罪:

  1. 須要配置在每次構建時都不參與編譯的靜態依賴,並在首次構建時爲它們預編譯出一份 JS 文件(後文將稱其爲 lib 文件),每次更新依賴須要手動進行維護,一旦增刪依賴或者變動資源版本忘記更新,就會出現 Error 或者版本錯誤。

  2. 沒法接入瀏覽器的新特性 script type="module",對於某些依賴庫提供的原生 ES Modules 的引入方式(好比 vue 的新版引入方式)沒法獲得支持,無法更好地適配高版本瀏覽器提供的優良特性以實現更好地性能優化。

  3. 將全部資源預編譯成一份文件,並將這份文件顯式注入項目構建的 HTML 模板中,這樣的作法,在 HTTP1 時代是被推崇的,由於那樣能減小資源的請求數量,但在 HTTP2 時代若是拆成多個 CDN Link,就可以更充分地利用 HTTP2 的多路複用特性。口說無憑,直接上圖驗證結論:

    • 使用 webpack-dll-plugin 生成的 lib 文件,總體資源做爲一個文件加載,須要 400 多毫秒
    • 使用 Externals 配合 HTTP2,全部資源並行加載,總體時長不超過 100ms

這,就是我選擇 Externals 的緣由。

可是,若是你的公司沒有成熟的 CDN 服務,但又想對項目中的靜態依賴進行抽離該怎麼辦呢?那筆者的建議仍是選擇 webpack-dll-plugin 來優化你的構建效率。若是你仍是以爲每次更新依賴都須要去維護一個 lib 文件特別麻煩,那我仍是特別提醒你,在使用 Externals 時選擇一個靠譜的 CDN 是一件特別重要的事,畢竟這些依賴好比 React 都是你網站的骨架,少了他們但是連站點都運行不起來了噢。

2.3.2.如何更爲優雅地編寫 Externals

咱們都知道,在使用 Externals 的時候,還須要同時去更新 HTML 裏面的 CDN,有時候時常會忘記這一過程而致使一些錯誤發生。那做爲一名追求極致的前端,咱們是否能夠嘗試利用現有資源將這一過程自動化呢?

這裏我就給你們提供一個思路,咱們先來回顧及分析一下,在咱們配置 Externals 時,須要配置那些部分。

首先,在 webpack.config.js 配置文件內,咱們須要添加 webpack 配置項:

module.exports = {
  ...,
  externals: {
    // key是咱們 import 的包名,value 是CDN爲咱們提供的全局變量名
    // 因此最後 webpack 會把一個靜態資源編譯成:module.export.react = window.React
    "react": "React",
    "react-dom": "ReactDOM",
    "redux": "Redux",
    "react-router-dom": "ReactRouterDOM"
  }
}
複製代碼

與此同時,咱們須要在模板 HTML 文件中同步更新咱們的 CDN script 標籤,通常一個常見的 CDN Link 就像這樣:

https://cdn.bootcss.com/react/16.9.0/umd/react.production.min.js

這裏以 BootCDN 提供的靜態資源 CDN 爲例(但不表明筆者推薦使用 BootCDN 提供的 CDN 服務,它上次更換域名的事件可真是讓我踩了很多坑),咱們能夠發現,一份 CDN Link 其實主要也只是由四部分組成,它們分別是:CDN 服務 host、包名、版本號以及包路徑,其餘 CDN 服務也是同理。以上面的 Link 爲例,這四部分對應的內容就是:

  • CDN 服務 host:cdn.bootcss.com/
  • 包名:react
  • 版本號:16.9.0
  • 包路徑:umd/react.production.min.js

到了這一步,你們應該想到了吧。咱們徹底能夠本身編寫一個 webpack 插件去自動生成 CDN Link script 標籤並掛載在 html-webpack-plugin 提供的事件鉤子上以實現自動注入 HTML,而咱們所須要的一個 CDN Link 的四部份內容,CDN 服務 host 咱們只須要與公司提供的服務統一便可,包名咱們能夠經過 compiler.options.externals 拿到,而版本號咱們只須要讀取項目的 package.json 文件便可,最後的包路徑,通常都是一個固定的值。

具體代碼實現我就不做詳細介紹了,團隊在項目腳手架更新迭代期間,筆者已經根據公司提供的 CDN 服務定製了一款 Webpack 插件,實現邏輯就如上述所示,因此後續工程師們就再也不須要去關注同步 script 標籤了,一切都被集成進 Plugin 邏輯自動化處理了,固然,你們若是對插件的源碼有興趣,能夠在評論區提出噢~筆者會考慮做爲團隊的開源項目貢獻給社區。

2.4. 拆分

雖說在大前端時代下,SPA 已經成爲主流,但咱們難免仍是會有一些項目須要作成 MPA(多頁應用),得益於 webpack 的多 entry 支持,所以咱們能夠把多頁都放在一個 repo 下進行管理和維護。但隨着項目的逐步深刻和不斷迭代,代碼量必然會不斷增大,有時候咱們只是更改了一個 entry 下的文件,可是卻要對全部 entry 執行一遍構建,所以,這裏爲你們介紹一個集羣編譯的概念:

什麼是集羣編譯呢?這裏的集羣固然不是指咱們的真實物理機,而是咱們的 docker。其原理就是將單個 entry 剝離出來維護一個獨立的構建流程,並在一個容器內執行,待構建完成後,將生成文件打進指定目錄。爲何能這麼作呢?由於咱們知道,webpack 會將一個 entry 視爲一個 chunk,並在最後生成文件時,將 chunk 單獨生成一個文件,

由於現在團隊在實踐前端微服務,所以每個子模塊都被拆分紅了一個單獨的repo,所以咱們的項目與生俱來就繼承了集羣編譯的基因,可是若是把這些子項目以 entry 的形式打在一個 repo 中,也是一個很常見的狀況,這時候,就須要進行拆分,集羣編譯便能發揮它的優點。由於團隊裏面不須要進行相關實踐,所以這裏筆者就不提供細節介紹了,只是爲你們提供一個方向,若是你們有疑問也歡迎在評論區與我討論。

3. 提高體驗

這裏主要是介紹幾款 webpack 插件來幫助你們提高構建體驗,雖說它們在提高構建效率上對你沒有什麼太大的幫助,但能讓你在等待構建完成的過程當中更加舒服。

3.1. progress-bar-webpack-plugin

這是一款能爲你展現構建進度的 Plugin,它的使用方法和普通 Plugin 同樣,也不須要傳入什麼配置。下圖就是你加上它以後,在你的終端面板上的效果,在你的終端底部,將會有一個構建的進度條,可讓你清晰的看見構建的執行進度:

3.2. webpack-build-notifier

這是一款在你構建完成時,可以像微信、Lark這樣的APP彈出消息的方式,提示你構建已經完成了。也就是說,當你啓動構建時,就能夠隱藏控制檯面板,專心去作其餘事情啦,到「點」了天然會來叫你,它的效果就是下面這樣,同時還有提示音噢~

3.3. webpack-dashboard

固然,若是你對 webpack 原始的構建輸出不滿意的話,也可使用這樣一款 Plugin 來優化你的輸出界面,它的效果就是下面這樣,這裏我就直接上官圖啦:

4. 總結

綜上所述,其實本質上,咱們對與webpack構建效率的優化措施也就兩個大方向:緩存和多核。緩存是爲了讓二次構建時,不須要再去作重複的工做;而多核,更是充分利用了硬件自己的優點(我相信現現在你們的電腦確定都是雙核以上了吧,我本身這臺公司發的低配 MAC 都有雙核),讓咱們的複雜工做都能充分利用咱們的 CPU。而將這兩個方向化爲實踐的主角,也是咱們前面介紹過的兩大王牌,就是:cache-loaderhappypack,因此你只要知道它並用好它,那你就能作到更好的構建優化實踐。因此,別光看看,快拿着你的項目動手實踐下,讓你優化後的團隊項目在你的 leader 面前眼前一亮吧!

可是,你們必定要記着,這些東西並非說用了效果就必定會是最好的,咱們必定要切記把它們用在刀刃上,就是那些在第一階段咱們經過打點得出的構建代價高昂的 Loader 或者 Plugin,由於咱們知道,像本地緩存就須要讀寫硬盤文件,系統IO須要時間,像啓動多核也須要 IPC 通訊時間,也就是說,若是原本構建時長就不長的模塊,有可能由於添加了緩存或者多核會有得不償失的結果,所以這些優化手段也須要合理的分配和使用。

現在,webpack 自身也在不斷的迭代與優化,它早就已經不是兩三年前那個一直讓咱們吐槽構建慢、包袱重的構建新星了,之因此會成爲主流,也正是由於 webpack 團隊已經在效率及體驗上爲咱們作出不少了,而咱們須要作的,已經不多了,並且我堅信,未來還會更少。

最後,附上一段硬廣,若是你有興趣加入咱們,和咱們一塊兒探尋前端的奧祕,請戳👇👇👇下方圖片👇👇👇便可打開個人內推連接噢~

若是你想了解咱們的團隊,也能夠點擊這個連接查看詳情噢👉:字節跳動最「掙錢」的前端團隊招人啦~

相關文章
相關標籤/搜索