經過優化頁面性能,提高用戶的體驗,一直是咱們追求的目標。咱們能夠經過瀏覽器緩存、預加載、預渲染等各類方案,來提高頁面的訪問性能和體驗。但在實際業務場景中,有一類頁面一直是性能優化的老大難,那就是首跳頁面。即用戶是第一次訪問網站的場景。
對於 web 頁面來講,首跳場景(例如 SEO、付費引流)的性能廣泛比二跳場景下要差。緣由有多種,主要是首跳用戶在鏈接複用,和本地資源緩存利用方面,有很大的劣勢。首跳場景下,不少在端上的優化手段(預加載,預執行,預渲染等)沒法實施。
在客戶端緩存能力沒法利用的狀況下,利用 cdn 距離用戶近的特性,多是一個性能優化的方向。接下來將介紹幾種常見的性能優化方案,並引出咱們提出的邊緣渲染方案。javascript
爲了性能優化考慮,咱們通常都會經過服務端渲染(SSR) ,將首屏動態內容直接服務端輸出。
css
爲了減小白屏時間,考慮利用 CDN 的邊緣緩存能力,能夠把頁面 html 直接緩存在 cdn 節點上。但對於大部分場景來講,頁面的主體內容都是動態,或者個性化的,把所有 html 內容緩存在 cdn 上對於業務影響較大,頗有少場景能接受。那麼換個思路,只把 html 靜態部分緩存在 cdn 上呢?其實這個思路也是一個很常見的操做,即把 html 的靜態框架部分緩存在 cdn 上,讓用戶能快速看到部份內容,而後再在客戶端發起異步請求,獲取動態內容而且渲染(CSR)。CSR + CDN 模式下的渲染時空圖以下:
html
CSR + CDN 的方式,很好地解決了白屏時間問題,但帶來了動態內容展現的延時。之因此有這個問題,是由於咱們把頁面的動態內容和靜態內容分割到了兩個階段中,而且是串行的,並且串行過程當中還穿插了 js 的下載和執行。有什麼辦法把動態內容和靜態內容在 CDN 上整合起來呢?前端
ESI(Edge Side Include) 給了咱們一個很好的思路啓發,ESI 最初也是 CDN 服務商們提出的規範,可經過 html 標籤里加特定的動態標籤,可以讓頁面的靜態內容緩存在 cdn 上,動態內容能夠自由組裝。ESI 的渲染時空圖以下:
java
雖然 ESI 的效果不符合咱們預期,但給了咱們很好的思考方向。若是能把 ESI 改形成可先返回靜態內容,動態內容在 CDN 節點獲取到以後,再返回給頁面,就能夠保證白屏時間短而且動態內容返回不推遲。若是要實現相似於流式 ESI 的效果,要求在 CDN 上能對請求進行細粒度的操做,以及流式的返回。CDN 節點上支持這麼複雜的操做嗎?答案是確定的:邊緣計算。目前一些 CDN 服務商已提供完善的邊緣計算能力(cloudfare已經支持,alicdn 也已有內測版本支持,並即將對外開放),咱們能夠在 CDN 上作相似於瀏覽器的 service worker 的操做,可對請求和響應作靈活的編程。react
基於邊緣計算的能力,咱們有了一種新的選擇:邊緣流式渲染方案。方案詳情以下nginx
方案的核心思想是,藉助邊緣計算的能力,將靜態內容與動態內容以流式的方式,前後返回給用戶。cdn 節點相比於 server,距離用戶更近,有着更短的網絡延時。在 cdn 節點上,將可緩存的頁面靜態部分,先快速返回給用戶,同時在 cdn 節點上發起動態部份內容請求,並將動態內容在靜態部分的響應流後,繼續返回給用戶。最終頁面渲染的時空圖以下:
web
從上圖能夠看出,cdn 邊緣節點能夠很快地返回首字節和頁面靜態部份內容,而後動態內容由 cdn 發起向 server 起並流式返回給用戶。方案有如下特色:redis
目前在 alicdn 上對主搜頁面作了一個 demo (edge-routine.m.alibaba.com/)(由於 demo 頁面可能會頻繁), 下面是在不一樣網絡(經過 charles 的 network throttle 配置限速)狀況下,與原始頁面的加載對比:編程
從上面結果能夠看出,在網速越慢的狀況下,經過 cdn 流式渲染的最終主要元素出來的時間比原始 ssr 的方式出來得越早。這與實際推論也符合,由於網絡越慢,靜態資源加載時間越慢,對應的瀏覽器提早加載靜態資源帶來的效果也越明顯。另外,無論在什麼網絡狀況下,cdn 流式渲染方式的白屏時間要短不少。
模板就是一個相似於包含 ESI 區塊的語法,基於模板,會將須要動態請求的內容提取出來,把能夠靜態返回的內容分離出來並緩存起來。因此模板本質上定義了頁面動態內容和靜態內容。
在流式渲染過程當中,會從上到下解析頁面模板,若是是靜態內容,直接返回給用戶,若是遇到動態內容,會執行動態內容的 fetch 邏輯。整個過程當中可能有靜態和動態內容交替出現。
設計有如下幾種類型的模板。
這種模板對現有業務的侵入性最小,只須要在現有的 SSR 頁面內容里加上必定的標籤,便可把頁面中動態部分申明出來:
<html>
<head>
<link rel="stylesheet" type="text/css" href="index.css">
<script src="index.js"></script>
<meta name="esr-version" content="0.0.1"/>
</head>
<body>
<div>staic content....</div>
<script type="esr/snippet/start" esr-id="111" content="SLICE"></script>
<div>
dynamic content1....
</div>
<script type="esr/snippet/end"></script>
<div>staic content....</div>
<script type="esr/snippet/start" esr-id="222" content="https://test.alibaba.com/snippet/222"></script>
<div id="222">
dynamic content2....
</div>
<script type="esr/snippet/end"></script>
</body>
</html>
複製代碼
這咱模板須要單獨把模板發到 cdn 上(將來若是渲染層接入了 FASS 網關和 SSR ,在這塊能夠和他們共用模板內容,而且在工做流中發佈模板時自動同步到 cdn 上一份,同時清空 cdn 上緩存)。動態的內容有兩種渲染方式。一種是利用後端 SSR 出來的動態 html 片段,另外一種是後端提供動態數據,由邊緣節進行動態html片段渲染。
使用 SSR 動態 html 片段的好處是,不須要在邊緣上作 html 模板渲染,而且不須要開發者寫兩套模板邏輯。缺點是須要後端有 SSR 能力,而且動態內容傳輸體積較大。
使用邊緣節點渲染動態 html 內容的好處是,後端只須要提供動態數據,不須要 SSR 能力(但前端要有 CSR 的能力作降級兜底),而且傳輸的動態內容體積小。切點是邊緣節點上沒法流式透傳動態內容,須要等完整下載到邊緣節點上,處理後再返回給用戶。
<html>
<head>
<link rel="stylesheet" type="text/css" href="index.css">
<script src="index.js"></script>
</head>
<body>
<div>staic content....</div>
<script type="esr/block" esr-id="111" content="https://test.alibaba.com/snippet/111"></script>
<div>staic content....</div>
<script type="esr/template" esr-id="222" content="https://test.alibaba.com/api/data"></script>
</body>
</html>
複製代碼
靜態內容來自於模板。對於不一樣模板類型,獲取靜態內容的方式不同。對於 「原始 HTML」 類型的模板,靜態內容會從首次動態請求返回的完整 HTML 中,根據 html 註釋標記提取出來,並存儲到 edge 緩存上。對於 「靜態模板」,會經過拉取 CDN 的的模板文件 ,並存儲到 edge 緩存上。靜態內容有緩存過時時間和版本號。
模板一開始的靜態內容會在響應時直接返回給用戶。後續的靜態內容(例如 html 和 body 的閉合標籤)有兩種方式:
a. 一種是等待動態內容返回後,再寫到響應流中。這種方式對 SEO 比較友好,但缺點是動態內容會阻塞住後續靜態內容,而且若是有多個動態內容區塊的話,沒法實現先返回的動態模板先展現,只能依次展現.
b. 另外一種方式是先把靜態內容徹底返回,而後動態內容以類 bigpipe 的方式,經過腳本把內容插入到對應的坑位。這種方式的優勢是靜態內容能夠一開始就完整展現,且多個動態內容能夠先到先展現。缺點是對 SEO 不友好(由於動態內容是能進 js 插進去的)
動態內容是在渲染過程當中,解析到須要動態獲取的區域,會在 edge 上發起動態內容請求。動態內容支持以動態加速的形式到達服務端(源站)。連續節點與後端的動態的內容交互,分爲三種方式:
a. 第一種是後端動態內容返回的是全量的頁面,須要經過註釋標記來從內容中提取。這種方式的優勢是對現有業務侵入較小,缺點是動態內容傳輸體積大,而且須要下載完整 html 後再截取動態內容;
b. 第二種是後端動態內容只返回動態區塊的內容,這種方式的優勢是能夠將動態響應流式返回給用戶,缺點時須要頁面單獨對外提供一個只返回動態區塊內容的 url。
c. 第三種是後端動態內容只返回數據,配合靜態模板中的動態渲染模板,在邊緣節點上渲染出動態 html 後返回給用戶。優勢是與後端傳輸數據量小,且不須要後端有 SSR 能力。缺點是須要開發者多維護一套模板邏輯,而且在邊緣節點上作複雜的模板渲染可能會有 cpu 開銷和限制。
用戶和邊緣節點的動態內容交互,分爲兩種形式:
a. 瀑布流式(對應路由配置裏的 WATER_FALL
): 動態內容以瀑布流的形式依次返回。雖然在邊緣節點上多個動態內容加載的操做是並行的,但對於用戶來講,會從上到下依次展現頁面內容。這種方式優勢是對 SEO 友好,而且不影響頁面模塊的加載順序。缺點是多個動態模塊時,沒法看到總體頁面的框架,首個動態塊的內容會阻塞後續動態塊內容的展現,且頁面底部的 js css 資源沒法提早加載和執行。
b. 嵌入式(對應路由配置裏的 ASYNC_INSERT
):靜態內容一次性所有返回,其中動態部份內容會先佔一些坑位。後續動態內容會以 innerHTML 的形式,插入到先前佔的坑中。這種方式優勢是頁面底部的 js css 資源沒法提早加載和執行,而且頁面能夠先看到一個全貌。缺點是對 SEO 不友好,且頁面模塊的執行順序會根據動態塊返回速度有所變化,須要在瀏覽器端頁面邏輯裏作一些判斷和兼容。
路由配置:
g.alicdn.com/edgerender/… (只是一個設想的 url,是一個發佈到靜態 cdn 上的 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 便可。
location.reload()
的 script 標籤,並結束響應,讓頁面強制刷新。刷新時可帶上 bypass 邊緣計算的 query 參數以保證刷新時不走邊緣渲染先後端分離的發模式下,有一個廣泛存在的問題:平滑發佈。當頁面的靜態資源(js, css )的發佈,不是與後端一塊兒發佈時,可能引發後端返回的 HTML 內容與前端的 js ,css 內容不匹配的問題。若是二者之間的不匹配沒作兼容處理,可能會出現樣式錯亂或者 document 選擇器找不到元素的問題。
解決平滑發佈的一種方式是,在作先後端同時變動的需求時,在代碼上作兼容。這樣前後發佈就不影響頁面可用性。
另外一種方式是經過版本號。在後端頁面上手動配置版本號。當有不兼容發佈時,先發前端資源,而後後端手動修改版號,保證只有發佈成功的後端機器, HTML 裏引用的纔是新版本的靜態資源。
平滑發佈的問題其實在分批發布和 Beta 發佈的場景一直存在。只是在 ESR 的場景,咱們把靜態部分緩存在 cdn 上,會使先後端不一致的可能性更大。爲了解決這個問題,須要對應業務的開發者進行發佈時的風險識別。若是已經作了兼容,能夠不用作特殊處理。但若是沒有兼容,須要在修改頁面模板的版本號,新版本的動態內容,在遇到版本號不匹配的靜態內容時,會放棄本次流式渲染,保證頁面不出動態內容和靜態內容的兼容問題。
目前各大 cdn 服務商對邊緣計算的支持狀況以下:
目前經過 demo,已經驗證了方案的可行性。正在阿里巴巴國際站上的實際業務場景作實驗。將來將會分享更完善和豐富的方案(好比直接在邊緣節點上進行 react 組件渲染)和實際線上的運行效果。