构建时预渲染:网页首帧优化实践
前言
自JavaScript誕生以來,前端技術發展非常迅速。移動端白屏優化是前端界面體驗的一個重要優化方向,Web 前端誕生了 SSR 、CSR、預渲染等技術。在美團支付的前端技術體系里,通過預渲染提升網頁首幀優化,從而優化了白屏問題,提升用戶體驗,并形成了最佳實踐。
在前端渲染領域,主要有以下幾種方式可供選擇:
| 優點 | 不依賴數據FP 時間最快客戶端用戶體驗好內存數據共享 | 不依賴數據FCP 時間比 CSR 快客戶端用戶體驗好內存數據共享 | SEO 友好首屏性能高,FMP 比 CSR 和預渲染快 | SEO 友好首屏性能高,FMP 比 CSR 和預渲染快客戶端用戶體驗好內存數據共享客戶端與服務端代碼公用,開發效率高 |
| 缺點 | SEO 不友好FCP 、FMP 慢 | SEO 不友好FMP 慢 | 客戶端數據共享成本高模板維護成本高 | Node 容易形成性能瓶頸 |
通過對比,同構方案集合 CSR 與 SSR 的優點,可以適用于大部分業務場景。但由于在同構的系統架構中,連接前后端的 Node 中間層處于核心鏈路,系統可用性的瓶頸就依賴于 Node ,一旦作為短板的 Node 掛了,整個服務都不可用。
結合到我們團隊負責的支付業務場景里,由于支付業務追求極致的系統穩定性,服務不可用直接影響到客訴和資損,因此我們采用瀏覽器端渲染的架構。在保證系統穩定性的前提下,還需要保障用戶體驗,所以采用了預渲染的方式。
那么究竟什么是預渲染呢?什么是 FCP/FMP 呢?我們先從最常見的 CSR 開始說起。
以 Vue 舉例,常見的 CSR 形式如下:
一切看似很美好。然而,作為以用戶體驗為首要目標的我們發現了一個體驗問題:首屏白屏問題。
為什么會首屏白屏
瀏覽器渲染包含 HTML 解析、DOM 樹構建、CSSOM 構建、JavaScript 解析、布局、繪制等等,大致如下圖所示:
要搞清楚為什么會有白屏,就需要利用這個理論基礎來對實際項目進行具體分析。通過 DevTools 進行分析:
- 等待 HTML 文檔返回,此時處于白屏狀態。
- 對 HTML 文檔解析完成后進行首屏渲染,因為項目中對 加了灰色的背景色,因此呈現出灰屏。
- 進行文件加載、JS 解析等過程,導致界面長時間出于灰屏中。
- 當 Vue 實例觸發了 mounted 后,界面顯示出大體框架。
- 調用 API 獲取到時機業務數據后才能展示出最終的頁面內容。
由此得出結論,因為要等待文件加載、CSSOM 構建、JS 解析等過程,而這些過程比較耗時,導致用戶會長時間出于不可交互的首屏灰白屏狀態,從而給用戶一種網頁很“慢”的感覺。那么一個網頁太“慢”,會造成什么影響呢?
“慢”的影響
Global Web Performance Matters for ecommerce的報告中指出:
- 57%的用戶更在乎網頁在3秒內是否完成加載。
- 52%的在線用戶認為網頁打開速度影響到他們對網站的忠實度。
- 每慢1秒造成頁面 PV 降低11%,用戶滿意度也隨之降低降低16%。
- 近半數移動用戶因為在10秒內仍未打開頁面從而放棄。
我們團隊主要負責美團支付相關的業務,如果網站太慢會影響用戶的支付體驗,會造成客訴或資損。既然網站太“慢”會造成如此重要的影響,那要如何優化呢?
優化思路
在User-centric Performance Metrics一文中,共提到了4個頁面渲染的關鍵指標:
基于這個理論基礎,再回過頭來看看之前項目的實際表現:
可見在 FP 的灰白屏界面停留了很長時間,用戶不清楚網站是否有在正常加載,用戶體驗很差。
試想:如果我們可以將 FCP 或 FMP 完整的 HTML 文檔提前到 FP 時機預渲染,用戶看到頁面框架,能感受到頁面正在加載而不是冷冰冰的灰白屏,那么用戶更愿意等待頁面加載完成,從而降低了流失率。并且這種改觀在弱網環境下更明顯。
通過對比 FP、FCP、FMP 這三個時期 DOM 的差異,發現區別在于:
- FP:僅有一個 div 根節點。
- FCP:包含頁面的基本框架,但沒有數據內容。
- FMP:包含頁面所有元素及數據。
仍然以 Vue 為例, 在其生命周期中,mounted 對應的是 FCP,updated 對應的是 FMP。那么具體應該使用哪個生命周期的 HTML 結構呢?
| 缺點 | 只是視覺體驗將 FCP 提前,實際的 TTI 時間變化不大 | 構建時需要獲取數據,編譯速度慢構建時與運行時的數據存在差異性有復雜交互的頁面,仍需等待,實際的 TTI 時間變化不大 |
| 優點 | 不受數據影響,編譯速度快 | 首屏體驗好對于純展示類型的頁面,FP 與 TTI 時間近乎一致 |
通過以上的對比,最終選擇在 mounted 時觸發構建時預渲染。由于我們采用的是 CSR 的架構,沒有 Node 作為中間層,因此要實現 DOM 內容的預渲染,就需要在項目構建編譯時完成對原始模板的更新替換。
至此,我們明確了構建時預渲染的大體方案。
構建時預渲染方案
構建時預渲染流程:
配置讀取
由于 SPA 可以由多個路由構成,需要根據業務場景決定哪些路由需要用到預渲染。因此這里的配置文件主要是用于告知編譯器需要進行預渲染的路由。
在我們的系統架構里,腳手架是基于 Webpack 自研的,在此基礎上可以自定義自動化構建任務和配置。
觸發構建
項目中主要是使用 TypeScript,利用 TS 的裝飾器,我們封裝了統一的預渲染構建的鉤子方法,從而只用一行代碼即可完成構建時預渲染的觸發。
裝飾器:
使用:
構建編譯
從流程圖上,需要在發布機上啟動模擬的瀏覽器環境,并通過預渲染的事件鉤子獲取當前的頁面內容,生成最終的 HTML 文件。
由于我們在預渲染上的嘗試比較早,當時還沒有 Headless Chrome 、 Puppeteer、Prerender SPA Plugin等,因此在選型上使用的是 phantomjs-prebuilt(Prerender SPA Plugin 早期版本也是基于 phantomjs-prebuilt 實現的)。
通過 phantom 提供的 API 可獲得當前 HTML,示例如下:
為了提高構建效率,并行對配置的多個頁面或路由進行預渲染構建,保證在 5S 內即可完成構建,流程圖如下:
方案優化
理想很豐滿,現實很骨感。在實際投產中,構建時預渲染方案遇到了一個問題。
我們梳理一下簡化后的項目上線過程:
開發 -> 編譯 -> 上線
假設本次修改了靜態文件中的一個 JS 文件,這個文件會通過 CDN 方式在 HTML 里引用,那么最終在 HTML 文檔中的引用方式是 <script src="http://cdn.com/index.js"></script>。然而由于項目還沒有上線,所以其實通過完整 URL 的方式是獲取不到這個文件的;而預渲染的構建又是在上線動作之前,所以問題就產生了:
構建時預渲染無法正常獲取文件,導致編譯報錯
怎么辦?
請求劫持
因為在做預渲染時,我們使用啟動了一個模擬的瀏覽器環境,根據 phantom 提供的 API,可以對發出的請求加以劫持,將獲取 CDN 文件的請求劫持到本地,從而在根本上解決了這個問題。示例代碼如下:
構建時預渲染研發流程及效果
最終,構建時預渲染研發流程如下:
開發階段:
- 通過 TypeScript 的裝飾器單行引入預渲染構建觸發的方法。
- 發布前修改編譯構建的配置文件。
發布階段:
- 先進行常規的項目構建。
- 若有預渲染相關配置,則觸發預渲染構建。
- 通過預渲染得到最終的文件,并完成發布上線動作。
完整的用戶請求路徑如下:
通過構建時預渲染在項目中的使用,FCP 的時間相比之前減少了 75%。
作者簡介
- 寒陽,美團資深研發工程師,多年前端研發經歷,負責美團支付錢包團隊和美團支付前端基礎技術。
招聘信息
我們美團金融服務平臺大前端研發組在高速成長中,我們歡迎更多優秀的 Web 前端研發工程師加入,感興趣的朋友可以將簡歷發送到郵箱:shanghanyang@meituan.com。
總結
以上是生活随笔為你收集整理的构建时预渲染:网页首帧优化实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android Hook技术防范漫谈
- 下一篇: 计算机史上首篇教你从算法问题提炼算法思想