阿里妹導讀:當前幾種常見的前端性能優化方案仍然不可避免地會存在一些缺點 。本文 在 ESI (Edge Side Include) 的基礎上,提出了一種新的優化思路:邊緣流式渲染方案(ESR),即藉助 CDN 的邊緣計算 能力,將靜態內容與動態內容以流式的方式,先後返回給用戶。

文末福利:下載《覆蓋全端業務的大前端技術》電子書

背景

對於 web 頁面來說,首跳場景(例如 SEO、付費引流)的性能普遍比二跳場景下要差。原因有多種,主要是首跳用戶在連接複用,和本地資源緩存利用方面,有很大的劣勢。 首跳場 景下,很多在端上的優化手段(預加載,預執行,預渲染等)無法實施。

在客戶端緩存能力無法利用的情況下,利用 cdn 距離用戶近的特性,可以結合緩存做一些性能優化。

思路

思路 1:SSR

爲了性能優化考慮,我們一般都會通過服務端渲染(SSR) ,將首屏動態內容直接服務端輸出。

這種方式的優點是一次 html 返回即可包含頁面主體內容,不需要瀏覽器二次請求接口後再用 js 渲染。但這種方式的缺點也比較明顯,對於距離服務端遠,或者服務端處理時間較長的場景,用戶會看到較長時間的白屏。而且即使 html 返回完成了,用戶並不會立即看到內容,頁面還需要加載前置的 js,css 等資源後,才能看到內容。

思路 2:CSR + CDN

爲了減少白屏時間,考慮利用 CDN 的邊緣緩存能力,可以把頁面 html 直接緩存在 cdn 節點上。但對於大部分場景來說,頁面的主體內容都是動態,或者個性化的,把全部 html 內容緩存在 cdn 上對於業務影響較大,很有少場景能接受。那麼換個思路,只把 html 靜態部分緩存在 cdn 上呢?其實這個思路也是一個很常見的操作,即把 html 的靜態框架部分緩存在 cdn 上,讓用戶能快速看到部分內容,然後再在客戶端發起異步請求,獲取動態內容並且渲染(CSR)。CSR + CDN 模式下的渲染時序圖如下:

這種方式的優點是頁面靜態框架緩存在 cdn 上,用戶可以快速看到頁面框架內容,減少白屏等待焦慮。缺點是完整的頁面內容需要再執行 js ,拉取異步接口回來後再進行渲染。最終有意義的動態內容展示出來的時間,比 SSR 更晚。  

思路 3:ESI

CSR + CDN 的方式,很好地解決了白屏時間問題,但帶來了動態內容展示的延時。之所以有這個問題,是因爲我們把頁面的動態內容和靜態內容分割到了兩個階段中,並且是串行的,而且串行過程中還穿插了 js 的下載和執行。有什麼辦法把動態內容和靜態內容在 CDN 上整合起來呢?

ESI (Edge Side Include) 給了我們一個很好的思路啓發,ESI 最初也是 CDN 服務商們提出的規範,可通過 html 標籤里加特定的動態標籤,可讓頁面的靜態內容緩存在 cdn 上,動態內容可以自由組裝。ESI 的渲染時序圖如下:

這個方案看起來很美好,可以把靜態的部分緩存在 CDN 上了,動態部分在用戶請求時會動態請求和拼接。但最關鍵的問題在於,ESI 模式下,最終返回給用戶的首字節,還是要等到所有動態內容在 CDN 上都獲取和拼接完成。也就是並沒有減少白屏時間,只是減少了 CDN 和服務器之間內容傳輸的體積,帶來的性能優化收益很小。最終效果上與 SSR 區別不大。

雖然 ESI 的效果不符合我們預期,但給了我們很好的思考方向。如果能把 ESI 改造成可先返回靜態內容,動態內容在 CDN 節點獲取到之後,再返回給頁面,就可以保證白屏時間短並且動態內容返回不推遲。如果要實現類似於流式 ESI 的效果,要求在 CDN 上能對請求進行細粒度的操作,以及流式的返回。CDN 節點上支持這麼複雜的操作嗎?答案是肯定的:邊緣計算。我們可以在 CDN 上做類似於瀏覽器的 service worker 的操作,可對請求和響應做靈活的編程。

基於邊緣計算的能力,我們有了一種新的選擇:邊緣流式渲染方案(ESR)。方案詳情如下。

渲染流程

方案的核心思想是,藉助邊緣計算的能力,將靜態內容與動態內容以流式的方式,先後返回給用戶。cdn 節點相比於 server,距離用戶更近,有着更短的網絡延時。在 cdn 節點上,將可緩存的頁面靜態部分,先快速返回給用戶,同時在 cdn 節點上發起動態部分內容請求,並將動態內容在靜態部分的響應流後,繼續返回給用戶。最終頁面渲染的時序圖如下:

從上圖可以看出,cdn 邊緣節點可以很快地返回首字節和頁面靜態部分內容,然後動態內容由 cdn 發起向 server 起並流式返回給用戶。方案有以下特點:

  • 首屏 ttfb 會很短,靜態內容(例如頁面 Header 、基本結構、骨骼圖)可以很快看到。

  • 動態內容是由 cdn 發起,相比於傳統瀏覽器渲染,發起時間更早,且不依賴瀏覽器上下載和執行 js。理論上,最終 reponse 完結時間,與直接訪問服務器獲取完整動態頁面時間一致。

  • 在靜態內容返回後,已經可以開始部分 html 的解析,以及 js, css 的下載和執行。把一些阻塞頁面的操作提前進行,等完整動態內容流式返回後,可以更快地展示動態內容。

  • 邊緣節點與服務端之間的網絡,相比於客戶端與服務端之間的網絡,更有優化空間。例如通過動態加速,以及 edge 與 server 之間的連接複用,能爲動態請求減少 tcp 建連和網絡傳輸開銷。以做到最終動態內容的返回時間,比 client 直接訪問 server 更快。

demo 對比

目前在 alicdn 上對主搜頁面做了一個 demo (https://edge-routine.m.alibaba.com/), 下面是在不同網絡(通過 charles 的 network throttle 配置限速)情況下,與原始頁面的加載對比:

不限速(wifi)

限速 4G

限速 3g

從上面結果可以看出,在網速越慢的情況下,通過 cdn 流式渲染的最終主要元素出來的時間比原始 ssr 的方式出來得越早。這與實際推論也符合,因爲網絡越慢,靜態資源加載時間越慢,對應的瀏覽器提前加載靜態資源帶來的效果也越明顯。另外,不管在什麼網絡情況下,cdn 流式渲染方式的白屏時間要短很多。

整體架構

架構圖

邊緣流式渲染

1  模板

模板就是一個類似於包含 ESI 區塊的語法,基於模板,會將需要動態請求的內容提取出來,把可以靜態返回的內容分離出來並緩存起來。所以模板本質上定義了頁面動態內容和靜態內容。

在流式渲染過程中,會從上到下解析頁面模板,如果是靜態內容,直接返回給用戶,如果遇到動態內容,會執行動態內容的 fetch 邏輯。整個過程中可能有靜態和動態內容交替出現。

設計有以下幾種類型的模板。

1)原始 HTML

這種模板對現有業務的侵入性最小,只需要在現有的 SSR 頁面內容里加上一定的標籤,即可把頁面中動態部分申明出來:


<html>

<head>

<linkrel="stylesheet"type="text/css"href="index.css">

<scriptsrc="index.js"></script><metaname="esr-version"content="0.0.1"/>

</head>

<body>

<div>staic content....</div>

<scripttype="esr/snippet/start"esr-id="111"content="SLICE"></script>

<div>dynamic content1....</div>

<scripttype="esr/snippet/end"></script>

<div>staic content....</div>

<scripttype="esr/snippet/start"esr-id="222"content="https://test.alibaba.com/snippet/222"></script>

<divid="222">

dynamic content2....

</div>

<scripttype="esr/snippet/end"></script>

</body>

</html>

2)靜態模板(暫時沒有關聯的實際場景)

這種模板需要單獨把模板發到 cdn 上(未來如果渲染層接入了 FASS 網關和 SSR ,在這塊可以和他們共用模板內容,並且在工作流中發佈模板時自動同步到 cdn 上一份,同時清空 cdn 上緩存)。動態的內容有兩種渲染方式。一種是利用後端 SSR 出來的動態 html 片斷,另一種是後端提供動態數據,由邊緣節進行動態html片斷渲染。

使用 SSR 動態 html 片斷的好處是,不需要在邊緣上做 html 模板渲染,並且不需要開發者寫兩套模板邏輯。缺點是需要後端有 SSR 能力,並且動態內容傳輸體積較大。

使用邊緣節點渲染動態 html 內容的好處是,後端只需要提供動態數據,不需要 SSR 能力(但前端要有 CSR 的能力做降級兜底),並且傳輸的動態內容體積小。切點是邊緣節點上無法流式透傳動態內容,需要等完整下載到邊緣節點上,處理後再返回給用戶。


<html>

<head>

<linkrel="stylesheet"type="text/css"href="index.css">

<scriptsrc="index.js"></script>

</head>

<body>

<div>staic content....</div>

<scripttype="esr/block"esr-id="111"content="https://test.alibaba.com/snippet/111"></script>

<div>staic content....</div>

<scripttype="esr/template"esr-id="222"content="https://test.alibaba.com/api/data">

<div>

{$data.name}

</div>

</script>

</body>

</html>

2  靜態內容展現

靜態內容來自於模板。對於不同模板類型,獲取靜態內容的方式不一樣。對於 “原始 HTML” 類型的模板,靜態內容會從首次動態請求返回的完整 HTML 中,根據 html 註釋標記提取出來,並存儲到 edge 緩存上。對於 “靜態模板”,會通過拉取 CDN 的的模板文件 ,並存儲到 edge 緩存上。靜態內容有緩存過期時間和版本號。

模板一開始的靜態內容會在響應時直接返回給用戶。後續的靜態內容(例如 html 和 body 的閉合標籤)有兩種方式:

  • 一種是等待動態內容返回後,再寫到響應流中。這種方式對 SEO 比較友好,但缺點是動態內容會阻塞住後續靜態內容,並且如果有多個動態內容區塊的話,無法實現先返回的動態模板先展示,只能依次展示。

  • 另一種方式是先把靜態內容完全返回,然後動態內容以類 bigpipe 的方式,通過腳本把內容插入到對應的坑位。這種方式的優點是靜態內容可以一開始就完整展示,且多個動態內容可以先到先展示。缺點是對 SEO 不友好(因爲動態內容是能進 js 插進去的)。

3  動態內容

動態內容是在渲染過程中,解析到需要動態獲取的區域,會在 edge 上發起動態內容請求。動態內容支持以動態加速的形式到達服務端(源站)。連續節點與後端的動態的內容交互,分爲三種方式:

  • 第一種是後端動態內容返回的是全量的頁面,需要通過註釋標記來從內容中提取。這種方式的優點是對現有業務侵入較小,缺點是動態內容傳輸體積大,並且需要下載完整 html 後再截取動態內容。

  • 第二種是後端動態內容只返回動態區塊的內容,這種方式的優點是可以將動態響應流式返回給用戶,缺點時需要頁面單獨對外提供一個只返回動態區塊內容的 url。

  • 第三種是後端動態內容只返回數據,配合靜態模板中的動態渲染模板,在邊緣節點上渲染出動態 html 後返回給用戶。優點是與後端傳輸數據量小,且不需要後端有 SSR 能力。缺點是需要開發者多維護一套模板邏輯,並且在邊緣節點上做複雜的模板渲染可能會有 cpu 開銷和限制。

用戶和邊緣節點的動態內容交互,分爲兩種形式:

  • 瀑布流式(對應路由配置裏的 WATER_FALL ): 動態內容以瀑布流的形式依次返回。雖然在邊緣節點上多個動態內容加載的操作是並行的,但對於用戶來說,會從上到下依次展示頁面內容。這種方式優點是對 SEO 友好,並且不影響頁面模塊的加載順序。缺點是多個動態模塊時,無法看到整體頁面的框架,首個動態塊的內容會阻塞後續動態塊內容的展示,且頁面底部的 js css 資源無法提前加載和執行。

  • 嵌入式(對應路由配置裏的 ASYNC_INSERT ):靜態內容一次性全部返回,其中動態部分內容會先佔一些坑位。後續動態內容會以 innerHTML 的形式,插入到先前佔的坑中。這種方式優點是頁面底部的 js css 資源無法提前加載和執行,並且頁面可以先看到一個全貌。缺點是對 SEO 不友好,且頁面模塊的執行順序會根據動態塊返回速度有所變化,需要在瀏覽器端頁面邏輯裏做一些判斷和兼容。

邊緣路由

路由配置:

https://g.alicdn.com/edgerender/config.json


{

version: '0.0.1'//配置版本號

origin: 'us-proxy.alibaba.com',

host: 'edge.alibaba.com'

pages: [

{

pageName: 'seo', //頁面名稱標識

match: '/abc/efg/.*', //頁面path匹配正則字符串

renderConf: {

//渲染配置

renderType: 'ESR', //邊緣渲染

templateType: 'FULL_HTML', //模板類型:將SSR出的完整html作爲模板

dynamicMode: 'WATER_FALL|ASYNC_INSERT', // 動態內容append返回方式:瀑布流返回|異步填坑(innerHTML)

templateUrl: ''// 模板url

}

},

{

pageName: 'seo',

match: '/abc/efg/.*',

renderConf: {

renderType: 'ESR',

templateType: 'STATIC', // 靜態模板,可通過cdn url獲取

dynamicMode: 'WATER_FALL|ASYNC_INSERT', // 動態內容append返回方式:瀑布流返回|異步填坑(innerHTML)

templateUrl: 'https://g.alicdn.com/@g/xxx.html'

}

},

{

pageName: 'jump',

match: '/jump/.*',

renderConf: {

renderType: 'REDIRECT_302', // 302跳轉

rewriteUrl: 'https://jump'

}

},

{

pageName: 'proxy',

match: '/proxy/.*',

renderConf: {

renderType: 'PROXY_PASS', // 301跳轉

rewriteUrl: 'https://proxypassurl'

}

}

]

}

路由可以認爲是邊緣計算的一個入口,只有在路由配置中的頁面,纔會走對應的渲染流程。否則頁面會直接走回源,獲取頁面完整內容。上面的 json 是目前設計的路由配置文件。配置文件最終會在一個靜態資源的方式,走覆蓋式發佈發到 assets cdn 上。同時,爲了支持配置發佈灰度,線上會存在灰度版本和全量版本的兩個配置,在路由代碼裏配置固定比例,加載灰度或者全量版本的配置。

目前在路由裏設計了三種渲染模式,分別是流式渲染、重定向和反向代理。重定向和反向代理的配置比較簡單,與 nginx 配置類似,只需要提目標 url 即可。

穩定性

影響範圍控制

  • CDN 開關:域名按區域、按比例切流,同時可隨時從 cdn 上把流量切回統一接入。

  • 邊緣計算 SCOPE 開關:cdn 上配置邊緣計算覆蓋路徑,控制邊緣計算只運行在部分路徑下。

  • 邊緣計算路由開關:邊緣計算中通過讀取路由配置,控制只有部分頁面走流式渲染,否則請求直接走動態加速獲取完整頁面內容。  

異常處理

  • dns 開關,如出現 cdn 嚴重問題,直接 dns 回切到統一接入。

  • 如果邊緣計算基礎功能出現異常,在 cdn 配置平臺上關閉所有路徑的邊緣計算,走默認的動態加速。

  • 如果在進了邊緣渲染,在沒有返回任何響應內容給客戶端前,就出現了錯誤,捕獲錯誤並降級到獲取完整頁面內容。

  • 如果進了邊緣渲染,已經返回了靜態部分的響應給客戶端,然後在邊緣節點了加載動態內容出了問題(超時、http 錯誤碼、與靜態內容版本號不匹配),返回一個 location.reload()   的 script 標籤,並結束響應,讓頁面強制刷新。刷新時可帶上 bypass 邊緣計算的 query 參數以保證刷新時不走邊緣渲染。

灰度

1)邊緣計算代碼灰度

本身平臺支持灰度發佈邊緣計算代碼。

2)路由配置灰度

在邊緣計算代碼裏,根據固定比例,加載灰度版本和正式版本的兩個配置 url。灰度發佈時只發布灰度配置,全量發佈時發佈全量配置。發佈的同時清空 cdn 緩存。

3)頁面內容灰度

給灰度頁面一個特殊的模板版本號,遇到這個版本號的話,就不走邊緣渲染。

平滑發佈

前後端分離的發模式下,有一個普遍存在的問題:平滑發佈。當頁面的靜態資源(js,css )的發佈,不是與後端一起發佈時,可能引起後端返回的 HTML 內容與前端的 js,css 內容不匹配的問題。如果兩者之間的不匹配沒做兼容處理,可能會出現樣式錯亂或者 document 選擇器找不到元素的問題。

解決平滑發佈的一種方式是,在做前後端同時變更的需求時,在代碼上做兼容。這樣先後發佈就不影響頁面可用性。

另一種方式是通過版本號。在後端頁面上手動配置版本號。當有不兼容發佈時,先發前端資源,然後後端手動修改版號,保證只有發佈成功的後端機器, HTML 裏引用的纔是新版本的靜態資源。

平滑發佈的問題其實在分批發布和 Beta 發佈的場景一直存在。只是在 ESR 的場景,我們把靜態部分緩存在 cdn 上,會使前後端不一致的可能性更大。爲了解決這個問題,需要對應業務的開發者進行發佈時的風險識別。如果已經做了兼容,可以不用做特殊處理。但如果沒有兼容,需要在修改頁面模板的版本號,新版本的動態內容,在遇到版本號不匹配的靜態內容時,會放棄本次流式渲染,保證頁面不出動態內容和靜態內容的兼容問題。

邊緣 cdn 服務商

目前各大 cdn 服務商對邊緣計算的支持情況如下:

alicdn

  • 支持類 service worker  環境的邊緣計算,功能滿足需求。

  • 海外節點目前還有限,部分區域性能可與akamai 對標甚至超過,但有些域名性能因節點少的原因還是比 akamai 稍差。

akamai

  • 只支持簡單的請求改寫計算,不滿足邊緣渲染的需求。

  • ESI 可以組裝動態和靜態內容,但不支持流式,動態內容會阻塞首屏。

  • 海外節點多,在一些地區下相比於 alicdn 有性能優勢。

cloudfare

  • 支持類 service worker  環境的邊緣計算,功能滿足需求。

  • 沒有使用經驗,如果要用的話可能流程比較複雜。

落地計劃

我們會在一個典型的首跳場景進行實驗。 目前已經在灰度上線,通過  webpagetest 在印尼測試進方案和不進方案的對比,可以看出優化效果:

  1. ttfb 減少 1s

  2. 白屏時間減少 1s

  3. 核心內容展示時間減少 500ms

webpagetest 對比結果:

https://www.webpagetest.org/video/view.php?id=191202_242 18f13985a6403b911beb7f94d6d1a1940fc13

參考

[1]cloudfare edge worker

(https://blog.cloudflare.com/introducing-cloudflare-workers/)

[2]2016 - the year of web streams

(https://jakearchibald.com/2016/streams-ftw/)

[3]ESI (https://www.w3.org/TR/esi-lang/)

[4]Async Fragments: Rediscovering Progressive HTML Rendering with Marko (https://tech.ebayinc.com/engineering/async-fragments-rediscovering-progressive-html-rendering-with-marko/)

[5]The Lost Art of Progressive HTML Rendering

(https://blog.codinghorror.com/the-lost-art-of-progressive-html-rendering/)

  福利來了  

免費下載

《覆蓋全端業務的大前端技術》

優酷前端業務場景多、技術棧繁雜,對前端工程能力的要求越來越高 阿里文娛將團隊遇到的技術挑 戰以及解決過程做詳細的展開,希望由解決方案的推演抽絲剝繭,一探 優酷前端團隊在支撐業務過程中的技術思考和沉澱,爲讀者帶來一些啓發。

識別下方二維碼,或點擊文末“閱讀原文”立即下載

推薦閱讀

    在阿里雲做前端,是種怎樣的體驗?

    2020年的雙11,阿里需要什麼樣的渲染方案?

    阿里主管通知我試用期延期……

關注 「阿里技術」

把握前沿技術脈搏

戳我,下載電子書。

相關文章