瞭解咱們如何爲每一個 Webiny 網站得到出色的 SEO 支持,以及如何在無服務器環境中使用 SSR 使其超快運行。html
本文系譯文
我確實意識到這是一篇很長的文章,請相信我不是故意寫的很長。據我瞭解,有些人可能沒有時間通篇讀完,下面我準備了一個簡短的內容概要:前端
chrome-aws-lambda
)和「服務端渲染與激活」; 這就是內容概要的所有內容了,若是您想更深刻地研究該主題,或者只是想看看咱們嘗試過的無服務器方法和實現成果,我建議您繼續往下看。node
在 Webiny,咱們的使命是建立一個平臺,使開發人員可以構建無服務器應用程序。換句話說,咱們但願爲開發人員提供適當的工具和流程,以便使用無服務器技術的開發更加輕鬆,高效和愉悅。最重要的是,咱們還但願構建一個包含插件乃至現成應用程序的生態系統,這將進一步減小開發時間和成本。react
爲了應用程序便於快速開發,Webiny 實際上提供了一些基本的應用供開發人員使用,其中之一就是咱們的 Page Builder 應用程序。我不想浪費您的時間,這也不是一篇作廣告的文章,咱們已經爲此工做了至關長的時間(並將繼續這樣作),儘管面臨許多挑戰,但無疑,最有趣的挑戰之一就是以最佳方式爲用戶展現頁面。換句話說,儘量快地展現頁面,固然,還對搜索引擎優化 (SEO) 提供了出色的支持。git
爲了實現上述目標,咱們不只要利用無服務器技術,並且要利用現代的單頁應用程序 (SPA) 方法來構建網站和應用程序。可是事實證實,同時實現和使用全部上述提到的可能有點難度。github
SPA 很酷,可是它們有一個嚴重的缺點:SEO 支持很差,這是由於它們徹底是客戶端渲染的,這意味着若是咱們不能徹底依靠客戶端渲染 (CSR) 來渲染咱們的應用程序咱們該怎麼作呢?在無服務器環境中,咱們如何處理服務器「傳統上」完成的工做?咱們如何實現「無服務器端渲染」?web
在本文開始時,我直接放棄講一些不是那麼重要的內容,若是您想要擁有一個現代、快速、可擴展且通過 SEO 優化的單頁應用程序,那麼您確定須要關注這些內容,我會講咱們真正想要爲咱們的用戶提供些什麼。chrome
在本文中,我想介紹一下咱們嘗試幾種方法去作,也會講哪種方法是最適合咱們的解決方案。您會看到沒有一個方案能解決全部問題,像靈丹妙藥同樣,您選擇的解決方案將取決於您正在構建的應用程序以及它自身的要求和條件。數據庫
因爲有不少零散部分要說,爲了能給您呈現一個全面的解析,我決定從頭開始講。express
首先,讓咱們談談單頁應用程序!
單頁應用程序,咱們將介紹它們的主要功能,優勢/缺點,而且整體上,咱們還將討論 Web 上的不一樣渲染方法。若是您是來這裏購買嚴格的無服務器產品的,或者您已經有足夠的使用 SPA 的經驗,請跳轉至「選擇什麼?」這個部分,咱們將說明咱們決定嘗試使用哪一種渲染方法,以及如何在無服務器環境中實現它們。
儘管咱們確實計劃探索其餘雲提供商,但在 Webiny,咱們目前主要與 AWS 合做,所以您將要看到的也是將是針對於 AWS 的一些實踐。可是,若是您不使用 AWS,我仍然認爲您應該可以閱讀本文並使用相似的服務在您的雲中構建全部內容。
若是您是網絡開發人員,那麼我很肯定您已經熟悉單頁應用程序 (SPA) 的概念。可是,讓咱們快速瞭解一下它的一些主要功能和優點。
每一個 SPA 的主要功能都是客戶端渲染(CSR)。這意味着全部用戶界面(HTML)都是在用戶瀏覽器內部生成的,而不是在某種後端(服務器,容器,函數等等……¯_(ツ)_/¯) 上生成的。最酷的是,不須要整個頁面刷新,這意味着當您在應用程序中的其餘位置交互操做時,僅這部分頁面被從新渲染,而沒有刷新整個頁面,這樣會有更好的體驗。
若是您曾經使用過 PHP,尤爲是在過去,那麼您可能會記得那些長的 Smarty/Twig 模板文件,其中包含 HTML,CSS,JS,也許是一些 if 語句,多是對數據庫的一兩個調用,以及一些相似的別的什麼東西。若是你問我,那真是一團糟。
有了 SPA,整個應用程序代碼將變得更加整潔。此次咱們有兩個單獨的代碼庫,一個表明實際的 SPA,另外一個表明應用程序鏈接的後端或 API。
SPA 易於維護,尤爲是在無服務器環境中。建立應用的生產版本後,基本上惟一要作的就是將其上傳到您選擇的靜態文件存儲中,例如 Amazon S3。並且,若是您但願給您的應用和靜態資源提供更快的服務,那麼能夠將 CDN 引入到後端體系結構,這種方式也很容易執行。可是,若是您的應用程序依賴於 API,值得注意的是,該應用程序將與您的API速度同樣快,若是 API 速度很慢,那麼 SPA 也將變慢,儘管服務速度很是快。
如圖所示,SPA 確實具備不少優勢。可是它也有其自身的不足之處,下面我不得不吐槽下它最大的缺點。
每當您建立公開的網站(SPA 或非 SPA)時,顯然都但願擁有連接預覽。換句話說,當您分享您的網站連接時,例如
社交媒體網站(如 Facebook),您但願得到的是以下圖所示的預覽:
可是,若是您之前從未使用過 SPA,則可能會收到下圖的空連接預覽,並非上圖完整的連接預覽:
沒有顯示任何內容,僅顯示了連接標題和連接描述的純 URL。可是爲何會這樣呢? 🤔🧐
毫無疑問,您會開始檢查代碼,很快,您就能看到最初訪問您的網站時提供的 index.html
咱們能夠看到,上面代碼中沒有太多內容,只有一些基本的 HTML 標籤和一些網站的 JavaScript 和 CSS 文件的連接。這是意料之中的,由於這個初始 HTML 文檔其實是咱們應用程序構建的一部分。也就是說,該文檔不是動態生成的,用戶每次訪問咱們的網站時都存在的。
一旦用戶在瀏覽器中輸入 SPA 支持的網站的 URL,我粗略地列舉下將會出現如下過程:
可是,當網絡抓取工具(例如 Facebook 的網絡爬蟲)訪問了該網站,會發生什麼呢?
首先是下載初始的 SPA HTML,與常規用戶不一樣,網絡爬蟲不會等到 SPA 徹底初始化,才獲取生成的 HTML,他們只會分析最初提供給他們的 HTML,僅此而已。這就是 Facebook 的網絡爬蟲沒法生成完整的連接預覽的緣由,由於初始內容根本沒有包含足夠的信息。
可是社交媒體網絡爬蟲並非惟一的問題,更重要的關於搜索引擎爬蟲和 SEO
儘管搜索引擎也在尋求可能的解決方案了來應對 SPA 初始化沒有包含足夠的信息的問題,但到目前爲止,咱們仍然不能徹底依賴這些解決方案。
嗨,夥計……想象一下您在一個項目上花費了三個月,在發佈以前,您意識到本身根本沒有 SEO 支持。
到目前爲止,只有一種可靠地解決此問題的方法,那就是爲網絡爬蟲提供有價值的 HTML。換句話說,當網絡爬蟲訪問您的網站時,最初提供的 HTML 必須包含諸如頁面標題,適當的 meta 標記,頁面內容(正文)之類的。例如:
可是,實現這一目標的最佳方法是什麼?咱們是否須要在每一個頁面請求上動態生成 HTML 的服務器?仍是咱們可使用其餘方法?
好吧……這將是咱們看的下一個主題:在 Web 上渲染。
實際上,在 web 上渲染應用程序有多種方法。「Rendering on the Web」是 Google 博客上的一篇文章,我看過好多遍了,寫得很是好。它能夠幫助您很好地瞭解不一樣的渲染方法,併爲您提供每種方法的利弊信息。
在本文的結尾,咱們能夠很好地總結咱們今天可使用的全部渲染方法:
如您所見,摘要中包含了不少有用的信息。讓咱們快速瀏覽下:
早先咱們都知道一種方法,就是後端返回一個簡單的 HTML,在用戶的瀏覽器中進行應用初始化。這種方法不適合作SEO,可是若是構建網頁的時候不須要進行 SEO(例如管理員登錄頁面),那麼它仍然是一種不錯的方法。
若是您曾經與 Gatsby 一塊兒工做過,則可能對這種方法很熟悉。基本上,一旦咱們準備好部署您的網站,便會開始構建過程,該過程會預先生成應用程序的全部頁面,而後能夠將其上傳到靜態文件存儲中,例如 Amazon S3。
因爲構建的頁面包含完整的 HTML,而且不會動態生成任何內容,所以該應用將以超快的速度提供服務。最重要的是,它將擁有出色的 SEO 支持。
這種方法的要點是,每當須要進行更改時,即便更改很小,也須要從頭開始徹底重建全部內容,而在較大的項目上,這可能會花費一些時間。所以,若是您常常進行更改,那麼對您來講這可能不是一種超級方便的方法。
經過這種方法,咱們在服務器端的每一個初始頁面請求上動態生成 HTML。注意這裏的「initial」一詞。咱們的意思是,服務器端 HTML 的生成只會在初始頁面請求(例如用戶在瀏覽器中輸入URL或刷新整個頁面時)的時候,有趣的是,在收到初始 HTML 以後,會初始化完整的 CSR SPA,這意味着該時間點的全部 HTML 都會在用戶的瀏覽器中生成,所以仍然能夠建立出色的用戶體驗。這種方法也稱爲「同構渲染」。
聽起來很不錯,但要注意,採用這種方法時,您實際上須要爲應用建立兩個獨立的生產版本,一個仍將在用戶瀏覽器中提供並執行,而另外一個將在後端執行以動態生成 HTML。建立兩個版本的緣由是不一樣的環境,也就是說在 Node.js 後端中運行瀏覽器代碼根本行不通(反之亦然)。
儘管有時沒法簡單地設置 SSR,可是一旦學習了一些技巧,您就能夠了(設置是,性能徹底是另外一回事)。使用諸如 Next.js 之類的框架能夠大大節省您的時間。
如前所述,靜態 SSR 在構建過程當中刪除 JavaScript,並用於提供純靜態 HTML 頁面。若是您的特定用例能夠接受 JavaScript 刪除,則此方法可能對您有用。
最後,一個純服務器渲染不屬於 SPA 類別,由於它根本不依賴任何客戶端渲染。HTML 老是從服務器返回,而且在您的應用程序中瀏覽時,將假定刷新了整個頁面,那麼,這與咱們最早提到的 Full CSR徹底相反。
上面顯示的摘要絕對能夠幫助咱們選擇正確的方法來渲染咱們的應用程序。可是咱們應該使用哪個呢?
其實,這取決於您正在構建的應用程序,換句話說,取決於您面前的特定需求。若是您有一個簡單的靜態網站,那麼帶有預渲染的 CSR 絕對是一個不錯的選擇。另外一方面,若是您要建立更具動態性的內容,那麼,根據您的 SEO 需求,您可能要使用 SSR 渲染與激活或簡單的 Full CSR SPA。
所以,對您的應用程序進行快速分析確定會幫助您選擇正確的方法,這正是咱們爲改進 Page Builder 應用程序在 Webiny 所作的。
下圖一目瞭然地顯示了 Webiny Page Builder 最初的工做方式:
所以,在上方的圖中,咱們有管理員用戶,他們能夠經過 admin UI 建立新頁面或編輯現有頁面。整個管理界面是一個完整的 CSR SPA(使用比較受歡的 create-react-app 建立),這沒有任何問題。
咱們有一個面向公衆的網站和普通用戶,咱們爲他們提供了完整的 CSR SPA。這裏沒有什麼超高級的。基本上,一旦應用程序經過 GraphQL API 初始化,應用程序就會獲取須要顯示給用戶當前 URL 的內容,而且差很少就能夠了。
固然,據咱們瞭解,對於面向公衆的應用程序而言,徹底 CSR 方法還不夠好,由於公共頁面必須具備 SEO 支持。只是沒有更好的辦法。所以,如今能夠查閱下 Web 文檔上的「渲染」,並嘗試選擇最佳的方法。
由於 Page Builder 本質上是動態的,這意味着一旦用戶單擊編輯器中 publish 按鈕,該頁面必須當即上線(而且固然是兼容 SEO 的),咱們選擇了第三種方法,即 SSR 渲染和激活。
可是,由於咱們知道當時咱們的代碼庫須要大量更改才能正常工做,因此實際上咱們還有一個想法,咱們想首先嚐試一下這種方法。也就是若是咱們能夠從後端訪問該 URL,就像普通用戶那樣訪問該 URL,並在 Web 爬網程序發出請求時將其返回,該怎麼辦?您知道嗎,只需模擬普通用戶,等待完整的 UI 生成,獲取最終的 HTML,而後就可使用?對於普通用戶而言,什麼都不會改變,咱們仍然會爲他們提供常規的單頁面應用,由於實際上,用戶並不關心最初從後端收到的 HTML(實際上,這確實很重要,在如下各節中將對此進行更多說明)。
咱們認爲能夠這樣作,因此咱們嘗試了一下。咱們將這種方法稱爲「按需呈現」。
所以,總而言之,咱們決定嘗試如下兩種方法:
讓咱們看看如何在無服務器環境中實現這些渲染方法,固然,從中能夠比較出哪一種方法效果更好。
如前所述,請注意,因爲咱們目前僅與 AWS 雲廠商合做,所以接下來的示例主要是基於 AWS 來實現。可是,若是您將應用程序託管在任何其餘雲上,那麼我相信您仍然可使用雲提供商提供的相似服務來實現同一目標。
好吧,讓咱們看看!
爲了實現按需預渲染,咱們使用瞭如下AWS服務:
所以,咱們使用一個 S3 Bucket 來託管 SPA 的生產版本,幾個 Lambda 函數以及最後的 API Gateway 和 CloudFront,以使全部內容在 Internet 上公開可用並分別啓用適當的緩存。
爲此,咱們還使用了 chrome-aws-lambda
庫,該庫基本上是 (Headless)
瀏覽器,能夠經過編程方式在 Lambda 函數內部進行控制。咱們將使用它來訪問網絡爬蟲程序請求的 URL,等待單頁面應用徹底初始化,獲取最終生成的 HTML,最後將輸出返回給網絡爬蟲程序。
首先,讓咱們看看普通用戶訪問網頁時會發生什麼。
當普通用戶訪問站點時,HTTP 請求將經過 CloudFront 重定向到 API 網關,該 API 網關將調用 Web 服務器 Lambda。咱們之因此給它起這個名字是由於 —— 在某種程度上,它實際上起着常規 Web 服務器的做用,即基於接收到的調用有效負載(HTTP 請求),它提供了從 S3 bucket中請求的靜態資源(JS,CSS,HTML,圖像等)。此功能的一些其餘做用是,當請求靜態資源時發送適當的緩存響應標頭,並檢測網絡爬蟲程序,所以咱們使用了 isisbot 軟件包。
因此,若是普通用戶發出 HTTP 請求,咱們只需從 S3 bucket 中獲取請求的文件,並將其做爲調用響應發送回API網關,而後將其返回給 CloudFront,就能夠返回該文件。
當網絡爬蟲訪問該站點時會發生什麼?
在這種狀況下,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 部分):
service: mySiteService provider: name: aws runtime: nodejs10.x functions: preRender: role: arn:aws:iam::222359618365:role/SOME-ROLE memorySize: 1600 timeout: 30 layers: - arn:aws:lambda:us-east-1:764866452798:layer:chrome-aws-lambda:8 handler: fns/my-server/index.handler
注意:對於完整的生產環境,您還能夠選擇本身構建該層,從而爲您提供更多的控制權和更好的安全狀態。
下圖顯示了全部優勢和缺點:
這裏要注意的是,儘管咱們設法得到了良好的 SEO 支持。但不幸的是,咱們仍然面臨着嚴重的速度/用戶體驗問題。
因爲用戶仍在接收完整的 CSR 單頁面應用,所以在每次請求時,他都必須等待初始化資源(JS 和 CSS)以及頁面數據被加載。當頁面加載時,會向用戶顯示一個加載屏幕,而且用戶在每次訪問頁面時,基本上都會在頁面上停留 1-3 秒,這絕對不是一個很好的用戶體驗,尤爲是咱們研究的靜態頁面。簡單的說就是它很慢。
即便咱們已經嘗試了一些改進的方法,但最終仍是沒法使它以可以知足咱們目標的方式工做,所以放棄了按需渲染的想法。
可是,請注意若是加載屏幕對您的應用程序沒有問題,那麼這仍然是一種有效的實現方法。我我的喜歡此解決方案,由於與採用服務器端渲染與激活方法不一樣,此方法更易於維護,由於它不須要構建兩個單獨的應用程序。
讓咱們看看咱們如今如何使用服務器端渲染與激活方法!
對於此實現,咱們實際上使用了在按需預渲染實現中相同的服務
可是固然,該圖會有所不一樣:
在解釋其所有工做原理以前,還記得咱們提到服務器渲染與激活方法須要咱們構建 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 中提取。如前所述,這與之前看到的按需預渲染方法(普通用戶訪問該站點)沒有什麼不一樣。
那麼,這種方法的結果是什麼?
有趣的是,即便咱們已經經過先前提到的按需預渲染方法解決了 SEO 兼容性問題,但咱們確實也遇到了頁面加載速度緩慢問題,這在UX方面多是很是糟糕的。不幸的是,這和採用服務器渲染與激活方法相比,二者沒有什麼不一樣。
使用按需預渲染的方法時,用戶必須盯着加載屏幕,直到應用程序徹底初始化爲止。如今,他們須要再次等待相同的時間,可是此次,他們盯着空白屏幕,等待後端返回服務端渲染的 HTML。
您可能會問本身爲何要等呢?好吧,這很合邏輯,這是由於之前在用戶瀏覽器中進行的全部處理(在加載疊加層以後)如今都在後端 SSR Lambda 函數內部進行。更重要的是,開箱即用的服務器端渲染是一項資源密集型任務,所以生成整個 HTML 文檔須要花費時間。將其與冷啓動功能可能會增長的其餘延遲配對,能夠確保您度過了一段愉快的時光。
當您查看時,因爲用戶盯着黑屏,而不是咱們之前擁有的漂亮的加載疊加,咱們實際上已經設法使用戶體驗變得更糟!
儘管咱們嘗試增長 SSR Lambda 函數的系統資源量,但這仍然沒有對總體性能產生足夠積極的影響。最後,爲了加快處理速度,咱們決定引入緩存。咱們嘗試了許多不一樣的解決方案,最後,咱們解決了以下兩個問題:
對於這二者,整個雲架構的惟一補充就是數據庫,咱們將使用該數據庫來緩存接收到的 SSR HTML。它能夠是任何您喜歡的數據庫,咱們決定使用 MongoDB,由於咱們已經很是依賴它了。可是,您可使用 DynamoDB 或 Redis,這些絕對也是不錯的選擇。
下圖幾乎與咱們在上一節中看到的圖如出一轍,只不過如今有了一個數據庫:
所以,每次 Web Server Lambda
收到來自 SSR Lambda 的 SSR HTML,在將其返回給 API 網關以前,咱們還將其存儲在數據庫中。一個簡單的數據庫條目可能看起來像這樣:
{ "_id" : ObjectId("5e144526b5705a00089efb95"), "path" : "/", "lastRefresh" : { "startedOn" : ISODate("2020-01-07T13:13:48.898Z"), "endedOn" : ISODate("2020-01-07T13:13:52.373Z"), "duration" : 3475 }, "content" : "<!doctype html><html lang=\"en\">...</html>", "expiresOn" : ISODate("2020-01-26T16:46:16.876Z"), "refreshedOn" : ISODate("2020-01-07T13:13:52.373Z") }
所以,一旦將 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
這幾乎就是整個流程。從性能角度來看,大多數狀況下,用戶會在約 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 值。
此解決方案的體系結構保持不變。
所以,用戶將盡量從 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。
同時,下面兩點須要注意:
能夠看出,咱們看到的「菜單更改」事件是一個重要事件,必須觸發不只一頁的緩存失效。可是,假設咱們要更新的輔助菜單僅位於少數頁面上。更新後,咱們絕對不想將網站的全部頁面都標記爲過時,對嗎?所以,天然而然地出現的問題是:有沒有一種方法可使咱們更有效,而且只對實際上包含更新菜單的頁面的緩存無效?
由於有這個問題,咱們決定引入 HTML 標記。換句話說,咱們利用咱們本身的 customsr-cache HTML 標記來有目的地標記不一樣的 HTML 部分/ UI 部分。
例如,若是您正在使用 Menu React 組件(由咱們的 Page Builder 應用提供)在頁面上呈現菜單,除了實際的菜單外,該組件在渲染時還將包括如下 HTML:
<ssr-cache data-class =「 pb-menu」 data-id =「 small-menu」 /\>
一個頁面能夠具備多個這樣的不一樣標記(您也能夠介紹本身的標記),而且在進行 SSR HTML 生成時,全部這些標記都將存儲在數據庫中。讓咱們看一下更新的數據庫條目:
{ "_id": ObjectId("5e2eb625e2e7c80007834cdf"), "path": "/", "cacheTags": [ { "class": "pb-menu", "id": secondary-menu" }, { "class": "pb-menu", "id": "main-menu" }, { "class": "pb-pages-list" } ], "lastRefresh": { "startedOn": ISODate("2020-01-27T10:06:29.982Z"), "endedOn": ISODate("2020-01-27T10:06:36.607Z"), "duration": 6625 }, "content": "<!doctype html><html lang=\"en\">...</html>", "expiresOn": ISODate("2020-02-26T10:06:36.607Z"), "refreshedOn": ISODate("2020-01-27T10:06:36.607Z") }
接收到的 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 客戶端。
解決方案 2 很好,但又不是最終解決方案。
對您來講是不是一種好方法的最重要因素是您網站上正在發生的更改量。若是更改(必須觸發 SSR HTML 無效的特定事件)很是頻繁地發生,例如每隔幾秒鐘或幾分鐘,那麼我絕對不建議使用這種方法,由於緩存無效性幾乎老是發生,而且以某種方式使目標無效。在這種狀況下,咱們前面提到的解決方案 1 可能會更好。分析和測試您的應用程序是關鍵。
一樣,若是長時間不訪問某個頁面,而且其 SSR HTML 同時被標記爲已過時,則首次訪問該頁面的用戶仍會看到舊頁面。由於若是您還記得,在某個鍵事件觸發了多個頁面的 SSR HTML 無效的狀況下(例如「菜單更改」事件),實際的緩存無效是由實際訪問該頁面的用戶觸發的,而不是咱們發送大量的向 CloudFront 的緩存失效請求數量,並在執行過程當中花錢。
可是總的來講,考慮到該解決方案提供的驚人的速度優點和異步緩存失效,咱們認爲這是一種很好的方法。
實際上,咱們已將其設置爲每一個新 Webiny 項目的默認緩存行爲,可是您能夠經過輕鬆刪除幾個插件切換到解決方案1。若是您想了解更多信息,請務必查看咱們的文檔。
你看到最後了嗎?哇,我很佩服你!
開個玩笑,哈哈,但願我能向您分享咱們的一些經驗,而且您從本文中得到了一些價值。
今天,咱們學到了不少不一樣的東西。從單頁應用程序的基本概念,缺少 SEO 支持以及在 Web 上呈現的不一樣方法開始,到在無服務器環境中實現其中兩種方法(最適合咱們的頁面生成器應用程序),即按需預渲染和服務器端渲染和激活。儘管在默認狀況下,兩種方法都解決了上述提到的 SEO 支持不足的問題,可是在頁面加載時間方面,這些方法都沒法提供使人滿意的性能。固然,若是您的特定應用程序不太在乎屏幕加載問題的話,那麼按需預渲染可能對您有用。可是若是沒有的話,服務器端渲染與激活多是您的最佳選擇。
咱們也能夠看到,只需使用一些 AWS serverless 服務,包括 S3,Lambda,API Gateway 和 CloudFront,就能夠在無服務器環境中相對容易地實現這些方法。儘管咱們無需管理任何物理層面上的基礎架構就可使全部這些服務正常工做,但咱們仍然須要考慮分配給 Lambda 函數的 RAM 數量。對於基本的文件服務需求,最少須要 128MB RAM,可是對於按需預渲染或服務器端渲染這種資源密集型任務,咱們必須分配更多空間。請注意分配並進行適當測試,由於這可能會影響您的每個月費用。確保檢查每一個服務的訂價頁面,並嘗試根據您的每個月流量進行估算。
最後,爲解決 SSR 生成緩慢和功能冷啓動的問題,咱們利用了 CDN 緩存,這可在性能和成本方面產生重大差別。根據最適合咱們的狀況,咱們可使用短或長的 max-age/TTL 進行緩存。若是咱們選擇使用後者,則將須要手動緩存無效。而且若是因爲內容太動態而致使出現不少此類狀況,則您可能須要從新考慮您的策略,看看使用較短的 max-age (TTL) 值是不是更好的解決方案。
一般任何問題都沒有靈丹妙藥,咱們今天討論的主題無疑是一個很好的例子。嘗試不一樣的事情是關鍵,它將幫助您找到最適合您的特定狀況的方案。
哦,順便說一句,好消息是,若是您不想折磨本身並但願避免從頭開始實現全部操做,則能夠嘗試 Webiny!您甚至能夠經過應用一組特定的插件,在咱們展現的兩種不一樣的服務器端渲染 HTML 緩存方法之間進行選擇。咱們喜歡保持靈活性。
謝謝閱讀!我叫 Adrian,是 Webiny 的全職開發人員。在業餘時間,我想寫一些關於我/咱們在一些現代前端和後端(無服務器)Web 開發工具的經驗,但願它能夠對其餘開發人員的平常工做有所幫助。
原文地址: Serverless Side Rendering — The Ultimate Guide
咱們誠邀您來體驗最便捷的 Serverless 開發和部署方式。在試用期內,相關聯的產品及服務均提供免費資源和專業的技術支持,幫助您的業務快速、便捷地實現 Serverless!
詳情可查閱: Serverless Framework 試用計劃
3 秒你能作什麼?喝一口水,看一封郵件,仍是 —— 部署一個完整的 Serverless 應用?
複製連接至 PC 瀏覽器訪問: https://serverless.cloud.tenc...
3 秒極速部署,當即體驗史上最快的 Serverless HTTP 實戰開發!
傳送門:
- GitHub: github.com/serverless
- 官網:serverless.com
歡迎訪問:Serverless 中文網,您能夠在 最佳實踐 裏體驗更多關於 Serverless 應用的開發!
推薦閱讀: 《Serverless 架構:從原理、設計到項目實戰》