看懂 Serverless SSR,這一篇就夠了!

  • 2020 年 3 月 18 日
  • 筆記

作者:Adrian S. 譯者:王俊傑,王天雲 審校:王俊傑,江柳

了解我們如何為每個Webiny網站獲得出色的SEO支援,以及如何在無伺服器環境中使用SSR使其超快運行。

內容概要

我確實意識到這是一篇很長的文章,請相信我不是故意寫的很長。據我了解,有些人可能沒有時間通篇讀完,下面我準備了一個簡短的內容概要:

  • 單頁應用程式(SPAs)很酷,但不幸的是,對SEO的支援不佳。
  • 查閱這篇文章,了解有關在Web上進行渲染的不同方法,然後選擇最適合您的用例的方法。
  • 用Webiny構建的應用程式,我們嘗試了「按需預渲染」(使用chrome-aws-lambda)和「服務端渲染與激活」  。
  • 只需幾個無伺服器服務就可以在AWS雲中實現這兩種方法,他們是S3、Lambda、API網關和CloudFront。
  • 就用戶體驗方面,如果初始載入螢幕(在應用程式初始化時顯示)不是問題,並且搜索引擎優化是您唯一關心的問題,則按需進行預渲染是一種很好的方法,否則可以使用伺服器端渲染和激活。
  • 將更多的RAM(1600MB +)分配給實際上將進行預渲染的Lambda函數,並將最小的RAM分配給僅用於服務靜態文件的RAM(128MB或256MB)。
  • 儘管我們沒有嘗試過,但是您可能需要對預渲染的內容進行某種形式的快取,以便通過更快地返回初始HTML來獲得更好的SEO結果。
  • 在使用服務端渲染與激活時,為生成SSR HTML的Lambda函數分配更多的RAM.
  • 通常,SSR是一項資源密集型任務,它會阻止您足夠快地為網站提供服務,因此您很可能需要實現某種快取
  • 我們使用CloudFront CDN來快取SSR HTML,並根據您所構建的應用程式,在短期和長期快取TTL之間進行選擇
  • 如果要使用長期快取,需要處理快取失效的情況,這個會有點棘手。
  • 有選擇地進行快取失效,或者說,如果可能的話,僅對必要的頁面進行快取失效–這樣可以為您節省大量資金(快取失效請求由CloudFront收取)
  • 如果內容更改非常頻繁,請使用短期快取TTL,因為這樣更有效。

好消息是,使用Webiny,上面提到的都可以處理並定期更新維護一些方法,如果您覺得不錯的話,可以經常來Webiny查閱。

這就是內容概要的全部內容了,如果您想更深入地研究該主題,或者只是想看看我們嘗試過的無伺服器方法和實現成果,我建議您繼續往下看。

Serverless Side Rendering

在Webiny,我們的使命是創建一個平台,使開發人員能夠構建無伺服器應用程式。換句話說,我們希望為開發人員提供適當的工具和流程,以便使用無伺服器技術的開發更加輕鬆,高效和愉悅。最重要的是,我們還希望構建一個包含插件乃至現成應用程式的生態系統,這將進一步減少開發時間和成本。

為了應用程式便於快速開發,Webiny實際上提供了一些基本的應用供開發人員使用,其中之一就是我們的Page Builder應用程式。我不想浪費您的時間,這也不是一篇做廣告的文章,我們已經為此工作了相當長的時間(並將繼續這樣做),儘管面臨許多挑戰,但無疑,最有趣的挑戰之一就是以最佳方式為用戶展示頁面。換句話說,儘可能快地展示頁面,當然,還對搜索引擎優化(SEO)提供了出色的支援。

為了實現上述目標,我們不僅要利用無伺服器技術,而且要利用現代的單頁應用程式(SPA)方法來構建網站和應用程式。但是事實證明,同時實現和使用所有上述提到的可能有點難度。

SPA很酷,但是它們有一個嚴重的缺點:SEO支援不好,這是因為它們完全是客戶端渲染的,這意味著如果我們不能完全依靠客戶端渲染(CSR)來渲染我們的應用程式我們該怎麼做呢?在無伺服器環境中,我們如何處理伺服器「傳統上」完成的工作?我們如何實現「無伺服器端渲染」?

在本文開始時,我直接放棄講一些不是那麼重要的內容,如果您想要擁有一個現代,快速,可擴展且經過SEO優化的單頁應用程式,那麼您肯定需要關注這些內容,我會講我們真正想要為我們的用戶提供些什麼。

在本文中,我想介紹一下我們嘗試幾種方法去做,也會講哪一種方法是最適合我們的解決方案。您會看到沒有一個方案能解決所有問題,像靈丹妙藥一樣,您選擇的解決方案將取決於您正在構建的應用程式以及它自身的要求和條件。

由於有很多零散部分要說,為了能給您呈現一個全面的解析,我決定從頭開始講。

首先,讓我們談談單頁應用程式!

Before we begin

單頁應用程式, 我們將介紹它們的主要功能,優點/缺點,並且總體上,我們還將討論Web上的不同渲染方法。如果您是來這裡購買嚴格的無伺服器產品的,或者您已經有足夠的使用SPA的經驗,請跳轉至「選擇什麼?」這個 部分,我們將說明我們決定嘗試使用哪種渲染方法,以及如何在無伺服器環境中實現它們。

儘管我們確實計劃探索其他雲提供商,但在Webiny,我們目前主要與AWS合作,因此您將要看到的也是將是針對於AWS的一些實踐。但是,如果您不使用AWS,我仍然認為您應該能夠閱讀本文並使用類似的服務在您的雲中構建所有內容。

Single Page Applications

如果您是網路開發人員,那麼我很確定您已經熟悉單頁應用程式(SPA)的概念。但是,讓我們快速了解一下它的一些主要功能和優勢。

Client-side Rendering (CSR)

每個SPA的主要功能都是客戶端渲染(CSR)。這意味著所有用戶介面(HTML)都是在用戶瀏覽器內部生成的,而不是在某種後端(伺服器,容器,函數等等… _(ツ)_ /¯)上生成的。最酷的是,不需要整個頁面刷新,這意味著當您在應用程式中的其他位置交互操作時,僅這部分頁面被重新渲染,而沒有刷新整個頁面,這樣會有更好的體驗。

Cleaner code

如果您曾經使用過PHP,尤其是在過去,那麼您可能會記得那些長的Smarty / Twig模板文件,其中包含HTML,CSS,JS,也許是一些if語句,可能是對資料庫的一兩個調用,以及一些類似的別的什麼東西。如果你問我,那真是一團糟。

有了SPA,整個應用程式程式碼將變得更加整潔。這次我們有兩個單獨的程式碼庫,一個代表實際的SPA,另一個代表應用程式連接的後端或API。

Easy to serve

SPA易於維護,尤其是在無伺服器環境中。創建應用的生產版本後,基本上唯一要做的就是將其上傳到您選擇的靜態文件存儲中,例如Amazon S3。而且,如果您希望給您的應用和靜態資源提供更快的服務,那麼可以將CDN引入到後端體系結構,這種方式也很容易執行。但是,如果您的應用程式依賴於API,值得注意的是,該應用程式將與您的API速度一樣快,如果API速度很慢,那麼SPA也將變慢,儘管服務速度非常快。

Drawbacks?

如圖所示,SPA確實具有很多優點。但是它也有其自身的不足之處,下面我不得不吐槽下它最大的缺點。

每當您創建公開的網站(SPA或非SPA)時,顯然都希望擁有鏈接預覽。換句話說,當您分享您的網站鏈接時,例如 社交媒體網站(如Facebook),您希望獲得的是如下圖所示的預覽:

在Facebook上生成的鏈接預覽

但是,如果您以前從未使用過SPA,則可能會收到下圖的空鏈接預覽,並不是上圖完整的鏈接預覽:

空鏈接預覽

沒有顯示任何內容,僅顯示了鏈接標題和鏈接描述的純URL。但是為什麼會這樣呢??

毫無疑問,您會開始檢查程式碼,很快,您就能看到最初訪問您的網站時提供的index.html

最初提供的SPA HTML

我們可以看到,上面程式碼中沒有太多內容,只有一些基本的HTML標籤和一些網站的JavaScript和CSS文件的鏈接。這是意料之中的,因為這個初始HTML文檔實際上是我們應用程式構建的一部分,也就是說,該文檔不是動態生成的,用戶每次訪問我們的網站時都存在的。

一旦用戶在瀏覽器中輸入SPA支援的網站的URL,我粗略地列舉下將會出現以下過程:

  1. 下載用於SPA初始化的 HTML
  2. 下載文件(遇到CSS,JavaScript,影像等)
  3. 一旦載入了JavaScript並執行它,這通常意味著SPA初始化開始,獲取初始數據,呈現初始UI介面。

但是,當網路抓取工具(例如 Facebook的網路爬蟲)訪問了該網站,會發生什麼呢?

首先是下載初始的SPA HTML,與常規用戶不同,網路爬蟲不會等到SPA完全初始化,才獲取生成的HTML,他們只會分析最初提供給他們的HTML,僅此而已。這就是Facebook的網路爬蟲無法生成完整的鏈接預覽的原因,因為初始內容根本沒有包含足夠的資訊。

但是社交媒體網路爬蟲並不是唯一的問題,更重要的關於搜索引擎爬蟲和SEO

儘管搜索引擎也在尋求可能的解決方案了來應對SPA初始化沒有包含足夠的資訊的問題,但到目前為止,我們仍然不能完全依賴這些解決方案。實際上,我已經看到幾個示例,其中介紹了SPA大大降低了SEO品質的結果,例如:

嗨,夥計…想像一下您在一個項目上花費了三個月,在發布之前,您意識到自己根本沒有SEO支援。

How to deal with this?

到目前為止,只有一種可靠地解決此問題的方法,那就是為網路爬蟲提供有價值的HTML。換句話說,當網路爬蟲訪問您的網站時,最初提供的HTML必須包含諸如頁面標題,適當的meta標記,頁面內容(正文)之類的。例如:

一個HTML文檔,其中包含資源鏈接,必要的meta標籤,完整的頁面主體等。

但是,實現這一目標的最佳方法是什麼?我們是否需要在每個頁面請求上動態生成HTML的伺服器?還是我們可以使用其他方法?

好吧……這將是我們看的下一個主題:在Web上渲染。

Rendering on the Web

實際上,在web上渲染應用程式有多種方法。,這是Google部落格上的一篇文章,我看過好多遍了,寫的非常好,它可以幫助您很好地了解不同的渲染方法,並為您提供每種方法的利弊資訊。

在本文的結尾,我們可以很好地總結我們今天可以使用的所有渲染方法:

網路上不同渲染方法的摘要

如您所見,摘要中包含了很多有用的資訊。讓我們快速瀏覽下每個:

Full CSR

早先我們都知道一種方法,就是後端返回一個簡單的HTML,在用戶的瀏覽器中進行應用初始化。這種方法不適合做SEO,但是如果構建網頁的時候不需要進行SEO(例如管理員登陸頁面),那麼它仍然是一種不錯的方法。

CSR with Prerendering

如果您曾經與Gatsby一起工作過,則可能對這種方法很熟悉。基本上,一旦我們準備好部署您的網站,便會開始構建過程,該過程會預先生成應用程式的所有頁面,然後可以將其上傳到靜態文件存儲中,例如亞馬遜S3。

由於構建的頁面包含完整的HTML,並且不會動態生成任何內容,因此該應用將以超快的速度提供服務,最重要的是,它將擁有出色的SEO支援。

這種方法的要點是,每當需要進行更改時,即使更改很小,也需要從頭開始完全重建所有內容,而在較大的項目上,這可能會花費一些時間。因此,如果您經常進行更改,那麼對您來說這可能不是一種超級方便的方法。

Server-side Rendering (SSR) with (re)hydration

通過這種方法,我們在伺服器端的每個初始頁面請求上動態生成HTML。注意這裡的「 initial」一詞。我們的意思是,伺服器端HTML的生成只會在初始頁面請求(例如用戶在瀏覽器中輸入URL或刷新整個頁面時)的時候,有趣的是,在收到初始HTML之後,會初始化完整的CSR SPA,這意味著該時間點的所有HTML都會在用戶的瀏覽器中生成,因此仍然可以創建出色的用戶體驗。這種方法也稱為「同構渲染」。

聽起來很不錯,但要注意,採用這種方法時,您實際上需要為應用創建兩個獨立的生產版本,一個仍將在用戶瀏覽器中提供並執行,而另一個將在後端執行以動態生成HTML。創建兩個版本的原因是不同的環境,也就是說在NodeJS後端中運行瀏覽器程式碼根本行不通(反之亦然)。

儘管有時無法簡單地設置SSR,但是一旦學習了一些技巧,您就可以了(設置是,性能完全是另一回事)。使用諸如Next.js之類的框架可以大大節省您的時間。

Last two — Static SSR & Server Rendering

如前所述,靜態SSR在構建過程中刪除JavaScript,並用於提供純靜態HTML頁面。如果您的特定用例可以接受JavaScript刪除,則此方法可能對您有用。

最後,一個純伺服器渲染不屬於SPA類別,因為它根本不依賴任何客戶端渲染。HTML總是從伺服器返回,並且在您的應用程式中瀏覽時,將假定刷新了整個頁面,那麼,這與我們最先提到的Full CSR完全相反。

What to choose?

上面顯示的摘要絕對可以幫助我們選擇正確的方法來渲染我們的應用程式。但是我們應該使用哪一個呢?

其實,這取決於您正在構建的應用程式,換句話說,取決於您面前的特定需求。如果您有一個簡單的靜態網站,那麼帶有預渲染的CSR絕對是一個不錯的選擇。另一方面,如果您要創建更具動態性的內容,那麼,根據您的SEO需求,您可能要使用SSR渲染與激活或簡單的 Full CSR SPA。

因此,對您的應用程式進行快速分析肯定會幫助您選擇正確的方法,這正是我們為改進Page Builder應用程式在Webiny所做的。

The Page Builder

下圖一目了然地顯示了Webiny Page Builder最初的工作方式:

該圖顯示了Webiny Page Builder的工作方式

因此,在上方的圖中,我們有管理員用戶,他們可以通過admin UI創建新頁面或編輯現有頁面。整個管理介面是一個完整的CSR SPA(使用比較受歡的create-react-app創建),這沒有任何問題。

在下圖中,我們有一個面向公眾的網站和普通用戶,我們為他們提供了完整的CSR SPA。這裡沒有什麼超高級的,基本上,一旦應用程式通過GraphQL API初始化,應用程式就會獲取需要顯示給用戶當前URL的內容,並且差不多就可以了。

當然,據我們了解,對於面向公眾的應用程式而言,完全CSR方法還不夠好,因為公共頁面必須具有SEO支援。只是沒有更好的辦法, 因此,現在可以查閱下Web文檔上的「渲染」,並嘗試選擇最佳的方法。

What we』ve chosen?

因為Page Builder本質上是動態的,這意味著一旦用戶單擊編輯器中的publish按鈕,該頁面必須立即上線(並且當然是兼容SEO的),我們選擇了第三種方法,即SSR渲染和激活。

但是,因為我們知道當時我們的程式碼庫需要大量更改才能正常工作,所以實際上我們還有一個想法,我們想首先嘗試一下這種方法。也就是如果我們可以從後端訪問該URL,就像普通用戶那樣訪問該URL,並在Web爬網程式發出請求時將其返回,該怎麼辦?您知道嗎,只需模擬普通用戶,等待完整的UI生成,獲取最終的HTML,然後就可以使用?對於普通用戶而言,什麼都不會改變,我們仍然會為他們提供常規的單頁面應用,因為實際上,用戶並不關心最初從後端收到的HTML(實際上,這確實很重要,在以下各節中將對此進行更多說明)。

我們認為可以這樣做,所以我們嘗試了一下。我們將這種方法稱為「按需呈現」。

因此,總而言之,我們決定嘗試以下兩種方法:

  1. 按需預渲染
  2. SSR(渲染並激活)

讓我們看看如何在無伺服器環境中實現這些渲染方法,當然,從中可以比較出哪種方法效果更好。

如前所述,請注意,由於我們目前僅與AWS雲提供商合作,因此接下來的示例主要是基於AWS來實現。但是,如果您將應用程式託管在任何其他雲上,那麼我相信您仍然可以使用雲提供商提供的類似服務來實現同一目標。

好吧,讓我們看看!

Prerendering on demand

為了實現按需預渲染,我們使用了以下AWS服務:

按需預渲染-利用的AWS服務

因此,我們使用一個S3 Bucket來託管SPA的生產版本,幾個Lambda函數以及最後的API Gateway和CloudFront,以使所有內容在Internet上公開可用並分別啟用適當的快取。

為此,我們還使用了chrome-aws-lambda庫,該庫基本上是(Headless

)瀏覽器,可以通過編程方式在Lambda函數內部進行控制。我們將使用它來訪問網路爬蟲程式請求的URL,等待單頁面應用完全初始化,獲取最終生成的HTML,最後將輸出返回給網路爬蟲程式。

首先,讓我們看看普通用戶訪問網頁時會發生什麼。

Regular users

按需預渲染-用戶流

當普通用戶訪問站點時,HTTP請求將通過CloudFront重定向到API網關,該API網關將調用Web伺服器Lambda。我們之所以給它起這個名字是因為,在某種程度上,它實際上起著常規Web伺服器的作用,即基於接收到的調用有效負載(HTTP請求),它提供了從S3 bucket中請求的靜態資源(JS,CSS,HTML,影像等)。此功能的一些其他作用是,當請求靜態資源時發送適當的快取響應標頭,並檢測網路爬蟲程式,因此我們使用了isisbot軟體包。

所以,如果普通用戶發出HTTP請求,我們只需從S3 bucket中獲取請求的文件,並將其作為調用響應發送回API網關,然後將其返回給CloudFront,就可以返回該文件。

當網路爬蟲訪問該站點時會發生什麼?

Web crawlers

在這種情況下,HTTP請求再次通過CloudFront和API網關到達Web伺服器Lambda,但是我們不是從S3提取文件,而是調用Prerender Lambda,它內部使用了上述chrome-aws-lambda庫來獲取所請求URL的完整的HTML。

按需預渲染-網路爬蟲流程

這裡有兩點需要注意,第一個是chrome-aws-lambda的運行成本可能很高,因為它需要大量資源。圖書館的文檔指出,應至少分配512MB的RAM,但建議分配1600MB或更多。這就是為什麼我們沒有將所有邏輯都放在一個Lambda函數中(放入Web伺服器Lambda中)的原因。僅當網路爬蟲訪問該站點時,Prerender Lambda函數才會被調用,該訪問頻率比普通用戶訪問的頻率要低。為普通用戶提供簡單的靜態資源,具有基本的128MB或256MB RAM的Lambda函數就足夠了,從而為我們節省了一些錢。

我們還有一些有關chrome-aws-lambda庫的提示,以某種方式對它進行配置,以免下載不生成DOM的資源(如CSS和影像)。您無需載入這些文件即可獲取完整的HTML,這將大大加快HTML的獲取過程。

另外,為簡化部署,您還可以使用chrome-aws-lambda-layer庫,該庫基本上使您可以將包含所有必需程式碼的公共Lambda函數層附加到函數中,這意味著您不必自己上傳所有程式碼(和Chromium二進位文件)。您可以使用Lambda控制台,甚至使用更好的Serverless框架,輕鬆引用該層。以下serverless.yaml顯示了如何執行此操作(請注意preRender函數內部的layers部分):

服務:mySiteService

提供者:

名稱:aws

運行時:nodejs 10.x

功能:

preRender:

角色:arn:aws:iam :: 222359618365:role / SOME-ROLE

記憶體大小:1600

超時:30

層數:

-arn:aws:lambda:us-east-1:764866452798:layer:chrome-aws-lambda:8

處理程式:fns / my-server / index.handler

注意:對於完整的生產環境,您還可以選擇自己構建該層,從而為您提供更多的控制權和更好的安全狀態。

Results

下圖顯示了所有優點和缺點:

按需預渲染—優點和缺點

這裡要注意的是,儘管我們設法獲得了良好的SEO支援,但不幸的是,我們仍然面臨著嚴重的速度/用戶體驗問題。

由於用戶仍在接收完整的CSR單頁面應用,因此在每次請求時,他都必須等待初始化資源(JS和CSS)以及頁面數據被載入。當頁面載入時,會向用戶顯示一個載入螢幕,並且用戶在每次訪問頁面時,基本上都會在頁面上停留1-3秒,這絕對不是一個很好的用戶體驗,尤其是我們研究的靜態頁面。簡單的說就是它很慢。

即使我們已經嘗試了一些改進的方法,但最終還是無法使它以能夠滿足我們目標的方式工作,因此放棄了按需渲染的想法。

但是,請注意如果載入螢幕對您的應用程式沒有問題,那麼這仍然是一種有效的實現方法。我個人喜歡此解決方案,因為與採用伺服器端渲染與激活方法不同,此方法更易於維護,因為它不需要構建兩個單獨的應用程式。

讓我們看看我們現在如何使用伺服器端渲染與激活方法!

SSR with (re)hydration

對於此實現,我們實際上使用了在按需預渲染實現中相同的服務

伺服器渲染與激活-利用的AWS服務

但是當然,該圖會有所不同:

 伺服器渲染與激活-流程

在解釋其全部工作原理之前,還記得我們提到伺服器渲染與激活方法需要我們構建SPA的兩個生產版本嗎?一個提供給瀏覽器並在瀏覽器中執行,另一個真正在伺服器上執行?是的,但是這些應用生產版本將會被存儲在哪裡呢?

提供給用戶瀏覽器的內部版本與我們先前使用的內部版本沒有什麼不同,即按需預渲染方法,並且以相同的方式將其存儲在一個簡單的S3 bucket中。請注意,就像在任何單頁面應用版本中一樣,此版本不僅包含JavaScript文件,而且還包含CSS文件、影像以及您的網站可能需要的其他靜態資源。另一方面,SSR構建不包含所有內容,它僅包含一個JS文件,其中包含最小化的程式碼,因此,我們決定將其直接捆綁到SSR Lambda中。由於文件大小約為1MB,因此我們認為這可能不是性能問題。好了,回到圖上!

這次,用戶和網路爬蟲的流程是相同的。CloudFront接收HTTP請求並將其轉發到API網關,API網關將調用Web伺服器Lambda,然後由它決定是必須從S3 bucket中提取文件還是必須調用SSR Lambda。路由很簡單,如果請求未指向文件(我們檢查文件擴展名是否存在),Web Server Lambda會將請求轉發至SSR Lambda,SSR Lambda會生成需要返回給訪客的HTML。另一方面,如果請求了靜態文件,則將其直接從S3 bucket中提取。如前所述,這與以前看到的按需預渲染方法(普通用戶訪問該站點)沒有什麼不同。

那麼,這種方法的結果是什麼?

Results

伺服器渲染與激活—優點和缺點

有趣的是,即使我們已經通過先前提到的按需預渲染方法解決了SEO兼容性問題,但我們確實也遇到了頁面載入速度緩慢問題,這在UX方面可能是非常糟糕的。不幸的是,這和採用伺服器渲染與激活方法相比,兩者沒有什麼不同。

使用按需預渲染的方法時,用戶必須盯著載入螢幕,直到應用程式完全初始化為止。現在,他們需要再次等待相同的時間,但是這次,他們盯著空白螢幕,等待後端返回服務端渲染的HTML。

您可能會問自己為什麼要等呢?好吧,這很合邏輯,這是因為以前在用戶瀏覽器中進行的所有處理(在載入疊加層之後)現在都在後端SSR Lambda函數內部進行。更重要的是,開箱即用的伺服器端渲染是一項資源密集型任務,因此生成整個HTML文檔需要花費時間。將其與冷啟動功能可能會增加的其他延遲配對,可以確保您度過了一段愉快的時光。

當您查看時,由於用戶盯著黑屏,而不是我們以前擁有的漂亮的載入疊加,我們實際上已經設法使用戶體驗變得更糟!

SSR HTML Caching

儘管我們嘗試增加SSR Lambda函數的系統資源量,但這仍然沒有對整體性能產生足夠積極的影響。最後,為了加快處理速度,我們決定引入快取。我們嘗試了許多不同的解決方案,最後,我們解決了如下兩個問題:

對於這兩者,整個雲架構的唯一補充就是資料庫,我們將使用該資料庫來快取接收到的SSR HTML。它可以是任何您喜歡的資料庫,我們決定使用MongoDB,因為我們已經非常依賴它了。但是,您可以使用DynamoDB或Redis,這些絕對也是不錯的選擇。

Solution 1— short cache max-age (TTL)

下圖幾乎與我們在上一節中看到的圖一模一樣,只不過現在有了一個資料庫:

SSR 的渲染與激活-快取流(長TTL)

因此,每次Web Server Lambda收到來自SSR Lambda的SSR HTML,在將其返回給API網關之前,我們還將其存儲在資料庫中。一個簡單的資料庫條目可能看起來像這樣:

因此,一旦將SSR HTML(以及上面片段中顯示的其他一些數據)存儲在資料庫中,我們就將其連同Cache-Control一起發送回API網關:public,max-age = MAX_AGE標頭,將指示CloudFront CDN將結果快取MAX_AGE秒。

為了獲得MAX_AGE值,我們使用存儲在資料庫中的expiresOn(SSR HTML被視為過期的時間點)。由於這是一個日期字元串,並且必須以秒為單位定義MAX_AGE,因此我們只計算expiresOn — CURRENT_TIME。這裡要注意的重要一點是,最初設置expiresOn時,該值將為CURRENT_TIME + 60秒。換句話說,calculatedMAX_AGE將為60秒,因此,以下響應標頭將返回到CloudFront CDN:控制:public,max-age = 60。

因此,在發出初始請求之後,接下來的60秒內,每次用戶在瀏覽器中點擊相同的URL時,由於SSR HTML是從CDN邊緣提供的,因此用戶基本上會遇到即時響應(〜100ms)。在這種情況下,根本不會調用Lambda函數)。

這太棒了,但是當CDN快取過期時會發生什麼?我們是否還必須等待服務端渲染生成?不需要,在那種情況下,請求將再次到達Web Server Lambda函數,但是現在,我們將立即檢查資料庫中是否已經存在未過期的快取SSR HTML,而不是立即調用SSR Lambda。

如果是這樣,我們將僅返回接收到的SSR HTML,並再次使用Cache-Control:public,max-age = MAX_AGE響應標頭。請注意,我們已經使用資料庫條目的expiresOn值來再次計算MAX_AGE,這次不必是60秒,也可以更短(並且將是)。如果59秒鐘前在先前訪問者的URL請求之一中將SSR HTML保存到資料庫,則甚至可能需要1秒鐘。還要注意,如果請求到達的CDN邊緣還沒有快取的SSR HTML,則該請求仍會響應Web Server Lambda函數。

另一方面,如果我們確定收到的SSR HTML已過期,我們實際上會執行以下操作:首先開始一個進程,該進程將使用新的SSR HTML和新的expiresOn值更新資料庫中的SSR HTML條目,該值等於SSR_HTML_REFRESH_FINISHED_TIME + 60秒。此過程將以非同步方式觸發,這意味著我們不會等待它完成,因為如我們所見,獲取SSR HTML可能需要一些時間。觸發該操作後,我們將立即使用新的expiresOn值將資料庫中的同一SSR HTML條目更新為CURRENT_TIME + 10秒(請注意短暫的10秒增量)。保存完之後,緊接著,我們將* expired*的SSR HTML返回到API網關,再次使用Cache-Control:public,max-age = MAX_AGE標頭,僅這次MAX_AGE將為10,這意味著CloudFront CDN只會將此過期的SSR HTML快取10秒鐘。

換句話說,在接下來的10秒鐘內,用戶將從CloudFront CDN收到SSR HTML的過期版本。之後,快取將再次過期,並且在那個時間點,我們肯定會準備好要提供的新SSR HTML(在上述非同步過程中進行了刷新)。這裡唯一需要注意的是,在10秒鐘的CDN快取過期之後,所提供的新鮮SSR HTML的newMAX_AGE將取決於從資料庫接收到的expiresOn(等於(SSR_HTML_REFRESH_FINISHED_TIME + 60秒)— CURRENT_TIME)。它實際上可以在0s到60s之間,具體取決於10秒鐘快取過期和之後的第一個請求經過了多少時間。如果超過60秒,則該過程將再次重複,這意味著將再次返回10秒的MAX_AGE,並且將觸發新的非同步SSR HTML刷新過程。

Results

這幾乎就是整個流程。從性能角度來看,大多數情況下,用戶會在約100毫秒的時間內從瀏覽器中收到初始HTML。例外情況是CDN快取已過期,並且需要先從Web伺服器Lambda返回SSR HTML,在這種情況下,如果我們要處理冷函數,則延遲可能會跳到200ms(400ms)和800ms(1200ms)。開始。如果你問我,還不錯!

另一方面,這種方法的問題之一是,如果資料庫中根本沒有SSR HTML(甚至沒有過期的HTML),那麼用戶將不得不等待SSR HTML生成過程完成。沒有別的辦法,因為我們沒有任何東西可以返還給用戶。這意味著他必須等待1到4秒鐘才能返回SSR HTML,如果後台開始冷啟動,則還要等待4到7秒鐘。

但是請注意,每個網址只會發生一次,因此它並不是很頻繁,而且也沒什麼大不了的。為了減少由冷啟動引起的額外延遲,您可以嘗試利用最近引入的預配置並發。我必須肯定地說我們沒有試過,但是可能值得檢查一下是否引起了您的問題。另外,如果可能的話,如果您要避免在用戶的實際請求上生成SSR HTML,甚至可以提前請求一些頁面。

儘管此方法的一個優點是您不必手動進行任何快取失效操作(因為快取會很快過期),但必須注意,API Gateway和Lambda函數將經常被調用,這需要考慮,因為這可能會影響總成本。

這基本上就是為什麼我們開始思考如何避免API網關和Lambda函數調用以及如何將儘可能多的流量卸載到CDN的原因。首先想到的是較長的MAX_AGE值。

Solution2— long cache max-age (TTL)

此解決方案的體系結構保持不變。

SSR 的渲染與激活-快取流(長TTL)

因此,用戶將儘可能從CDN接收SSR HTML。否則,Web伺服器Lambda將由API網關調用,並且將直接從資料庫中或通過現場生成SSR HTML來返回(如圖所示,當SSR HTML不存在時,甚至不存在過期的HTML時,都會發生這種情況)。

如上所述,唯一的區別是,我們在響應標頭中發送的MAX_AGE值要長得多,例如一個月(快取控制:public, max-age=2592000)。請注意,如果請求到達Web伺服器Lambda,並且我們確定資料庫中有過期的SSR HTML快取,我們仍將使用簡短的Cache-Control進行響應:public,max-age = 10response標頭。這沒有改變。

使用這種方法,我們可以更少地調用Lambda函數,因為在大多數情況下,用戶會遇到CDN,這意味著用戶不會經歷太多的冷啟動延遲,而且我們也可以少擔心Lambda函數會生成很多費用。完美!

但是現在我們必須考慮快取失效。我們如何告訴CloudFront CDN清除其擁有的SSR HTML,以便可以從Web伺服器Lambda中獲取一個新的HTML?例如,當管理員通過「頁面構建器」對現有頁面進行更改並發布時,這種情況經常發生。

當您考慮它時,它應該很簡單,對吧?每次管理員用戶對現有頁面進行更改並發布時,我們都可以通過編程方式使頁面URL的快取無效,就是這樣嗎?

好吧,實際上,這只是完整解決方案的一部分。我們還有其他一些關鍵事件,應使CDN快取無效。

例如,我們的Page Builder應用程式支援許多不同的頁面元素,您可以將它們拖動到頁面上,其中之一是可讓您從Form Builder應用程式中嵌入表單的元素。因此,您可以在頁面上添加表單,發布頁面,一切都很好。但是,如果有人在實際表單上進行了更改,例如,添加了其他欄位怎麼辦?如果發生這種情況,站點用戶必須能夠看到這些更改(SSR HTML必須包含這些更改)。因此,「僅僅在頁面上發布無效」的想法在這裡還不夠。

但是還有更多!假設管理員用戶對網站的主菜單進行了更改。由於基本上可以在每個頁面上看到菜單,這是否意味著我們應該使包含該菜單的所有頁面的快取無效?好吧,很不幸,但是,沒有別的辦法了。在我們這樣做之前,我們應該了解有關快取無效定價的任何資訊嗎?

要的,對於較小的站點,包含菜單的頁面總數可以從10到20頁不等,但是對於較大的站點,我們可以輕鬆擁有數百甚至數千頁!因此,這可能迫使我們向CDN創建許多快取無效請求,如果您查看CloudFront的定價頁面,我們會發現這些請求並不便宜:每月要求無效的前1,000條路徑不會收取額外費用。此後,請求無效的每個路徑$ 0.005。

正如我們所看到的,如果我們要實現基本的「只是使包含菜單的所有頁面失效」邏輯,我們可能會很快脫離免費層,並且基本上開始為每進行1000次失效支付5美元。這不友好。

因此,我們開始考慮替代性想法,並提出了以下建議。

如果菜單發生更改,請不要使包含該菜單的所有頁面的快取都失效。相反,讓我們檢查一下是否只有在實際訪問時才需要使頁面無效。因此,每次用戶訪問頁面時,我們都會發出一個簡單的HTTP請求(非同步觸發,因此不會影響頁面性能),該調用將調用Lambda函數,該函數通過以下方法檢查CDN快取是否需要無效:檢查存儲在資料庫中的SSR HTML是否已過期,是因為自生成以來已經經過了足夠的時間,還是在一個關鍵事件中將其簡單地標記為已過期(例如,菜單已更新或頁面已發布)。如果是的話,它將僅獲取新的SSR HTML並將無效請求發送到CDN。

同時,有以下幾點需要注意:

  • 首先,對於每次頁面訪問,我們都會調用Lambda函數。但是,我們嘗試使用這種更長的最大壽命(TTL)方法的原因之一是為在實踐中避免了這種情況。不幸的是,這是不可避免的。但幸運的是,您可以通過較少地觸發此檢查來減少調用次數。一分鐘,五分鐘甚至十分鐘觸發一次,選擇最適合您的觸發次數即可。
  • 其次,使CDN快取無效會花費一些時間,因此,新的SSR HTML會在5秒到5分鐘甚至更晚的時間內到達,具體取決於CDN的當前狀態。在大多數情況下,這會非常快,這就是我們所經歷的平均5-10秒。

Trigger invalidation selectively with custom HTML tags

可以看出,我們看到的「菜單更改」事件是一個重要事件,必須觸發不僅一頁的快取失效。但是,假設我們要更新的輔助菜單僅位於少數頁面上。更新後,我們絕對不想將網站的所有頁面都標記為過期,對嗎?因此,自然而然地出現的問題是:有沒有一種方法可以使我們更有效,並且只對實際上包含更新菜單的頁面的快取無效?

因為有這個問題,我們決定引入HTML標記。換句話說,我們利用我們自己的customsr-cache HTML標記來有目的地標記不同的HTML部分/ UI部分。

例如,如果您正在使用Menu React組件(由我們的Page Builder應用提供)在頁面上呈現菜單,除了實際的菜單外,該組件在渲染時還將包括以下HTML:

<ssr-cache data-class =「 pb-menu」 data-id =「 small-menu」 />

一個頁面可以具有多個這樣的不同標記(您也可以介紹自己的標記),並且在進行SSR HTML生成時,所有這些標記都將存儲在資料庫中。讓我們看一下更新的資料庫條目:

接收到的SSR HTML中包含的所有ssr-cache HTML標記都被提取並保存在cacheTags數組中,這使我們以後可以更輕鬆地查詢數據。

我們可以看到,cacheTags數組包含三個對象,其中第一個是{「 class」:「 pb-menu」,「 id」:「 small-menu」}。這僅表示SSR HTML包含一個頁面構建器菜單(pb-menu),該菜單具有ID二級菜單(此處的ID實際上由菜單的唯一slug表示,該slug是通過admin UI設置的)。

還有更多類似的標籤,例如pb-pages-list。此標記僅表示SSR HTML包含頁面構建器的「頁面列表」頁面元素。它之所以存在,是因為如果您的頁面上有頁面列表,並且發布了新頁面(或修改了現有頁面),則SSR HTML可以視為已過期,因為曾經在頁面上的頁面列表可能已受到新發布頁面的影響。

因此,既然我們了解了這些標籤的用途,那麼如何利用它們?其實很簡單。為了使開發人員更輕鬆,我們實際上創建了一個小型SsrCacheClient客戶端,您可以使用該客戶端分別通過invalidateSsrCacheByPath和invalidateSsrCacheByTags方法通過特定的URL路徑或傳遞的標籤觸發失效事件。在您定義的關鍵事件中,當你需要將SSR HTML標記為已過期且快取無效時,可以使用它們。

例如,當菜單更改時,我們執行以下程式碼(完整程式碼):

await ssrApiClient.invalidateSsrCacheByTags({   tags: [{ class: "pb-menu", id: this.slug }]});

發布新頁面(或刪除現有頁面)時,所有包含pb-pages-list頁面元素的頁面都必須無效(完整程式碼):

await ssrApiClient.invalidateSsrCacheByTags({   tags: [{ class: "pb-pages-list" }]});

基本的Webiny應用程式(例如頁面生成器或表單生成器)已經在利用React組件中的ssr-cache標籤和後端的SsrCacheClient客戶端,因此您不必為此擔心。最後,如果要進行自定義開發,則基本上可以歸結為識別必須觸發SSR HTML失效的事件,將ssr-cache標記放入組件中,並適當地使用SsrCacheClient客戶端。

Results

解決方案2很好,但又不是最終解決方案。

對您來說是否是一種好方法的最重要因素是您網站上正在發生的更改量。如果更改(必須觸發SSR HTML無效的特定事件)非常頻繁地發生,例如每隔幾秒鐘或幾分鐘,那麼我絕對不建議使用這種方法,因為快取無效性幾乎總是發生,並且以某種方式使目標無效。在這種情況下,我們前面提到的解決方案1可能會更好。分析和測試您的應用程式是關鍵。

同樣,如果長時間不訪問某個頁面,並且其SSR HTML同時被標記為已過期,則首次訪問該頁面的用戶仍會看到舊頁面。因為如果您還記得,在某個鍵事件觸發了多個頁面的SSR HTML無效的情況下(例如「菜單更改」事件),實際的快取無效是由實際訪問該頁面的用戶觸發的,而不是我們發送大量的向CloudFront的快取失效請求數量,並在執行過程中花錢。

但是總的來說,考慮到該解決方案提供的驚人的速度優勢和非同步快取失效,我們認為這是一種很好的方法。

實際上,我們已將其設置為每個新Webiny項目的默認快取行為,但是您可以通過輕鬆刪除幾個插件切換到解決方案1。如果您想了解更多資訊,請務必查看我們的文檔。

Conclusion

你看到最後了嗎?哇,我很佩服你!

開個玩笑,哈哈,希望我能向您分享我們的一些經驗,並且您從本文中獲得了一些價值。

今天,我們學到了很多不同的東西。從單頁應用程式的基本概念,缺乏SEO支援以及在Web上呈現的不同方法開始,到在無伺服器環境中實現其中兩種方法(最適合我們的頁面生成器應用程式),即按需預渲染和伺服器端渲染和激活。儘管在默認情況下,兩種方法都解決了上述提到的SEO支援不足的問題,但是在頁面載入時間方面,這些方法都無法提供令人滿意的性能。當然,如果您的特定應用程式不太在意螢幕載入問題的話,那麼按需預渲染可能對您有用。但是如果沒有的話,伺服器端渲染與激活可能是您的最佳選擇。

我們也可以看到,只需使用一些AWS serverless服務,包括S3,Lambda,API Gateway和CloudFront,就可以在無伺服器環境中相對容易地實現這些方法。儘管我們無需管理任何物理層面上的基礎架構就可以使所有這些服務正常工作,但我們仍然需要考慮分配給Lambda函數的RAM數量。對於基本的文件服務需求,最少需要128MB RAM,但是對於按需預渲染或伺服器端渲染這種資源密集型任務,我們必須分配更多空間。請注意分配並進行適當測試,因為這可能會影響您的每月費用。確保檢查每個服務的定價頁面,並嘗試根據您的每月流量進行估算。

最後,為解決SSR生成緩慢和功能冷啟動的問題,我們利用了CDN快取,這可在性能和成本方面產生重大差異。根據最適合我們的情況,我們可以使用短或長的max-age/ TTL進行快取。如果我們選擇使用後者,則將需要手動快取無效。並且如果由於內容太動態而導致出現很多此類情況,則您可能需要重新考慮您的策略,看看使用較短的max-age(TTL)值是否是更好的解決方案。

通常任何問題都沒有靈丹妙藥,我們今天討論的主題無疑是一個很好的例子。嘗試不同的事情是關鍵,它將幫助您找到最適合您的特定情況的方案。

哦,順便說一句,好消息是,如果您不想折磨自己並希望避免從頭開始實現所有操作,則可以嘗試Webiny!您甚至可以通過應用一組特定的插件,在我們展示的兩種不同的伺服器端渲染 HTML快取方法之間進行選擇。我們喜歡保持靈活性。

如果您有興趣,請隨時查看!如果您有任何疑問,請隨時關注我們,我們將很樂意為您解答!

再次希望我能解釋一下我們在Webiny嘗試過的方法,但是如果您有任何疑問,請隨時提出!我也想聽聽您對這個話題的看法,因此,如果您有什麼要分享的,請分享吧!

謝謝閱讀!我叫Adrian,是Webiny的全職開發人員。在業餘時間,我想寫一些關於我/我們在一些現代前端和後端(無伺服器)Web開發工具的經驗,希望它可以對其他開發人員的日常工作有所幫助。如果您有任何疑問,評論或想打個招呼,請隨時通過Twitter與我聯繫。

原文地址:

https://blog.webiny.com/serverless-side-rendering-e1c0924b8da1

Serverless Framework 免費試用計劃

Serverless Framework 免費試用名額已開放,我們誠邀您來試用和體驗最便捷的 Serverless 開發和部署方式。包括服務中使用到雲函數 SCF、API 網關、對象存儲 COS 等產品,均在試用期內提供免費資源,並伴有專業的技術支援,幫助您的業務快速、便捷實現 Serverless !

Serverless Framework 落地 Serverless 架構的全雲端開發閉環體驗,覆蓋編碼、運維、調試、部署等開發全生命周期。使用 Serverless Framework 即可在幾秒鐘內將業務部署至雲端。

詳情可查閱:https://cloud.tencent.com/document/product/1154/38792

立即使用Serverless,只需三步

Serverless Framework 是構建和運維 Serverless 應用的框架,簡單三步,即可通過 Serverless Framework 快速實現服務部署。
1、創建本地應用
  • 通過 npm 安裝 Serverless
$ npm install -g serverless
  • 基於 tencent_nodejs 模板創建 hello_world
$ serverless create --template tencent-nodejs --path my-service
2、安裝相關依賴
  • 執行 npm install 安裝相關依賴
$ cd my-service$ npm install
3. 部署
  • 掃描微信二維碼一鍵登錄騰訊雲帳號,部署函數到雲端
$ serverless deploy
  • 觸發雲函數
$ serverless invoke -f hello_world

部署完成後,即可在命令行中看到部署情況,也可以在騰訊雲控制台看到對應資源。