看懂 Serverless SSR,这一篇就够了!

了解咱们如何为每一个 Webiny 网站得到出色的 SEO 支持,以及如何在无服务器环境中使用 SSR 使其超快运行。html

本文系译文

内容概要

我确实意识到这是一篇很长的文章,请相信我不是故意写的很长。据我了解,有些人可能没有时间通篇读完,下面我准备了一个简短的内容概要:前端

  • 单页应用程序 (SPAs) 很酷,但不幸的是,对 SEO 的支持不佳;
  • 查阅这篇文章,了解有关在 Web 上进行渲染的不一样方法,而后选择最适合您的用例的方法;
  • 用 Webiny 构建的应用程序,咱们尝试了「按需预渲染」(使用 chrome-aws-lambda)和「服务端渲染与激活」; 
  • 只需几个无服务器服务就能够在 AWS 云中实现这两种方法,他们是 S三、Lambda、API 网关和 CloudFront。
  • 就用户体验方面,若是初始加载屏幕(在应用程序初始化时显示)不是问题,而且搜索引擎优化是您惟一关心的问题,则按需进行预渲染是一种很好的方法,不然可使用服务器端渲染和激活;
  • 将更多的 RAM(1600 MB+)分配给实际上将进行预渲染的 Lambda 函数,并将最小的 RAM 分配给仅用于服务静态文件的 RAM(128MB 或 256MB);
  • 尽管咱们没有尝试过,可是您可能须要对预渲染的内容进行某种形式的缓存,以便经过更快地返回初始 HTML 来得到更好的 SEO 结果;
  • 在使用服务端渲染与激活时,为生成 SSR HTML 的 Lambda 函数分配更多的 RAM;
  • 一般,SSR 是一项资源密集型任务,它会阻止您足够快地为网站提供服务,所以您极可能须要实现某种缓存;
  • 咱们使用 CloudFront CDN 来缓存 SSR HTML,并根据您所构建的应用程序,在短时间和长期缓存 TTL 之间进行选择;
  • 若是要使用长期缓存,须要处理缓存失效的状况,这个会有点棘手;
  • 有选择地进行缓存失效,或者说,若是可能的话,仅对必要的页面进行缓存失效–这样能够为您节省大量资金(缓存失效请求由 CloudFront 收取);
  • 若是内容更改很是频繁,请使用短时间缓存 TTL,由于这样更有效;
  • 好消息是,使用 Webiny,上面提到的均可以处理并按期更新维护一些方法,若是您以为不错的话,能够常常来 Webiny 查阅 🚀 🙂

这就是内容概要的所有内容了,若是您想更深刻地研究该主题,或者只是想看看咱们尝试过的无服务器方法和实现成果,我建议您继续往下看。node

Serverless Side Rendering

在 Webiny,咱们的使命是建立一个平台,使开发人员可以构建无服务器应用程序。换句话说,咱们但愿为开发人员提供适当的工具和流程,以便使用无服务器技术的开发更加轻松,高效和愉悦。最重要的是,咱们还但愿构建一个包含插件乃至现成应用程序的生态系统,这将进一步减小开发时间和成本。react

为了应用程序便于快速开发,Webiny 实际上提供了一些基本的应用供开发人员使用,其中之一就是咱们的 Page Builder 应用程序。我不想浪费您的时间,这也不是一篇作广告的文章,咱们已经为此工做了至关长的时间(并将继续这样作),尽管面临许多挑战,但无疑,最有趣的挑战之一就是以最佳方式为用户展现页面。换句话说,尽量快地展现页面,固然,还对搜索引擎优化 (SEO) 提供了出色的支持。git

为了实现上述目标,咱们不只要利用无服务器技术,并且要利用现代的单页应用程序 (SPA) 方法来构建网站和应用程序。可是事实证实,同时实现和使用全部上述提到的可能有点难度。github

SPA 很酷,可是它们有一个严重的缺点:SEO 支持很差,这是由于它们彻底是客户端渲染的,这意味着若是咱们不能彻底依靠客户端渲染 (CSR) 来渲染咱们的应用程序咱们该怎么作呢?在无服务器环境中,咱们如何处理服务器「传统上」完成的工做?咱们如何实现「无服务器端渲染」?web

在本文开始时,我直接放弃讲一些不是那么重要的内容,若是您想要拥有一个现代、快速、可扩展且通过 SEO 优化的单页应用程序,那么您确定须要关注这些内容,我会讲咱们真正想要为咱们的用户提供些什么。chrome

在本文中,我想介绍一下咱们尝试几种方法去作,也会讲哪种方法是最适合咱们的解决方案。您会看到没有一个方案能解决全部问题,像灵丹妙药同样,您选择的解决方案将取决于您正在构建的应用程序以及它自身的要求和条件。数据库

因为有不少零散部分要说,为了能给您呈现一个全面的解析,我决定从头开始讲。express

首先,让咱们谈谈单页应用程序!

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 初始化没有包含足够的信息的问题,但到目前为止,咱们仍然不能彻底依赖这些解决方案。

嗨,伙计……想象一下您在一个项目上花费了三个月,在发布以前,您意识到本身根本没有 SEO 支持。

How to deal with this?

到目前为止,只有一种可靠地解决此问题的方法,那就是为网络爬虫提供有价值的 HTML。换句话说,当网络爬虫访问您的网站时,最初提供的 HTML 必须包含诸如页面标题,适当的 meta 标记,页面内容(正文)之类的。例如:

一个 HTML 文档,其中包含资源连接,必要的meta标签,完整的页面主体等

可是,实现这一目标的最佳方法是什么?咱们是否须要在每一个页面请求上动态生成 HTML 的服务器?仍是咱们可使用其余方法?

好吧……这将是咱们看的下一个主题:在 Web 上渲染。

Rendering on the Web

实际上,在 web 上渲染应用程序有多种方法。「Rendering on the Web」是 Google 博客上的一篇文章,我看过好多遍了,写得很是好。它能够帮助您很好地了解不一样的渲染方法,并为您提供每种方法的利弊信息。

在本文的结尾,咱们能够很好地总结咱们今天可使用的全部渲染方法:

网络上不一样渲染方法的摘要

如您所见,摘要中包含了不少有用的信息。让咱们快速浏览下:

Full CSR

早先咱们都知道一种方法,就是后端返回一个简单的 HTML,在用户的浏览器中进行应用初始化。这种方法不适合作SEO,可是若是构建网页的时候不须要进行 SEO(例如管理员登录页面),那么它仍然是一种不错的方法。

CSR with Prerendering

若是您曾经与 Gatsby 一块儿工做过,则可能对这种方法很熟悉。基本上,一旦咱们准备好部署您的网站,便会开始构建过程,该过程会预先生成应用程序的全部页面,而后能够将其上传到静态文件存储中,例如 Amazon S3。

因为构建的页面包含完整的 HTML,而且不会动态生成任何内容,所以该应用将以超快的速度提供服务。最重要的是,它将拥有出色的 SEO 支持。

这种方法的要点是,每当须要进行更改时,即便更改很小,也须要从头开始彻底重建全部内容,而在较大的项目上,这可能会花费一些时间。所以,若是您常常进行更改,那么对您来讲这可能不是一种超级方便的方法。

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

经过这种方法,咱们在服务器端的每一个初始页面请求上动态生成 HTML。注意这里的「initial」一词。咱们的意思是,服务器端 HTML 的生成只会在初始页面请求(例如用户在浏览器中输入URL或刷新整个页面时)的时候,有趣的是,在收到初始 HTML 以后,会初始化完整的 CSR SPA,这意味着该时间点的全部 HTML 都会在用户的浏览器中生成,所以仍然能够建立出色的用户体验。这种方法也称为「同构渲染」。

听起来很不错,但要注意,采用这种方法时,您实际上须要为应用建立两个独立的生产版本,一个仍将在用户浏览器中提供并执行,而另外一个将在后端执行以动态生成 HTML。建立两个版本的缘由是不一样的环境,也就是说在 Node.js 后端中运行浏览器代码根本行不通(反之亦然)。

尽管有时没法简单地设置 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 部分):

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
注意:对于完整的生产环境,您还能够选择本身构建该层,从而为您提供更多的控制权和更好的安全状态。

The 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 中提取。如前所述,这与之前看到的按需预渲染方法(普通用户访问该站点)没有什么不一样。

那么,这种方法的结果是什么?

The 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 网关以前,咱们还将其存储在数据库中。一个简单的数据库条目可能看起来像这样:

{
  "_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

  • 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 条路径不会收取额外费用。此后,请求无效的每一个路径 &dollar;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 生成时,全部这些标记都将存储在数据库中。让咱们看一下更新的数据库条目:

{
  "_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 客户端,您可使用该客户端分别经过 invalidateSsrCacheByPathinvalidateSsrCacheByTags 方法经过特定的 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 缓存方法之间进行选择。咱们喜欢保持灵活性。

谢谢阅读!我叫 Adrian,是 Webiny 的全职开发人员。在业余时间,我想写一些关于我/咱们在一些现代前端和后端(无服务器)Web 开发工具的经验,但愿它能够对其余开发人员的平常工做有所帮助。

原文地址: Serverless Side Rendering — The Ultimate Guide

Serverless Framework 30 天试用计划

咱们诚邀您来体验最便捷的 Serverless 开发和部署方式。在试用期内,相关联的产品及服务均提供免费资源和专业的技术支持,帮助您的业务快速、便捷地实现 Serverless!

详情可查阅: Serverless Framework 试用计划

One More Thing

3 秒你能作什么?喝一口水,看一封邮件,仍是 —— 部署一个完整的 Serverless 应用?

复制连接至 PC 浏览器访问: https://serverless.cloud.tenc...

3 秒极速部署,当即体验史上最快的 Serverless HTTP 实战开发!

传送门:

欢迎访问:Serverless 中文网,您能够在 最佳实践 里体验更多关于 Serverless 应用的开发!


推荐阅读: 《Serverless 架构:从原理、设计到项目实战》
相关文章
相关标签/搜索