嗨,送你一張Web效能優化地圖
我們都知道對於Web應用來說效能很重要。然而效能優化相關的知識卻非常的龐大並且雜亂。對於效能優化需要做些什麼以及效能瓶頸是什麼,通常我們並不清楚。
不包括那些對效能優化有豐富經驗的高手
事實上關於Web效能有很多可以優化的點,其中涉及到的知識大致可以劃分為幾類:度量標準、編碼優化、靜態資源優化、交付優化、構建優化、效能監控。
圖1. 效能優化分類
本文主要介紹效能優化需要做的事以及需要考慮的問題。目的在於給讀者腦海中生成一個巨集觀的地圖。
不會介紹每個優化專案具體如何操作。PS:後續會有系列文章針對不同優化分類下的具體優化操作進行更詳細的介紹。
1. 度量標準與設定目標
在進行效能優化之前,我們需要為應用選擇一個正確的度量標準(效能指標)以及設定一個合理的優化目標。
並不是所有指標都同樣重要,這取決於你的應用。最後根據度量標準設定一個現實的目標。
1.1 度量標準
下面是一些值得考慮的指標:
- 首次有效繪製(First Meaningful Paint,簡稱FMP,當主要內容呈現在頁面上)
- 英雄渲染時間(Hero Rendering Times,度量使用者體驗的新指標,當用戶最關心的內容渲染完成)
- 可互動時間(Time to Interactive,簡稱TTI,指頁面佈局已經穩定,關鍵的頁面字型是可見的,並且主程序可用於處理使用者輸入,基本上使用者可以點選UI並與其互動)
- 輸入響應(Input responsiveness,介面響應使用者輸入所需的時間)
- 感知速度指數(Perceptual Speed Index,簡稱PSI,測量頁面在載入過程中視覺上的變化速度,分數越低越好)
- 自定義指標,由業務需求和使用者體驗來決定。
FMP與英雄渲染時間非常相似,但它們不一樣的地方在於FMP不區分內容是否有用,不區分渲染出的內容是否是使用者關心的。
1.2 設定目標
- 100毫秒的介面響應時間與60FPS
- 速度指標(Speed Index)小於1250ms
- 3G網路環境下可互動時間小於5s
- 重要檔案的大小預算小於170kb
以上四種指標的設定都有據可循。詳細資訊請檢視RAIL效能模型。
2. 編碼優化
編碼優化涉及到應用的執行時效能,本小節介紹幾個可以提升程式執行時效能的建議。
2.1 資料讀取速度
事實上資料訪問速度有快慢之分,下面列出幾個影響資料訪問速度的因素:
- 字面量與區域性變數的訪問速度最快,陣列元素和物件成員相對較慢
- 變數從區域性作用域到全域性作用域的搜尋過程越長速度越慢
- 物件巢狀的越深,讀取速度就越慢
- 物件在原型鏈中存在的位置越深,找到它的速度就越慢
推薦的做法是快取物件成員值。將物件成員值快取到區域性變數中會加快訪問速度
2.2 DOM
應用在執行時,效能的瓶頸主要在於DOM操作的代價非常昂貴,下面列出一些關於DOM操作相關提升效能的建議:
- 在JS中對DOM進行訪問的代價非常高。請儘可能減少訪問DOM的次數(建議快取DOM屬性和元素、把DOM集合的長度快取到變數中並在迭代中使用。讀變數比讀DOM的速度要快很多。)
- 重排與重繪的代價非常昂貴。如果操作需要進行多次重排與重繪,建議先讓元素脫離文件流,處理完畢後再讓元素迴歸文件流,這樣瀏覽器只會進行兩次重排與重繪(脫離時和迴歸時)。
- 善於使用事件委託
2.3 流程控制
下面列出一些流程控制相關的一些可以略微提升效能的細節,這些細節在大型開源專案中大量運用(例如Vue):
- 避免使用
for...in
(它能列舉到原型,所以很慢) - 在JS中倒序迴圈會略微提升效能
- 減少迭代的次數
- 基於迴圈的迭代比基於函式的迭代快8倍
- 用Map表代替大量的
if-else
和switch
會提升效能
3. 靜態資源優化
Web應用的執行離不開靜態資源,所以對靜態資源的優化至關重要。
3.1 使用Brotli
或Zopfli
進行純文字壓縮
在最高級別的壓縮下Brotli會非常慢(但較慢的壓縮最終會得到更高的壓縮率)以至於伺服器在等待動態資源壓縮的時間會抵消掉高壓縮率帶來的好處,但它非常適合靜態檔案壓縮,因為它的解壓速度很快。
使用Zopfli壓縮可以比Zlib的最大壓縮提升3%至8%。
3.2 圖片優化
儘可能通過srcset
,sizes
和<picture>
元素使用響應式圖片。還可以通過<picture>
元素使用WebP格式的影象。
響應式圖片可能大家未必聽說過,但響應式佈局大家肯定都聽說過。響應式圖片與響應式佈局類似,它可以在不同螢幕尺寸與解析度的裝置上都能良好工作(比如自動切換圖片大小、自動裁切圖片等)。
當然,如果您不滿足這種尺度的優化,還可以對圖片進行更深層次的優化。例如:模糊圖片中不重要的部分以減小檔案大小、使用自動播放與迴圈的HTML5視訊替換GIF圖,因為視訊比GIF檔案還小(好訊息是未來可以通過img
標籤載入視訊)。
4. 交付優化
交付優化指的是對頁面載入資源以及使用者與網頁之間的交付過程進行優化。
4.1 非同步無阻塞載入JS
JS的載入與執行會阻塞頁面渲染,可以將Script標籤放到頁面的最底部。但是更好的做法是非同步無阻塞載入JS。有多種無阻塞載入JS的方法:defer
、async
、動態建立script
標籤、使用XHR非同步請求JS程式碼並注入到頁面。
但更推薦的做法是使用defer
或async
。如果使用defer
或async
請將Script標籤放到head
標籤中,以便讓瀏覽器更早地發現資源並在後臺執行緒中解析並開始載入JS。
4.2 使用Intersection Observer
實現懶載入
懶載入是一個比較常用的效能優化手段,下面列出了一些常用的做法:
- 可以通過
Intersection Observer
延遲載入圖片、視訊、廣告指令碼、或任何其他資源。 - 可以先載入低質量或模糊的圖片,當圖片載入完畢後再使用完整版圖片替換它。
延遲載入所有體積較大的元件、字型、JS、視訊或Iframe是一個好主意
4.3 優先載入關鍵的CSS
CSS資源的載入對瀏覽器渲染的影響很大,預設情況下瀏覽器只有在完成<head>
標籤中CSS的載入與解析之後才會渲染頁面。如果CSS檔案過大,使用者就需要等待很長的時間才能看到渲染結果。針對這種情況可以將首屏渲染必須用到的CSS提取出來內嵌到<head>
中,然後再將剩餘部分的CSS用非同步的方式載入。可以通過Critical做到這一點。
4.4 資源提示(Resource Hints)
Resource Hints(資源提示)定義了HTML中的Link元素與dns-prefetch
、preconnect
、prefetch
與prerender
之間的關係。它可以幫助瀏覽器決定應該連線到哪些源,以及應該獲取與預處理哪些資源來提升頁面效能。
4.4.1 dns-prefetch
dns-prefetch
可以指定一個用於獲取資源所需的源(origin),並提示瀏覽器應該儘可能早的解析。
1 | <link rel="dns-prefetch"href="//example.com"> |
4.4.2 preconnect
preconnect
用於啟動預連結,其中包含DNS查詢,TCP握手,以及可選的TLS協議,允許瀏覽器減少潛在的建立連線的開銷。
12 | <link rel="preconnect"href="//example.com"><link rel="preconnect"href="//cdn.example.com"crossorigin> |
4.4.3 prefetch
Prefetch
用於標識下一個導航可能需要的資源。瀏覽器會獲取該資源,一旦將來請求該資源,瀏覽器可以提供更快的響應。
12 | <link rel="prefetch"href="//example.com/next-page.html"as="html"crossorigin="use-credentials"><link rel="prefetch"href="/library.js"as="script"> |
瀏覽器不會預處理、不會自動執行、不會將其應用於當前上下文。
as
與crossorigin
選項都是可選的。
4.4.4 prerender
prerender
用於標識下一個導航可能需要的資源。瀏覽器會獲取並執行,一旦將來請求該資源,瀏覽器可以提供更快的響應。
1 | <link rel="prerender"href="//example.com/next-page.html"> |
瀏覽器將預載入目標頁面相關的資源並執行來預處理HTML響應。
4.5 Preload
通過一個現有元素(例如:img
,script
,link
)宣告資源會將獲取與執行耦合在一起。然而應用可能只是想要先獲取資源,當滿足某些條件時再執行資源。
Preload提供了預獲取資源的能力,可以將獲取資源的行為從資源執行中分離出來。因此,Preload可以構建自定義的資源載入與執行。
例如,應用可以使用Preload進行CSS資源的預載入、並且同時具備:高優先順序、不阻塞渲染等特性。然後應用程式在合適的時間使用CSS資源:
1234567891011 | <!--通過宣告性標記預載入CSS資源--><link rel="preload"href="/styles/other.css"as="style"><!--或,通過JavaScript預載入CSS資源--><script>varres=document.createElement("link");res.rel="preload";res.as="style";res.href="styles/other.css";document.head.appendChild(res);</script> |
12 | <!--使用HTTP頭預載入-->Link:<https://example.com/other/styles.css>; rel=preload; as=style |
4.6 快速響應的使用者介面
PSI(Perceptual Speed Index,感知速度指數)是提升使用者體驗的重要指標,讓使用者感覺到頁面的反饋比沒有反饋體驗要好很多。
可以嘗試使用骨架屏或新增一些Loading過渡動畫提示使用者體驗。
輸入響應(Input responsiveness)指標同樣重要,甚至更重要。試想,使用者點選了網頁後缺毫無反應會是什麼心情。JS的單執行緒大家已經不能再熟悉,這意味著當JS在執行時使用者介面處於“鎖定”狀態,所以JS同步執行的時間越長,使用者等待響應的時間也就越長。
據調查,JS執行100毫秒以上使用者就會明顯覺得網頁變卡了。所以要嚴格限制每個JS任務執行時間不能超過100毫秒。
解決方案是可以將一個大任務拆分成多個小任務分佈在不同的Macrotask中執行(通俗的說是將大的JS任務拆分成多個小任務非同步執行)。或者使用WebWorkers,它可以在UI執行緒外執行JS程式碼運算,不會阻塞UI執行緒,所以不會影響使用者體驗。
應用越複雜,主動管理UI執行緒就越重要
5. 構建優化
現代前端應用都需要有構建的過程,專案在構建過程中是否進行了合理的優化,會對Web應用的效能有著巨大的影響。例如:影響構建後文件的體積、程式碼執行效率、檔案載入時間、首次有效繪製指標等。
5.1 使用預編譯
拿Vue舉例,如果您使用單檔案元件開發專案,元件會在編譯階段將模板編譯為渲染函式。最終程式碼被執行時可以直接執行渲染函式進行渲染。而如果您沒有使用單檔案元件預編譯程式碼,而是在網頁中引入vue.min.js
,那麼應用在執行時需要先將模板編譯成渲染函式,然後再執行渲染函式進行渲染。相比預編譯,多了模板編譯的步驟,所以會浪費很多效能。
5.2 使用 Tree-shaking、Scope hoisting、Code-splitting
Tree-shaking是一種在構建過程中清除無用程式碼的技術。使用Tree-shaking可以減少構建後文件的體積。
目前Webpack與Rollup都支援Scope Hoisting
。它們可以檢查import
鏈,並儘可能的將散亂的模組放到一個函式中,前提是不能造成程式碼冗餘。所以只有被引用了一次的模組才會被合併。使用Scope Hoisting
可以讓程式碼體積更小並且可以降低程式碼在執行時的記憶體開銷,同時它的執行速度更快。前面2.1節介紹了變數從區域性作用域到全域性作用域的搜尋過程越長執行速度越慢,Scope Hoisting
可以減少搜尋時間。
code-splitting
是Webpack中最引人注目的特性之一。此特效能夠把程式碼分離到不同的bundle
中,然後可以按需載入或並行載入這些檔案。code-splitting
可以用於獲取更小的bundle
,以及控制資源載入優先順序,如果使用合理,會極大影響載入時間。
5.3 服務端渲染(SSR)
單頁應用需要等JS載入完畢後在前端渲染頁面,也就是說在JS載入完畢並開始執行渲染操作前的這段時間裡瀏覽器會產生白屏。
服務端渲染(Server Side Render,簡稱SSR)的意義在於彌補主要內容在前端渲染的成本,減少白屏時間,提升首次有效繪製的速度。可以使用服務端渲染來獲得更快的首次有效繪製。
比較推薦的做法是:使用服務端渲染靜態HTML來獲得更快的首次有效繪製,一旦JavaScript載入完畢再將頁面接管下來。
5.4 使用import
函式動態匯入模組
使用import
函式可以在執行時動態地載入ES2015模組,從而實現按需載入的需求。
這種優化在單頁應用中變得尤為重要,在切換路由的時候動態匯入當前路由所需的模組,會避免載入冗餘的模組(試想如果在首次載入頁面時一次性把整個站點所需要的所有模組都同時載入下來會載入多少非必須的JS,應該儘可能的讓載入的JS更小,只在首屏載入需要的JS)。
使用靜態
import
匯入初始依賴模組。其他情況下使用動態import
按需載入依賴
5.5 使用HTTP快取頭
正確設定expires
,cache-control
和其他HTTP快取頭。
推薦使用Cache-control: immutable
避免重新驗證。
6. 其他
其他一些值得考慮的優化點:
- HTTP2
- 使用最高階的CDN(付費的比免費的強的多)
- 優化字型
- 其他垂直領域的效能優化
7. 效能監控
最後,你可能需要一個性能檢測工具來持續監視網站的效能。
8. 總結
最後用一張圖來總結這篇文章所表達的內容,感謝@anjia幫忙畫的這張圖。
非常感謝李鬆峰老師和安佳姐姐幫忙校驗這篇文章。