當前幾種常見的前端性能優化方案仍然不可避免地會存在一些缺點。本文在 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 在印尼測試進方案和不進方案的對比,可以看出優化效果:
ttfb 減少 1s
白屏時間減少 1s
核心內容展示時間減少 500ms
編輯:hfy
-
CSR
+關注
關注
3文章
118瀏覽量
69639 -
CDN
+關注
關注
0文章
314瀏覽量
28801 -
邊緣節點
+關注
關注
0文章
13瀏覽量
7646 -
邊緣計算
+關注
關注
22文章
3092瀏覽量
48963
發布評論請先 登錄
相關推薦
評論