重新學習瀏覽器資源載入機制,這些年努力改善電商網站效能的旅程 - Jason's Web Memo

重新學習瀏覽器資源載入機制,這些年努力改善電商網站效能的旅程

談到網頁的效能,前端工程師往往特別專注在 JavaScript 執行速度與如何減少網站下載資源的體積,但對於一般使用者來說,他們對一個網站的效能最重要的感受就是畫面呈現的速度,這篇文章記錄這些年來工作上嘗試改善網頁畫面呈現速度的經驗與心得。

認識網頁效能指標 #

Google 定義了很多效能指標去衡量網頁的速度狀況,先來談談使用者對於畫面呈現速度感受的效能指標,First Contentful Paint FCP 指的是瀏覽器畫面開始出現的時間,但是如果一個網頁一開始就先送一個轉圈圈的 loading 圖再慢慢下載資料,網頁還處於 loading 狀態的 FCP 對於使用者來說意義也不大,因此為了尋求更有意義的指標數據,因此後來又出現抓 DOM 變化速度最大的時間的 First Meaningful Paint FMP,以及用畫面未完成度對時間積分的 speed index,經過 Google 一陣子的研究,最後他們提出應該由畫面中最大的元素呈現時間為主,也就是 Largest Contentful Paint LCP 作為主要指標,LCP 這指標最後也進了 web vitals 三本柱裡頭。

評估效能的工具以及環境 #

Google 評估效能主要分成兩種環境,一個叫做 lab data,是模擬使用者在低網速低階手機的上網體驗,也就是我們平常在 Chrome 裡頭用的 Lighthouse,或者更細膩提供各種連線狀況 Webpagetest 效能量測服務都算這個範疇,另外一個是 field data,是 Chrome 瀏覽器在真實世界裡面搜集的網站效能資料 Chrome UX report,Pagespeed 裡頭針對 origin 的欄位資料就是這個部分。我們一般會用 lab data 作為開發上 debug 並尋求效能改善的建議,並在 field data 檢視效能改善對真實使用者影響的成果。

<img /> 跟 background-image 誰的速度比較快? #

我們常常用 <img /> 跟 background-image 在網頁上去呈現圖片,但到底哪種方式的載入速度比較快?要討論這個問題,我們必須要來研究瀏覽器畫面生成的流程 critical rendering path。

上圖為瀏覽器 critical rendering path,html markup 會轉換成 DOM 的資料結構 css 也會轉換成 CSSOM 的資料結構,兩者合成 render tree 的資料結構以後,瀏覽器才會開始計算每個節點 layout 的位置,最後進行繪製 paint 的階段。

而在 parsing 網頁的這個過程中,同步的 <script> tag 會 block 瀏覽器 parser 往下走繪製的過程,然而 html 裡頭的 img script css 等 resource 會由另外 preload scanner 負責,preload scanner 不會受到 <script> tag block 的影響,會 scan 剩餘的 html 並下載其餘的 resource,而 css 裡面的 font 跟 background image 則會等到瀏覽器進行 layout 的流程才會下載,所以 <img /> 下載時間會比 background image 來得早。

<html>
<head>
<link href="style.css" rel="styesheet" />
</head>
<body>
<div>
<img src="https://www.yimg.com/logo.jpg" />
<div style="background: url('https://www.yimg.com/bg.jpg')"></div>
</div>
</body>
<script async src="https://www.yimg.com/core.js"></script>
<script async src="https://www.yimg.com/verndor.js"></script>
<script async src="https://www.yimg.com/page.js"></script>
</html>

如果以上面的 html 為例,resource 載入的時間圖如下:

這也同時解釋了 Sever Side Rendering 跟 Client Side Rendering 對於畫面出現速度的影響,Client Side Rendering 先天上的劣勢要等 JavaScript 跑完畫面才會出來,所以許多 Client Side Rendering 的 web app 會在 html 偷吃步藏一個沒有內容的 loading 畫面空殼來改善使用者體驗,但如果從主要有意義的內容 LCP FMP 呈現速度與畫面完成度 SpeedIndex 來看,並沒有改善多少。

然而 <img /> 會比 background image 提早下載,所以多用 <img /> 網頁畫面就會出現的比較快嗎?不盡然。

connection 成本並沒有你想像的便宜 #

瀏覽器對於一個 https 的連線步驟如下,一開始除了基本的 dns 查詢 TCP/IP 三方握手之外,接下來因為是 https 加密也需要 TLS handshake,經過一堆握手 round trip 的過程以後,另外加上 TCP/IP congestion control 的 slow start 後,下載 resource 的速度才能夠上來

http/2 有 connection coalescing 的機制,不同的 origin domain connection 要重複使用必續滿足兩個前提,必須相同的 ip 和一樣 TLS cert name,一但 cdn domain 沒有同時滿足這兩個前提,上述的 connection 過程還要再來一遍。

http/2 resource priority 的美麗與哀愁 #

以往過去 http/1 的時代,同一時間瀏覽器對同一個 origin 的 connection 有其上限,chrome 上限是六個,然而 http/2 的時候,對同一個 origin 下載 resource 就可以利用一條 connection multiplexing 多工進行。

然而多工進行就會遇到 priority 的問題,並不是每一個 resource 都對瀏覽器的渲染都是一樣的重要,最重要的 resource 如 css,在同樣的頻寬下如果使用大量的 img 或其他的 external resource,就會 css 下載速度產生排擠的效應,css 下載速度被拖累,畫面就會很慢才出來。

因此 http/2 有 prioritization 的機制,resource stream 可以有 dependency,一個資源的下載必須等待其他資源完成,可以有 weight,用比例分配頻寬,可以有 exclusive 排他性,如下圖中的 resource B 必須先下載完才可以加載其他 resource C 跟 D,

而不同的瀏覽器對 http/2 proritization 有不同的實作,以下圖片影片摘錄自 https://blog.cloudflare.com/better-http-2-prioritization-for-a-faster-web

Chrome 目前是唯一有實作 exclusive bit 可以 serialize 的載入資源

Chrome resource downloading 示意影片

Firefox 則是根據 dependency 加上權重依序下載

Firefox resource downloading 示意影片

Safari 則是全部平行下載,但會根據權重分配頻寬

Safari resource downloading 示意影片

舊 Edge 則是全部平行下載 完全沒有支援 prioritization

舊 Edge resource downloading 示意影片

http/2 prioritization 這個機制並不是一個巴掌就拍的響,也要仰賴 server side 的實作,https://ishttp2fastyet.com 在各大主流 cdn 進行 http/2 prioritization 測試,然而令人沮喪的是有許多的 cdn vendor 不乏知名上市公司對於 http/2 prioritization 的支援是相當差的,甚至根本沒有 prioritization 這件事情。

CDN / HostingStatusTest Result
AkamaiPass ✅Dec 22, 2018
Amazon CloudFrontFAIL ❌Nov 28, 2019
BitGravityFAIL ❌Dec 22, 2018
CacheflyFAIL ❌Dec 22, 2018
CDN77FAIL ❌Dec 22, 2018
CDNetworksFAIL ❌Dec 22, 2018
CDNsunPass ✅Dec 22, 2018
ChinaCacheFAIL ❌Dec 22, 2018
CloudflarePass ✅Dec 22, 2018
DreamHostPass ✅Dec 22, 2018
EdgecastFAIL ❌Dec 22, 2018
FacebookPass ✅Dec 22, 2018
FastlyPass ✅Dec 22, 2018
Google Cloud CDNFAIL ❌June 12, 2019
Google FirebasePass ✅Dec 22, 2018
Google StorageFAIL ❌Dec 22, 2018
HighwindsFAIL ❌Dec 22, 2018
IncapsulaFAIL ❌Dec 22, 2018
Instart LogicFAIL ❌Dec 22, 2018
KeyCDNFAIL ❌Dec 22, 2018
LeaseWeb CDNFAIL ❌Dec 22, 2018
Level 3FAIL ❌Dec 22, 2018
LimelightFAIL ❌Dec 22, 2018
MedianovaFAIL ❌Dec 22, 2018
Microsoft AzureFAIL ❌Dec 22, 2018
NetlifyFAIL ❌Nov 28, 2019
Reflected NetworksFAIL ❌Dec 22, 2018
Rocket CDNFAIL ❌Dec 22, 2018
section.ioPass ✅Jan 1, 2019
Sucuri FirewallFAIL ❌Dec 22, 2018
StackPath/NetDNA/MaxCDNFAIL ❌Dec 22, 2018
WordPress.comPass ✅Dec 22, 2018
WordPress.com Jetpack CDN (Photon)FAIL ❌Dec 22, 2018
YottaaFAIL ❌Dec 22, 2018
ZeitFAIL ❌Feb 02, 2020
ZenedgeFAIL ❌Dec 22, 2018

如何保證 render blocking resource 如 css 能夠被最高的權重被下載就變成網頁效能的一個很重要的議題,因為 connection 成本以及 http/2 prioritization 支援差,也許透過不同的 domain cdn 存放 static css 不是一個好主意。

一個簡單的解決方法就是直接 inline style 在 html 裡頭,這就可以保證在一個 connection 下面 html css 的傳遞不會受到其他資源的競爭,然而這就會失去了 static asset 可以被瀏覽器 cache 的好處,因此 Google 曾提出建議 inline critical css,也就是不需要 inline 全部的 style,我只需要 inline viewport 會出現的 style 就好,剩下的 css 還是放在 cdn 上,但是問題就是難在這件事很難做到,很多畫面的組成是由 api 控制,加上各家裝置什麼樣的 size 都有,幾乎無法預先決定這個使用者會有哪些 style 會在 viewport 會被用到,因此簡單粗暴地 inline style 在 html 裡頭也許就是最有效的做法,而且現在整天改 design 加功能,大量的 mobile 裝置 app 裡面還有 webview,也許 cache invalidate 的速度遠比你想像的頻繁。

也許 API 效能才是你網站速度最大的瓶頸 #

對於大部分網速正常的使用者來說 Sever Side Rendering 的效能瓶頸大多是 API responding time,同樣對一個網頁來說也不是每一個 API 都同樣的重要,容易 cache 速度快的 API 在 server side 處理,時間長 user specific 的 API 可以改到 client side 處理,也許你不需要懂很多瀏覽器的運作原理,光是移動 API 的呼叫處,對於 api 速度落差很大的網頁來說這樣就可以十分有效的改善網頁速度。

而另外對於 nodejs 的 http api 預設是沒有開啟 http keep-alive 的,導致 Application server 對 API server 每一次 http request 都要重新建立 connection,在高流量下 enable http keep-alive 可以改善效能並減少 server 負擔。

lazyloading 效能與 SEO 的取捨 #

以往過去實作 image lazyload 都必須靠工程師用 JavaScript 實作,可是這就會讓 Google 的 crawler 爬不到 <img /> 就無法爬不到網站的圖片,過去 Google 提供了一些方式讓他可以爬到圖片,譬如說 <noscript> tag 裡面塞入圖片,Google 也曾經提倡我們可以認 googlebot 的 useragent 不要 lazyload 提供更完整網站資料,避免 google 爬不到有意義的資料,或者可以用 puppeteer 做 prerender 再餵給 google,但畢竟我們平常使用跟開發上並不會用 googlebot 的 useragent 去看網頁,萬一 SEO 頁面出問題會很難察覺,壞了很久沒人去修等到發現可能已經對 SEO 造成很大的傷害。

而現在 Chrome 在 <img> tag 裡面提供了 native attribute loading,只要設定成 loading="lazy" 就可以可以自動偵測圖片是否進入 viewport 範圍才下載,過去要搞老半天的 viewport 偵測 lazyload 圖片,現在輕輕鬆鬆一個 attribute 完成,也同時保留 <img> tag 可以兼顧 SEO 的需求,但美中不足的是支援度的問題,目前 Safari 需要手動開啟實驗性功能才啟用 native lazy loading,然而考量利弊與未來性仍是值得嘗試的做法。

最近 CSS 提出了一個新的屬性叫做 content-visibility,設定成 auto 就可以讓瀏覽器根據 DOM element 有沒有進到 viewport 來決定是否 render,如果單看渲染的負擔感覺可以作為 lazyload 的替代方案,但 html 體積上還有 js framework 的 hydration 效能上的幫助可能不大,另外就是 intrinsic size 似乎沒有辦法 rwd 的計算高度可能會導致 layout shift。

另外如果你的網頁有嵌入 youtube 的需求,直接 iframe youtube 都會導致增加很多瀏覽器的負擔,然而並不是每一個人都想要看 youtube 影片,如果沒有自動播放的需求的話,我們可以拉 youtube 的影片縮圖跟自行製作 youtube 按鈕來做一個假的 iframe,等到使用者點擊才真的去 load youtube iframe 回來,透過這樣 interaction to load 的方式就可以大大降低對不需要看 youtube 的人網頁初始化所需的資源,lite-youtube-embed 就是實作這樣想法的 web component,目前 Yahoo 購物中心 mobile 的商品頁 youtube 播放都採用類似的做法,但這個做法在 mobile Safari 上有一個缺點就是 iframe load 完無法自動播放,需要再次點擊播放按鈕才能播放 youtube 影片。

網頁改善效能成果 #

這場效能改善實作的時間點約莫是 2019 年中實作的,大概做的事情有

下面是當時紀錄的數據:

改善效能前 Lighthouse 52 分 #

改善效能後 Lighthouse 76 分 #

改善效能前後在 webpagetest 模擬網頁在 3G 網路下開啟網頁速度的比較 #

改善效能後與其他競業在 webpagetest 模擬網頁在 3G 網路下開啟網頁速度的比較 #

而畢竟台灣的網路基礎建設相當普及,與其糾結於 3G 網路環境下測試,不如來看看真實使用者的連線資料 Field data 來進行這次效能改善的成果評估:

Chrome UX report 做了 performance 改善後 FCP 數值的演進 #

時至今日,目前台灣各大電商 Pagespeed Field data 表現如下:

Pagespeed 202102 各大電商 origin Field data #

Yahoo 購物中心

Momo

PChome

蝦皮

結語 #

本文希望告訴你的事情是

調整網頁效能並不只是前端工程師的責任,牽扯到許多原生系統架構各個 team 的配合,甚至包括設計師,營運的 operation 的上稿,都會影響,而我們都背著過去系統的包袱不斷的努力著,你知道如何做,跟能不能做得到,真的是運氣運氣,跑分跑得比較慢,有時候真的是愛莫能助。

也許我們花太多力氣去爭論 JS 的框架性能優劣,糾結於各種奇淫技巧去避免 React component rendering,而忽略網頁的初衷就是輕量的把視覺化資訊快速呈現給使用者而已。

Ref #

Jason Chen - Yahoo Taiwan EC Web frontend engineer currently. Write something about web and React.js here

Jason Chen

Yahoo Taiwan EC Web frontend engineer currently. Write something about web and React.js here

Other Posts