最近對公司的一個 PC 站點作了一次總體的性能優化,因爲這個系統業務複雜、依賴很是多,加載速度很是慢,優化後各個性能指標都有了顯著提高,大約加載速度快了 5 倍左右。javascript
我在 構建、網絡、資源加載、運行時、服務端、功能組織等多個方面都進行了優化,準備作一個系列,分章節給你們分享下個人優化經驗。html
今天,咱們從優化效果最爲明顯的構建角度開始。前端
首先咱們看一下在優化前站點的資源加載狀況:java
可見最大的 vendor
包竟然有 3MB
(通過 gzip
壓縮後),沒有作額外配置的話,webpack
將全部的第三方依賴都打入了這個包,若是引入依賴愈來愈多,那麼這個包就會愈來愈大。react
另外,系統自己的邏輯打的包也達到了 600kb
webpack
咱們能夠藉助 webpack-bundle-analyzer
將打包後的內容展現爲方便交互的樹狀圖,咱們能夠很直觀的看到有哪些比較大的模塊,而後作針對性優化。web
npm install --save-dev webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
複製代碼
CDN 的工做原理是將源站的資源緩存到位於全球各地的 CDN 節點上,用戶請求資源時,就近返回節點上緩存的資源,而不須要每一個用戶的請求都回您的源站獲取,避免網絡擁塞、緩解源站壓力,保證用戶訪問資源的速度和體驗。npm
這個估計你們都明白,由於打包後的產物自己也是上傳到 CDN
的。可是咱們要作的是將體積較大的第三方依賴單獨拆出來放到 CDN
上,這樣這個依賴既不會佔用打包資源,也不會影響最終包體積。promise
若是一個依賴有直接打包壓縮好的單文件 CDN
資源,例如上面圖中的 g6
,就能夠直接使用。緩存
按照官方文檔的解釋,若是咱們想引用一個庫,可是又不想讓 webpack
打包,而且又不影響咱們在程序中以 import、require
或者 window/global
全局等方式進行使用,那就能夠經過配置 externals
。
externals
配置選項提供了「從輸出的 bundle 中排除依賴」的方法。相反,所建立的bundle
依賴於那些存在於用戶環境(consumer's environment)中的依賴。
首先將 CDN
引入的依賴加入到 externals
中。
而後藉助 html-webpack-plugin
將 CDN
文件打入 html
:
這裏有一點須要注意,在 html
中配置的 CDN
引入腳本必定要在 body
內的最底部,由於:
body
上面或 header
內,則加載會阻塞整個頁面渲染。body
外,則會在業務代碼被加載以後加載,模塊中使用了該模塊將會報錯。某些場景下, 一個第三方依賴可能拆成了多個子依賴,例如上面的 monaco
,或者沒有提供可直接經過 CDN
引入的文件,咱們就沒法經過配置一個 CDN
文件來引入它了。
這時咱們須要本身去 webpack
設置一些規則,將咱們想拆出來的依賴單獨打包一個 vendor
。
咱們來看 v8
中關於 import
的描述:
This syntactic form for importing modules is a static declaration: it only accepts a string literal as the module specifier, and introduces bindings into the local scope via a pre-runtime 「linking」 process. The static import syntax can only be used at the top-level of the file.
複製代碼
CommonJS
和 ES Module
在用法上有個很是大的區別,CommonJS
容許你能夠在用到的時候再去加載這個模塊,而不用所有放到頂部加載。而 ES Module
的語法是靜態的,靜態 import
語法只能在文件的頂層使用。這種引用方式有個巨大缺陷,就是沒法實現按需引入模塊。
<script type="module">
import * as module from './utils.js';
module.default();
// → logs 'Hi from the default export!'
module.doStuff();
// → logs 'Doing stuff…'
</script>
複製代碼
幸虧, ES Module
目前也支持了 Dynamic import
的用法,動態的 import
會返回一個 promise
,你能夠等待模塊加載完成後再去作一些事情,而不用在頁面初始化就加載它。
<script type="module">
const moduleSpecifier = './utils.js';
import(moduleSpecifier)
.then((module) => {
module.default();
// → logs 'Hi from the default export!'
module.doStuff();
// → logs 'Doing stuff…'
});
</script>
<script type="module"> (async () => { const moduleSpecifier = './utils.js'; const module = await import(moduleSpecifier) module.default(); // → logs 'Hi from the default export!' module.doStuff(); // → logs 'Doing stuff…' })(); </script>
複製代碼
將 vendor
拆分後,依賴仍然會在首屏被加載,若是依賴不在首屏使用,仍然會形成網絡資源的浪費,並阻塞頁面渲染,對於不必在首屏進行加載的依賴,咱們能夠採用動態 import
的方式。
例如上面這個 js-export-excel
這個依賴,本身自己有將近 500 kb
,可是其只會在用戶點擊【導出】按鈕的時候使用,咱們首先在 vendor
中將其拆出來。
使用時,將 import
的邏輯由首屏改到運行時異步加載
這樣的話,js-export-excel
這個依賴包只會在用戶點擊【導出】按鈕時引入,首屏再也不引入。
不是全部依賴都適合異步加載,若是你對使用該依賴有很高的性能要求,而後依賴自己也比較大,這種狀況是不適合的,由於你可能會看到明顯的延遲。以上 export 實際上是一個比較合適的場景,下載 excel 自己須要延遲時間,加上動態加載依賴的時間是可接收的。
相似的,對於某些第三方依賴組件,例如 monaco editor
,咱們只有在不多的業務場景下才會用到,可是其自己一個包占用了 5MB
。。咱們每次在打開頁面時都要加載它,這太耗費性能了。
對於一個依賴包,咱們能夠經過動態 import
的方式進行懶加載,可是對於一個 React
組件,直接使用動態 import
可能就不太合適了,組件渲染的運行時都是可屢次觸發了,不可能在每次組件渲染時都加載一次組件。
React.lazy
函數能讓你像渲染常規組件同樣處理動態引入組件。React.lazy
接受一個函數,這個函數須要動態調用 import()
。它必須返回一個 Promise
,該 Promise
須要 resolve
一個 default export
的 React
組件。
const MonacoEditor = React.lazy(() => import('react-monaco-editor'));
複製代碼
此代碼將會在組件首次渲染時,自動導入包含 MonacoEditor
組件的包。可是直接使用React.lazy
引入的組件是沒法直接使用的,由於 React
沒法預測組件什麼時候被加載,直接渲染會致使頁面崩潰。
在 Suspense
組件中渲染 lazy
組件,可使用在等待加載 lazy
組件時作優雅降級(如 loading
)。fallback
屬性接受任何在組件加載過程當中你想展現的 React
元素。你能夠將 Suspense
組件置於懶加載組件之上的任何位置。你甚至能夠用一個 Suspense
組件包裹多個懶加載組件。
將全部 monaco editor
改成懶加載後,首屏已經不會加載 monaco editor
。
上面 React
懶加載的方式,一樣適用於路由,對於每一個路由都使用懶加載的方式引入,則每一個模塊都會被單獨打爲一個 js
,首屏只會加載當前模塊引入的 js
。
不過 路由懶加載 也有一個很明顯的弊端,就是每一個模塊的資源是隻有加載這個模塊的時候纔回去下載的,因此在切換模塊的時候可能會有一小段白屏或
loading
效果,這個要結合業務自身的狀況綜合判斷要不要使用。
在某些場景下,語言包會佔用整個包體積的很是大一部分。實際上庫自己的邏輯不會很大,moment
就是一個很好例子。
若是最開始選擇日期庫,那直接推薦使用 dayjs
了,若是你選擇了 moment
,必定要注意把不使用的語言包過濾掉,推薦使用 ContextReplacementPlugin
,它會告訴 webpack
咱們會使用到哪一個本地文件:
plugins: [
new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /zh-cn/),
]
複製代碼
最終優化後,會發現模塊已經被咱們拆的很是均勻,而且只會在對應頁面渲染時加載對應模塊,這對首屏渲染速度有顯著提高。
文章中若有錯誤,歡迎在評論區指正;若是文章對你有幫助,歡迎點贊、評論、分享、但願能幫到更多人。
本文首發於公衆號《code祕密花園》歡迎你們關注,原文:我是如何將網頁性能提高5倍的 — 構建優化篇
字節跳動 IES 前端架構團隊急缺人才(p5/p7/p7大量HC),歡迎加我微信
ConardLi
一塊兒來搞事情。