如何將網(wǎng)頁性能提升5倍的 — 構(gòu)建優(yōu)化篇
最近對公司的一個 PC 站點做了一次整體的性能優(yōu)化,由于這個系統(tǒng)業(yè)務(wù)復(fù)雜、依賴非常多,加載速度非常慢,優(yōu)化后各個性能指標(biāo)都有了顯著提升,大約加載速度快了 5 倍左右。

我在 構(gòu)建、網(wǎng)絡(luò)、資源加載、運行時、服務(wù)端、功能組織等多個方面都進行了優(yōu)化,準(zhǔn)備做一個系列,分章節(jié)給大家分享下我的優(yōu)化經(jīng)驗。
今天,我們從優(yōu)化效果最為明顯的構(gòu)建角度開始。
優(yōu)化前
首先我們看一下在優(yōu)化前站點的資源加載情況:

可見最大的 vendor 包居然有 3MB(經(jīng)過 gzip 壓縮后),沒有做額外配置的話,webpack 將所有的第三方依賴都打入了這個包,如果引入依賴越來越多,那么這個包就會越來越大。
另外,系統(tǒng)本身的邏輯打的包也達(dá)到了 600kb
分析依賴關(guān)系
我們可以借助 webpack-bundle-analyzer 將打包后的內(nèi)容展示為方便交互的樹狀圖,我們可以很直觀的看到有哪些比較大的模塊,然后做針對性優(yōu)化。
npm?install?--save-dev?webpack-bundle-analyzer
const?BundleAnalyzerPlugin?=?require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports?=?{
??plugins:?[
????new?BundleAnalyzerPlugin()
??]
}

CDN 引入
CDN 的工作原理是將源站的資源緩存到位于全球各地的 CDN 節(jié)點上,用戶請求資源時,就近返回節(jié)點上緩存的資源,而不需要每個用戶的請求都回您的源站獲取,避免網(wǎng)絡(luò)擁塞、緩解源站壓力,保證用戶訪問資源的速度和體驗。
這個估計大家都明白,因為打包后的產(chǎn)物本身也是上傳到 CDN 的。但是我們要做的是將體積較大的第三方依賴單獨拆出來放到 CDN 上,這樣這個依賴既不會占用打包資源,也不會影響最終包體積。
如果一個依賴有直接打包壓縮好的單文件 CDN 資源,例如上面圖中的 g6,就可以直接使用。
按照官方文檔的解釋,如果我們想引用一個庫,但是又不想讓 webpack 打包,并且又不影響我們在程序中以 import、require 或者 window/global 全局等方式進行使用,那就可以通過配置 externals。
externals配置選項提供了「從輸出的 bundle 中排除依賴」的方法。相反,所創(chuàng)建的bundle依賴于那些存在于用戶環(huán)境(consumer's environment)中的依賴。
首先將 CDN 引入的依賴加入到 externals 中。

然后借助 html-webpack-plugin 將 CDN 文件打入 html:

這里有一點需要注意,在 html 中配置的 CDN 引入腳本一定要在 body 內(nèi)的最底部,因為:
如果放在 body上面或header內(nèi),則加載會阻塞整個頁面渲染。如果放在 body外,則會在業(yè)務(wù)代碼被加載之后加載,模塊中使用了該模塊將會報錯。
拆 vendor

某些場景下, 一個第三方依賴可能拆成了多個子依賴,例如上面的 monaco,或者沒有提供可直接通過 CDN 引入的文件,我們就無法通過配置一個 CDN 文件來引入它了。
這時我們需要自己去 webpack 設(shè)置一些規(guī)則,將我們想拆出來的依賴單獨打包一個 vendor。

動態(tài) import
將 vendor 拆分后,依賴仍然會在首屏被加載,如果依賴不在首屏使用,仍然會造成網(wǎng)絡(luò)資源的浪費,并阻塞頁面渲染,對于沒必要在首屏進行加載的依賴,我們可以采用動態(tài) import 的方式。

例如上面這個 js-export-excel 這個依賴,自己本身有將近 500 kb,但是其只會在用戶點擊【導(dǎo)出】按鈕的時候使用,我們首先在 vendor 中將其拆出來。

使用時,將 import 的邏輯由首屏改到運行時異步加載

這樣的話,js-export-excel 這個依賴包只會在用戶點擊【導(dǎo)出】按鈕時引入,首屏不再引入。
不是所有依賴都適合異步加載,如果你對使用該依賴有很高的性能要求,然后依賴本身也比較大,這種情況是不適合的,因為你可能會看到明顯的延遲。以上 export 其實是一個比較合適的場景,下載 excel 本身需要延遲時間,加上動態(tài)加載依賴的時間是可接收的。
React 懶加載
類似的,對于某些第三方依賴組件,例如 monaco editor ,我們只有在很少的業(yè)務(wù)場景下才會用到,但是其本身一個包占用了 5MB 。。我們每次在打開頁面時都要加載它,這太耗費性能了。

對于一個依賴包,我們可以通過動態(tài) import 的方式進行懶加載,但是對于一個 React 組件,直接使用動態(tài) import 可能就不太合適了,組件渲染的運行時都是可多次觸發(fā)了,不可能在每次組件渲染時都加載一次組件。
React.lazy 函數(shù)能讓你像渲染常規(guī)組件一樣處理動態(tài)引入組件。React.lazy 接受一個函數(shù),這個函數(shù)需要動態(tài)調(diào)用 import()。它必須返回一個 Promise,該 Promise 需要 resolve 一個 default export 的 React 組件。
const?MonacoEditor?=?React.lazy(()?=>?import('react-monaco-editor'));
此代碼將會在組件首次渲染時,自動導(dǎo)入包含 MonacoEditor 組件的包。但是直接使用React.lazy引入的組件是無法直接使用的,因為 React 無法預(yù)測組件何時被加載,直接渲染會導(dǎo)致頁面崩潰。
在 Suspense 組件中渲染 lazy 組件,可以使用在等待加載 lazy 組件時做優(yōu)雅降級(如 loading )。fallback 屬性接受任何在組件加載過程中你想展示的 React 元素。你可以將 Suspense 組件置于懶加載組件之上的任何位置。你甚至可以用一個 Suspense 組件包裹多個懶加載組件。

將所有 monaco editor 改為懶加載后,首屏已經(jīng)不會加載 monaco editor。

路由懶加載
上面 React 懶加載的方式,同樣適用于路由,對于每個路由都使用懶加載的方式引入,則每個模塊都會被單獨打為一個 js,首屏只會加載當(dāng)前模塊引入的 js。


不過 路由懶加載 也有一個很明顯的弊端,就是每個模塊的資源是只有加載這個模塊的時候才回去下載的,所以在切換模塊的時候可能會有一小段白屏或
loading效果,這個要結(jié)合業(yè)務(wù)自身的情況綜合判斷要不要使用。
語言包優(yōu)化

在某些場景下,語言包會占用整個包體積的非常大一部分。實際上庫本身的邏輯不會很大,moment 就是一個很好例子。
如果最開始選擇日期庫,那直接推薦使用 dayjs 了,如果你選擇了 moment ,一定要注意把不使用的語言包過濾掉,推薦使用 ContextReplacementPlugin,它會告訴 webpack 我們會使用到哪個本地文件:
plugins:?[
????new?webpack.ContextReplacementPlugin(/moment[/\\]locale$/,?/zh-cn/),
??]
優(yōu)化效果

最終優(yōu)化后,會發(fā)現(xiàn)模塊已經(jīng)被我們拆的非常均勻,并且只會在對應(yīng)頁面渲染時加載對應(yīng)模塊,這對首屏渲染速度有顯著提升。
未完待續(xù),期待一下后續(xù)文章吧 !
推薦閱讀
