Vue SSR 性能優(yōu)化實(shí)踐
齊云雷,微醫(yī)云服務(wù)團(tuán)隊(duì)前端工程師,本文是作者在《第二屆繽紛前端技術(shù)沙龍》分享主題的文字版。
估計(jì)大部分讀者對(duì)標(biāo)題中的性能優(yōu)化更感興趣,可惜我分享的重點(diǎn)其實(shí)更多在于實(shí)踐。實(shí)踐有深有淺,下面介紹的時(shí)候會(huì)存在比較大的側(cè)重。當(dāng)然,篇幅不代表難易程度,考慮到不少信息已經(jīng)有非常棒的公開資料,對(duì)這一部分我只會(huì)簡(jiǎn)單提起關(guān)鍵詞,希望能起到拋磚引玉的作用。
本次分享圍繞著 Vue SSR 和相關(guān)業(yè)務(wù)增長(zhǎng)的背景,向大家展示我們做過了哪些嘗試,以及一些踩坑經(jīng)歷,希望能給中小規(guī)模的團(tuán)隊(duì)帶來一定的參考價(jià)值。
對(duì)于大型團(tuán)隊(duì)來說,這里基礎(chǔ)的優(yōu)化可能已經(jīng)習(xí)以為常。并且許多人為了榨干機(jī)器性能,追求極致,已經(jīng)有了各式各樣的成功探索。我們從中學(xué)習(xí)到了很多思路,但不管是多么優(yōu)秀的想法,多多少少也有著各自的局限性,適合他們的不一定適合我們。
受限于分享人的經(jīng)驗(yàn)和水平,本文大多是從 Server 的角度思考如何解決問題,不免存在疏漏,望讀者大大們批評(píng)指正。
一、實(shí)踐背景
實(shí)踐和背景息息相關(guān),在展開篇幅之前,先交代一下我們進(jìn)行性能優(yōu)化的背景。
首先,不得不提的就是行業(yè)背景,順便給公司貼一則介紹:微醫(yī)是一家互聯(lián)網(wǎng)醫(yī)療企業(yè),在數(shù)字健康領(lǐng)域的前線奮戰(zhàn)十年有余,為廣大用戶提供線上線下融合的一站式醫(yī)療和健保服務(wù)。在今年以前,醫(yī)療行業(yè)的峰值流量是遠(yuǎn)遠(yuǎn)小于其他服務(wù)業(yè)的,特別是真正核心的醫(yī)療、醫(yī)藥、醫(yī)檢等業(yè)務(wù),不太可能出現(xiàn)高并發(fā)的情況。
說到這兒,大家可能都明白了,2020 年出現(xiàn)了一個(gè)重要的轉(zhuǎn)折點(diǎn)——新冠疫情。

業(yè)務(wù)背景

第一個(gè)問題,流量上漲,更重要的是不知道會(huì)有多少流量涌進(jìn)來。我們當(dāng)然不能給機(jī)器無限擴(kuò)容。
第二個(gè)問題,真正開始面向全國(guó)區(qū)域的用戶。使用云服務(wù)器的團(tuán)隊(duì)還可以添加不同地區(qū)的節(jié)點(diǎn),但微醫(yī)大部分業(yè)務(wù)使用的是自己的機(jī)房,甚至都在杭州附近。其他地區(qū)的用戶距離太遠(yuǎn)了,網(wǎng)絡(luò)體驗(yàn)差,訪問速度慢。
技術(shù)背景

鑒于知曉 SSR 技術(shù)的小伙伴對(duì)此圖已經(jīng)非常了解,所以我只給不太了解的朋友提一下服務(wù)端渲染的優(yōu)缺點(diǎn)。
SSR 優(yōu)勢(shì)

由于服務(wù)端直出頁面,從而縮短內(nèi)容到達(dá)時(shí)間、減少首頁白屏。
直出的頁面包含了頁面關(guān)鍵數(shù)據(jù)信息,對(duì)搜索引擎的爬蟲更友好,利于提高網(wǎng)站搜索排名。恰恰因?yàn)橹髁鞯呐老x不會(huì)解析 js 腳本,所以一些注重 SEO 的應(yīng)用不得不上 SSR。
另外提一句,現(xiàn)在的 SSR 渲染一般指的都是同構(gòu)渲染,可以兼顧客戶端渲染的大部分優(yōu)勢(shì)。
SSR 缺陷

SSR 的缺點(diǎn)也很突出,首要的問題自然是服務(wù)端壓力比客戶端大,這符合拆東補(bǔ)西的規(guī)律。SSR 通過壓榨服務(wù)端的性能提升客戶端首屏體驗(yàn),而渲染頁面屬于計(jì)算密集型的任務(wù),對(duì)于 Node.js 編寫的服務(wù)而言,效率實(shí)在捉襟見肘。頁面組件復(fù)雜的情況,少量的并發(fā)就能拖垮進(jìn)程。
另一個(gè)是潛在的問題在于影響開發(fā)體驗(yàn)。毫無后端經(jīng)驗(yàn)的前端團(tuán)隊(duì)可能對(duì)服務(wù)層代碼的把控力不足,貿(mào)然使用 SSR 風(fēng)險(xiǎn)非常大。不過由于我們將 SSR 服務(wù)端和客戶端進(jìn)行較好的解耦,對(duì)于開發(fā)體驗(yàn)而言,與 CSR 并沒有太大的區(qū)別。
二、方案討論
拿到問題之后,先來分解問題。在這兒借用一張圖,把一個(gè) SSR 請(qǐng)求的生命周期分為三個(gè)階段,主要是把執(zhí)行渲染的部分從整體中抽出來

FCP:首次內(nèi)容繪制時(shí)間,TTI:可交互時(shí)間
不過需要解釋一下,通常的拆解方式是用戶從瀏覽器發(fā)起的請(qǐng)求階段、服務(wù)器渲染階段和響應(yīng)階段,但這樣的話,戰(zhàn)線被拉長(zhǎng),可優(yōu)化的范圍太大。而我們的核心訴求是緩解服務(wù)器的壓力,并不是一味地追求極限數(shù)值。
所以,我們特地縮窄了視野,僅僅從服務(wù)器的立場(chǎng),將這三個(gè)階段分別理解為
請(qǐng)求已經(jīng)到到達(dá)服務(wù)還未執(zhí)行渲染 開始渲染計(jì)算,直到渲染完成 服務(wù)器處理響應(yīng)
SSR 最根本的性能問題,其實(shí)還是在中間這一步,密集的 CPU 運(yùn)算。
所以 Vue3 帶來了一個(gè)變革,保守能讓渲染性能提高 2 到 3 倍。但在 Vue3 到來之前,我們有辦法提高這一步的性能嗎?

Vue3 優(yōu)化的一大原因是盡可能將部分 VDOM 的渲染改為字符串拼接,我們可以按照同樣的思路,改造 Vue2。不過話說回來,Vue 整個(gè)渲染過程能讓我們干預(yù)的地方很少,更不用說涉及底層算法的替換,具體該如何實(shí)施呢?
在 Vue3 推出之前,已經(jīng)有許多前輩這樣做了。例如去年的 Tweb 分享上,有講師分享了一套將 SSR 性能優(yōu)化到極致的方案,使用自研的編譯器替換 vue-loader,在編譯時(shí)根據(jù) Vue 語法樹生成線性字符串的拼接,后續(xù)不再需要構(gòu)造和遍歷 VDOM。
但是,這樣做的普遍后果是難以兼容 Vue 全部的語法,乃至 Vuex 也無法繼續(xù)使用。可惜我們頁面的邏輯非常復(fù)雜,也重度依賴 Vuex 管理狀態(tài),如果為了嘗試這樣的方案而對(duì)項(xiàng)目進(jìn)行大幅改造,性價(jià)比顯得太低。更何況已經(jīng)有著未來可期的 Vue3,不如先把這個(gè)棘手的問題放一放,讓我們把精力優(yōu)先投入到另外兩個(gè)可優(yōu)化的階段。
三、常規(guī)優(yōu)化
性能優(yōu)化必然是始終在進(jìn)行的,有一些常規(guī)方法早就投入了使用,我們按渲染階段來盤點(diǎn)一二。
渲染前

對(duì)應(yīng)前面所說的,從服務(wù)器的視角出發(fā),有以下操作可以讓渲染任務(wù)執(zhí)行前就減輕一些負(fù)擔(dān)。
第一,多級(jí)緩存。接口數(shù)據(jù)、組件和最終吐出的頁面均可緩存。這一步的核心是繼續(xù)把 CPU 壓力轉(zhuǎn)移到內(nèi)存,前者可以縮短請(qǐng)求鏈路,后兩個(gè)可以減少渲染計(jì)算量。緩存的方式非常靈活,簡(jiǎn)陋一點(diǎn)就直接用內(nèi)存緩存,配合 LRU 算法基本夠用。復(fù)雜的場(chǎng)景就需要上 Redis 等內(nèi)存數(shù)據(jù)庫。
第二,請(qǐng)求復(fù)用。我們通常使用封裝好的 Request、Axios 等庫完成請(qǐng)求,最值得留意的選項(xiàng)就是使用開啟了 keep-alive 的 http-agent,它能讓后續(xù)的請(qǐng)求復(fù)用之前建立的連接,減少重復(fù)的握手次數(shù)。
第三點(diǎn),降級(jí)熔斷。如果沒有降級(jí),雖然 Node.js 節(jié)點(diǎn)比較穩(wěn)定,不至于因?yàn)閴毫Χ礄C(jī),但卻會(huì)出現(xiàn)請(qǐng)求堆積,導(dǎo)致 Node.js 請(qǐng)求后端接口超時(shí),服務(wù)將呈現(xiàn)不可用狀態(tài)。
回看上面這些做法,實(shí)現(xiàn)起來會(huì)遇到什么問題呢?
對(duì)于我們團(tuán)隊(duì)來說,多數(shù)組件依賴全局狀態(tài),組件緩存的適用場(chǎng)景不多,因此我們主要使用頁面緩存。如果業(yè)務(wù)存在高度定制的頁面,不同用戶之間存在無法復(fù)用的緩存,可能會(huì)消耗巨大的內(nèi)存。內(nèi)存也是服務(wù)器寶貴的資源,但比其成本和性能來說,使用不當(dāng)還會(huì)面臨更大的風(fēng)險(xiǎn)。緩存是一個(gè)非常復(fù)雜的課題,它的副作用在后面的小節(jié)還會(huì)再做介紹。簡(jiǎn)而言之,我們必須做好充分的準(zhǔn)備才有可能規(guī)避緩存帶來的隱患。
再談降級(jí)。一方面,降級(jí)會(huì)將 SSR 服務(wù)的壓力釋放到客戶端,而瀏覽器渲染頁面時(shí)無法讀取 SSR 服務(wù)層緩存的接口數(shù)據(jù),改為直接請(qǐng)求后端服務(wù)。這是對(duì) SSR 進(jìn)程是一種保護(hù),但對(duì)后端應(yīng)用卻不是件好事。另一方面,如果僅僅在發(fā)生異常時(shí)降級(jí),那么遇到請(qǐng)求堆積而超時(shí),降級(jí)沒能起到緩解壓力的作用,頁面整體響應(yīng)時(shí)間也被拖長(zhǎng)。因此,降級(jí)策略也需要靈活而完善地落實(shí)。
渲染后

在頁面渲染之后,我們會(huì)做一系列體驗(yàn)上的優(yōu)化,而其中稱得上性能優(yōu)化的主要是這兩點(diǎn)。
可以把 CDN 簡(jiǎn)單理解為一組代理服務(wù)器,所謂的 CDN 加速靜態(tài)資源,得益于資源被緩存到了代理服務(wù)器。通常靜態(tài)資源的內(nèi)容不會(huì)頻繁變更,因此比動(dòng)態(tài)的頁面數(shù)據(jù)更加適合緩存。
需要注意的是,gzip 壓縮有多種方式。近期就發(fā)生過出現(xiàn) CDN 將 gzip 響應(yīng)頭去掉的問題,導(dǎo)致壓縮沒有生效,內(nèi)容大小差了十幾 KB,頁面響應(yīng)時(shí)間卻差了 400ms。
四、深度實(shí)踐
前面介紹的是業(yè)務(wù)增長(zhǎng)之前所做過的優(yōu)化,但真正頂住壓力的辦法還在后面。
基礎(chǔ)網(wǎng)絡(luò)調(diào)優(yōu)
內(nèi)網(wǎng)調(diào)用

這是一個(gè)早期被疏忽的基礎(chǔ)問題。
最初,我們 SSR 服務(wù)器通過公網(wǎng)的網(wǎng)關(guān)域名來訪問后端接口,但是從公網(wǎng)解析域名的效率極低。雖然可以 keep-alive 在一定程度復(fù)用連接,但仍然存在周期性建立連接的過程,此時(shí)的網(wǎng)絡(luò)體驗(yàn)就很差。
為了穩(wěn)定縮短接口調(diào)用時(shí)間,我們將公網(wǎng)的域名解析改為配置 host 直接訪問網(wǎng)關(guān) IP,但限于網(wǎng)關(guān)配置,用得仍然是 https 協(xié)議。后來和運(yùn)維協(xié)商,才變更為使用 http 形式的內(nèi)網(wǎng)域名調(diào)用。

這里稍微引申一個(gè)話題。在運(yùn)維介入之前,使用 IP 訪問網(wǎng)關(guān)存在著一定的風(fēng)險(xiǎn)。如果只有單個(gè) IP,容易發(fā)生單點(diǎn)故障;而如果有多個(gè) IP,就需要面臨負(fù)載均衡和容災(zāi)的處理。
負(fù)載均衡主要是避免出現(xiàn)擁堵,這要求我們應(yīng)該記錄多個(gè)網(wǎng)關(guān) IP,通過輪詢?cè)L問來確保流量均勻分發(fā)到多個(gè)網(wǎng)關(guān)服務(wù)器。
容災(zāi)則要求我們?cè)谀硞€(gè)節(jié)點(diǎn)故障時(shí),能夠自動(dòng)剔除故障節(jié)點(diǎn),并在其恢復(fù)之后重新加入備選項(xiàng)。
除了上述的基本情況,實(shí)際上還存在著流量分配權(quán)重的問題。試想,不同服務(wù)器的性能、網(wǎng)絡(luò)帶寬等等都可能存在差異。我們想讓能者多勞,怎么辦?

如果沒有處理這種情況的經(jīng)驗(yàn),推薦使用 Nginx 的加權(quán)平滑輪詢,這也是它默認(rèn)的負(fù)載均衡算法。
加權(quán)和輪詢很容易理解,什么是平滑呢?對(duì)于一個(gè)高權(quán)重的節(jié)點(diǎn),經(jīng)過它的流量不會(huì)忽高忽低,被使用的頻率越穩(wěn)定,其負(fù)載均衡的算法越是平滑。
由于算法實(shí)現(xiàn)非常簡(jiǎn)單,不知情的同學(xué)可以自行查找資料。上面描述的依然是一個(gè)非?;A(chǔ)的模型,適用于網(wǎng)絡(luò)環(huán)境的過度,最終還是讓網(wǎng)關(guān)和運(yùn)維提供支持為好。
擴(kuò)展多級(jí)緩存
對(duì)于高并發(fā)的場(chǎng)景,我們都知道緩存頁面的重要性,具體又該如何處理呢?
隨著渲染方案的不同,主要也是分成兩個(gè)方向,一個(gè)是以 CSR 為主體的,可以將全部頁面部署到 CDN,并開啟 CDN 緩存。另一個(gè)是 SSR 為主體的,大多靠自身的緩存中間件硬抗,靠龐大的 Redis 和 MQ 集群,以設(shè)計(jì)傳統(tǒng)后端服務(wù)器的思路來處理。
在此之前,微醫(yī)的渲染服務(wù)比較簡(jiǎn)單,幾乎只有內(nèi)存緩存,導(dǎo)致 Node.js 進(jìn)程內(nèi)存占用比較夸張。如今面臨 CDN 緩存和引入 Redis 集群兩個(gè)方向的選擇,其實(shí)也不是選擇,兩個(gè)優(yōu)化都值得做,我們優(yōu)先采取了對(duì)于當(dāng)前架構(gòu)最為溫和的 CDN 緩存。
CDN 緩存介紹
剛才講靜態(tài)資源緩存的時(shí)候,對(duì) CDN 已經(jīng)有過初步介紹了,但它的功能不止用于緩存靜態(tài)資源。本小節(jié)則是講我們?nèi)绾螌?SSR 渲染出來的動(dòng)態(tài)頁面放在 CDN 緩存上,這和靜態(tài)資源有許多不同的關(guān)注點(diǎn)。
接下來通過一系列問答帶諸位走近這個(gè)話題。
為什么接入 CDN

抽象一個(gè)簡(jiǎn)單的請(qǐng)求鏈路,方便理解 CDN 的定位??此圃黾恿艘粚觽鬏敵杀荆鋵?shí)沒有那么簡(jiǎn)單。
CDN 利用自身廣大的服務(wù)器資源,能動(dòng)態(tài)優(yōu)化訪問路由、就近提供訪問節(jié)點(diǎn),以更低延遲、更高帶寬從源站獲取數(shù)據(jù),優(yōu)化了網(wǎng)絡(luò)層面的用戶體驗(yàn)。
出于成本問題,大部分公司不會(huì)自己搭建 CDN 集群,而是使用了大廠提供的 CDN 服務(wù)。
我們把 CDN 節(jié)點(diǎn)放大,進(jìn)一步體會(huì)它的作用
在沒有緩存的前提下,鏈路上存在一定損耗,總體效果仍要具體分析,不一定帶來正面優(yōu)化。但一旦引入了緩存,就產(chǎn)生了質(zhì)的變化
為什么開啟 CDN 緩存

CDN 能夠緩存用戶請(qǐng)求到的資源,并且可以包含 HTTP 響應(yīng)頭。在下一次任意用戶請(qǐng)求同樣的資源時(shí),用緩存的資源直接響應(yīng)用戶,節(jié)省了本該由源站處理的所有后續(xù)步驟。
簡(jiǎn)單來說,就是截短了請(qǐng)求鏈路。
如何開啟 CDN 緩存

在不考慮自研 CDN 的情況下,開啟 CDN 緩存的步驟非常簡(jiǎn)單:
域名接入 CDN 服務(wù),同時(shí)針對(duì)路徑啟用緩存 在源站設(shè)置 Cache-Control 響應(yīng)頭,為了更靈活地控制緩存規(guī)則,但并不是必須
一般兩者并非缺一不可,緩存時(shí)間的規(guī)則視 CDN 服務(wù)商而定。
哪些服務(wù)可以開啟 CDN 緩存

大部分網(wǎng)站都適合接入 CDN,但 SSR 頁面只有滿足一定條件才可以開啟 CDN 緩存。因?yàn)殚_啟緩存后,同一個(gè) url 下所有用戶訪問的都是同一份資源。并且頁面數(shù)據(jù)應(yīng)當(dāng)對(duì)時(shí)效性要求不高,至少能接受分鐘級(jí)的延遲。
CDN 緩存優(yōu)化
用來衡量緩存效果的重要指標(biāo)是緩存命中率,在正式設(shè)置 CDN 緩存之前,我們?cè)賮砹私鈳讉€(gè)提高緩存命中率的要點(diǎn)。這些要點(diǎn)也適合作為評(píng)估系統(tǒng)是否應(yīng)該接入 CDN 緩存的標(biāo)準(zhǔn)。

(1)緩存時(shí)間
提高 Cache-Control 的時(shí)間是最有效的措施,緩存持續(xù)時(shí)間越久,緩存失效的機(jī)會(huì)越少。即使頁面訪問量不大的時(shí)候也能顯著提高緩存命中率。
需要注意,Cache-Control 只能告知 CDN 該緩存的時(shí)間上限,并不影響它被 CDN 提早淘汰。流量過低的資源,很快會(huì)被清理掉,CDN 用逐級(jí)沉淀的緩存機(jī)制保護(hù)自己的資源不被浪費(fèi)。
(2)忽略 URL 參數(shù)
用戶訪問的完整 URL 可能包含了各種參數(shù),CDN 默認(rèn)會(huì)把它們當(dāng)作不同的資源,每個(gè)資源又是獨(dú)立的緩存。
而有些參數(shù)是明顯不合預(yù)期的,例如,頁面鏈接在微信等渠道分享后,末尾被掛上各種渠道自身設(shè)置的統(tǒng)計(jì)參數(shù)。平均到單個(gè)資源的訪問量就會(huì)大大降低,進(jìn)而降低了緩存效果。
部分 CDN 后臺(tái)支持開啟 過濾參數(shù) 選項(xiàng),來忽略 URL ? 后面的參數(shù)。此時(shí)同一個(gè) URL 一律當(dāng)作同一個(gè)資源文件。
(3)主動(dòng)緩存
化被動(dòng)為主動(dòng),才有可能實(shí)現(xiàn) 100% 的緩存命中率。常用的主動(dòng)緩存是資源預(yù)熱,更適合 URL 路徑明確的靜態(tài)文件,動(dòng)態(tài)路由無法交給 CDN 智能預(yù)熱,除非依次推送具體的地址。
應(yīng)用代碼演進(jìn)
談過 CDN 緩存優(yōu)化的幾個(gè)要點(diǎn),便可得知 CDN 后臺(tái)的配置是需要謹(jǐn)慎對(duì)待的。我在實(shí)際操作中,也經(jīng)過了幾個(gè)階段的調(diào)整,可畢竟具體配置方式取決于 CDN 服務(wù)商,因此本文不再深入討論。
現(xiàn)在,我們要把目光轉(zhuǎn)到代碼層的演進(jìn)了。
1. 掌控緩存
代碼配置有一個(gè)前提,即 CDN 后臺(tái)需要開啟讀取源站 Cache-Control 的支持。
而后,只要簡(jiǎn)單地添加響應(yīng)頭,就能從運(yùn)維手中接管設(shè)置 CDN 緩存規(guī)則的主動(dòng)權(quán)。
以 Node.js Koa 中間件為例,全局的初始化版本如下
app.use((ctx,?next)?=>?{
??ctx.set('Cache-Control',?`max-age=300`)
})
當(dāng)然,上述代碼的疏漏是非常多的。在 SSR 應(yīng)用中,不太需要緩存所有的頁面,這就要補(bǔ)充路徑的判斷條件。
2. 控制路徑
雖然 CDN 后臺(tái)也可以配置路徑,但配置方式乃至路徑數(shù)量都有局限性,不如代碼形式靈活。
假如我們只需要緩存 /foo 頁面,就加入 if 判斷
app.use((ctx,?next)?=>?{
??if?(ctx.path?===?'/foo')?{
????ctx.set('Cache-Control',?`max-age=300`)
??}
})
這就陷入了第一個(gè)陷阱,一定要注意路由對(duì) path 的處理。一般地,'/foo' 和 '/foo/' 是兩個(gè)獨(dú)立的 path。可能因?yàn)?ctx.path === '/foo' 而漏掉了請(qǐng)求 path 為 /foo/ 的處理。
3. 補(bǔ)充路徑
偽代碼如下
app.use((ctx,?next)?=>?{
??if?([?'/foo',?'/foo/'?].includes(ctx.path))?{
????ctx.set('Cache-Control',?`max-age=300`)
??}
})
此外,CDN 后臺(tái)的配置也需要規(guī)避這個(gè)問題。在騰訊 CDN 中,目錄和文件適用于不同的頁面路徑。
4. 忽略降級(jí)頁面
在服務(wù)端渲染失敗時(shí),為了提高容錯(cuò),我們會(huì)返回降級(jí)之后的頁面,轉(zhuǎn)為客戶端渲染。如果因?yàn)榕既坏木W(wǎng)絡(luò)波動(dòng),導(dǎo)致 CDN 緩存了降級(jí)頁面,將在一段時(shí)間內(nèi)持續(xù)影響用戶體驗(yàn)。
所以我們又引入了 ctx._degrade 自定義變量,標(biāo)識(shí)頁面是否觸發(fā)了降級(jí)
app.use(async?(ctx,?next)?=>?{
if?([?'/foo',?'/foo/'?].includes(ctx.path))?{
ctx.set('Cache-Control',?`max-age=300`)
??}
??await?next()
??//?頁面降級(jí)時(shí),取消緩存
??if?(ctx._degrade)?{
????ctx.set('Cache-Control',?'no-cache')
??}
})
沒錯(cuò),這并不是最后一個(gè)陷阱。
5. Cookie 和狀態(tài)治理
上面已經(jīng)提到了 CDN 可以選擇性地緩存 HTTP 響應(yīng)頭,可是此選項(xiàng)是對(duì)整個(gè)域名生效,又普遍需要開啟。
新的問題正是來自一個(gè)不希望被緩存的響應(yīng)頭。
應(yīng)用 Cookie 的設(shè)置依賴于響應(yīng)頭 Set-Cookie 字段,Set-Cookie 的緩存直接會(huì)導(dǎo)致所有用戶的 Cookie 被刷新為同一個(gè)。
有多個(gè)解決方案,一是該頁面不要設(shè)置任何 Cookie,二是代理層過濾掉 Set-Cookie 字段。可惜騰訊 CDN 目前還不支持對(duì)響應(yīng)頭的過濾,這步容錯(cuò)必須自己操作。
app.use(async?(ctx,?next)?=>?{
const?enableCache?=?[?'/foo',?'/foo/'?].includes(ctx.path)
??if?(enableCache)?{
????ctx.set('Cache-Control',?`max-age=300`)
??}
??await?next()
??//?頁面降級(jí)時(shí),取消緩存
??if?(ctx._degrade)?{
????ctx.set('Cache-Control',?'no-cache')
??}
??//?緩存頁面不設(shè)?Set-Cookie
??else?if?(enableCache)?{
????ctx.res.removeHeader('Set-Cookie')
??}
})
上面增加的代碼旨在頁面響應(yīng)前移除 Set-Cookie,但是中間件的加載順序是難以控制的。特別是一些(中間件)插件,會(huì)隱式地創(chuàng)建 Cookie,這讓 Cookie 的清理工作異常麻煩。如果后續(xù)維護(hù)人員不知情,很可能將 Set-Cookie 重新加入到響應(yīng)頭中。所以,這種擦屁股的工作,盡量在代理層處理,而不是放在代碼邏輯中。
除了 Cookie,還可能面臨其他狀態(tài)信息管理問題。比如在 Vuex 的 renderState 中存放請(qǐng)求用戶的登錄狀態(tài),此時(shí) HTML 頁面嵌入了用戶信息,如果被 CDN 緩存,在客戶端將發(fā)生和未清除 Set-Cookie 相似的問題。類似的例子還有很多,它們的解決思路非常相像,接入 CDN 緩存前務(wù)必對(duì)狀態(tài)信息做好全面的排查。
6. 定制緩存路徑
現(xiàn)在功能總算趨于正常,然而緩存規(guī)則復(fù)雜多變,如果想設(shè)置更多頁面,還要單獨(dú)定制緩存時(shí)間呢?這段代碼仍需要不斷地變動(dòng)。
例如,我們只想緩存 /foo/:id,而不緩存 /foo/foo、/foo/bar 等路徑。
注意 CDN 后臺(tái)可能只支持配置一個(gè) /foo/ 開頭的緩存路徑,這就要求我們需要將 ctx.set('Cache-Control', 'no-cache') 做為默認(rèn)處理,加在中間件的第一行。
又比如,我們想緩存 /foo 頁面 5 分鐘,/bar 頁面 1 天,又需要引入一個(gè)時(shí)間配置表。
這個(gè)中間件和相應(yīng)的配置就會(huì)變得越來越難以維護(hù)。
因此,我們換一種思路,緩存規(guī)則不再交給中間件,而是轉(zhuǎn)到 Vue SSR 的 entry-server,通過 metadata 可以做到頁面級(jí)別的配置。由于 SSR 方案的差異性,不再贅述具體實(shí)現(xiàn)。
7. 緩存失效
緩存失效是個(gè)中性詞,如何處理 CDN 緩存失效,此中利弊不得不慎重權(quán)衡。
一方面,它會(huì)間歇增加服務(wù)壓力,在 Serverless 應(yīng)用中還會(huì)提高計(jì)算成本。而另一方面,許多場(chǎng)景我們不得不主動(dòng)觸發(fā)它,才能真正更新資源。
CDN 緩存的黑暗面無法讓人忽視。對(duì)用戶而言,緩存是透明的,對(duì)產(chǎn)品、技術(shù)卻很可能成為阻礙。
如果處理不當(dāng),它將影響新功能能否及時(shí)發(fā)布、阻斷后置所有服務(wù)的埋點(diǎn)、提高風(fēng)險(xiǎn)感知的成本,以及無法保障一致性,增加了線上問題的排查難度。
因此,十分有必要設(shè)立一個(gè)負(fù)責(zé)緩存刷新、預(yù)熱的觸發(fā)式服務(wù),用以改進(jìn)開發(fā)人員的體驗(yàn)。可是 CDN 緩存可控性很低,刷新也不能做到全然實(shí)時(shí)生效。
處于頻繁變化的頁面,最好考慮進(jìn)入穩(wěn)定期再開啟 CDN 緩存。即使是穩(wěn)定的、大流量的頁面,也還需要考慮 CDN 緩存穿透的防范措施。
一旦 CDN 緩存在 SSR 架構(gòu)中得到重用,就要做好長(zhǎng)期調(diào)整決策的準(zhǔn)備。
頁面靜態(tài)化
在 CDN 緩存無法涉足的地方,我們也可以對(duì)自身進(jìn)行多級(jí)緩存的加固。
動(dòng)態(tài)路由下的頁面路徑比較分散,而分?jǐn)偟巾撁婢唧w URL 的流量可能就不高。顯然這樣的頁面不適合 CDN 緩存,緩存命中率很低,所以才引入了將頁面靜態(tài)化的預(yù)渲染方案。
在頁面正常渲染完成后,我們既然可以將整個(gè)頁面緩存下來,也能夠?qū)⒕彺鎻膬?nèi)存持久化到硬盤或云存儲(chǔ)服務(wù)。這樣一來,便可以低成本地完整“緩存”數(shù)量巨大的頁面庫。
這既是對(duì) CDN 緩存的良好補(bǔ)充,也可以廣泛用于頁面容災(zāi)。
五、總結(jié)
以上這些優(yōu)化,我們?cè)诹λ芗暗姆秶鷥?nèi)相時(shí)而動(dòng),還存在著非常多的問題和缺陷,但愿可以給從未進(jìn)行此類嘗試的朋友提供一個(gè)詳細(xì)的案例。

本文的大段篇幅留給了相對(duì)少見的優(yōu)化,尤其是多級(jí)緩存的處理。上圖是一個(gè)粗糙的性能對(duì)比,其中最大的影響因素就是 CDN 緩存。在本文的最后,我也將對(duì)此項(xiàng)改造著重進(jìn)行總結(jié)。
CDN 緩存是一把利刃,在大流量的場(chǎng)景下,可以替源站攔截幾乎所有的請(qǐng)求,能提供極強(qiáng)伸縮性的負(fù)載。
但是,你的 SSR 應(yīng)用適合接入 CDN 緩存嗎?

再一次細(xì)數(shù)上面提到的諸多問題:
路徑控制 頁面降級(jí) 狀態(tài)治理 緩存失效
答案得你自己說了算……
實(shí)際上,極少數(shù) SSR 頁面場(chǎng)景才需要 CDN 緩存,如門戶首頁。流量不高、路徑分散的一般業(yè)務(wù),只需要使用動(dòng)態(tài)的 CDN 加速和靜態(tài)文件緩存,就能基本滿足 CDN 代理層的優(yōu)化需要。
最后
如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:
點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
歡迎加我微信「qianyu443033099」拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
關(guān)注公眾號(hào)「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。

