Day24 X Web Rendering Architectures

今天开始正式进入系列文的最後一个章节 - Framework, Architecture and Memory Management,探索前端架构与底层实作对於效能的影响。

提到前端的架构,又或者说技术,CSRSSRSSG 是三个最常出现的名词,你可能曾经听过这样一句话:「使用 Server Side Rendering 相较於 CSR 可以加快网页的效能。」然而事情真的是这样吗?今天将来好好认识一下这些最常见的 Web Rendering 的架构,再分析他们与前端效能有什麽关系。

Client Side Rendering (CSR)

在前端框架(React、Vue、Angular)盛行後,SPA (Single-Page-Application) 就如雨後春笋般冒出,而它的运作概念是这样的,以往会被塞满各种 element 的 HTML 档,如今只会放入一个 tag 当作「容器」而已,例如以下范例:(以 React 为例子)

<html>
  <head></head>
  <body>
    <div id="root"></div>
    <script src="./script.js"></script>
  </body>
</html>

上面 id 为 root 的 div 即成为了上面提到的容器,script.js 则是 react code ,采用这种方式後,页面的元素都将交由 react 去渲染出来并塞进容器中。

server 回传只包含容器的 HTML -> 浏览器依据 HTML 下载 JS code -> 浏览器执行 React code -> 执行完毕後,页面才完整呈现与具有互动性

采用这种方式後,我们可以不必像以前ㄧ样准备ㄧ大堆的 HTML 档案,而是透过 Router 决定该渲染出哪些元素在画面上或是该抓哪些 API data,而 routing 的过程也不再像以往ㄧ样要重新去 server 抓取页面造成页面反白,使用者体验大大的提升,这就是 Client Side Rendering。

看似美好,然而,它也产生了一些问题。

SEO 问题

SEO 对很多企业网站来说是很重要的指标,而它很大一部分得靠搜寻引擎的爬虫爬取网站的资讯,问题出在上面说的,我们的 HTML 现在只有容器在里面而已,其他内容都是由 JS 动态产生的,这会造成爬虫在爬取资料时只会爬到空荡荡的几个 tag…,也因此造成 SEO 分数低落。(有兴趣的读者可以在想观察的网页点选右键:检查网页原始码)。

其实 CSR 架构还是有办法做 SEO 优化的,例如可以在 Web Server 例如 Nginx 检测 request agent 是不是爬虫引擎,是的话就回传一个另外准备好的填满内容的 HTML file 给爬虫程序,如果 agent 不是搜寻引擎爬虫就回传原本要给 CSR 用的容器档案再到前端做渲染。还有一种方式是在 lambda@Edge 或是 Cloudflare Wrokers 这类的 edge serverless 服务做 pre-rendering,有兴趣的读者可以看看这篇文章

(另外搜寻引擎也变得越来越强了,例如 google search engine 就发展出了一个名为 second wave of indexing 的技术,有兴趣的读者可以看这篇文章,未来 SEO 也许就不再会是 CSR 的一个痛点,不过要等到所有搜寻引擎都能做到这点可能需要一段时间。)

效能与使用者体验问题

上面提到 CSR 的步骤是浏览器载入 JS 後再去执行它,最後靠 JS 才能渲染出要显示的元件与交互,但是如果你的 JS 随着专案的扩充变得非常肥大呢?浏览器在下载 JS 与执行 JS 上所花的时间都会因此增加,尽管现在浏览器已经非常快速了,仍然会对效能造成影响,而因为 CSR 是得在 JS 执行完毕时才能显示出整个网页,上述流程的 delay 连带的影响到使用者等待页面显示的时间,(当然我们可以做 code-spliting、dynamic-import 等解决 bundle size 过於肥大的优化,但这些技巧能解决的问题是有限的。)

Server Side Rendering (SSR)

其实在过去,网页几乎都是透过 Server Side Rendering 的方式产生的,如果你用传统 PHP 写过网页,就知道会是透过在服务器端处理好资料与逻辑,再直接编译成 HTML 档案,回传时使用者看到的就会是完整包含资料的 HTML。

这种架构的问题很明显,就是换页时都会经过反白与闪烁,万一网页效能又不太好,使用者很容易因为糟糕的使用者体验选择直接离开页面。

承如大家所知,CSR 的出现解决了换页时使用者体验不佳的问题,不过却衍生出了前面提过的 SEO 问题。

後来不同於传统 SSR 的架构出现了,它通常被称作 Isomorphic SSR,或是混合式 SSR。

Isomorphic SSR 仍然保持了 SPA 换页时不会闪烁的优点,并同时考虑 SSR 与 CSR 两种渲染方式,在 server side 先渲染出带有基本资料的 HTML 档案,送到前端後再进行 hydration,让网站产生可互动性。因为在 server side 就先产生了基本的资料,因此 SEO 的问题就可以得到解决。

至於 hydration 的意思则是会在 client 把 server side 先渲染出的 DOM element 加上事件监听器等属性,让 DOM element 变为动态且具有互动性,能够响应後续的资料变化。

Isomorphic SSR(本文後面都将直接称之为 SSR)与 CSR 架构最大的差别是 SSR 不必等到 JS 执行完毕後才能让使用者看到画面,在 server 回传 HTML 後使用者就能够看到页面,即使因为 JS 还没被执行,所以画面还不具备互动性,但让使用者先看到画面,再利用人的感知延迟时间去执行 JS code,可以大大减少使用者的跳出率。

有利於 SEO 分数、可以动态取得资料、使用者可以提早看到画面,看起来...SSR 超棒的啊...!!

不要忘记 SSR 需要维护一个 server 来接收请求在服务器端产生内容,这个 server 通常被称为 rendering server。Isomorphic SSR 的精髓在於希望 client side 与 server side 可以共用一部分程序码,所以这个 rendering server 通常会是 Node.js 的服务器。而维护一个服务器是需要成本的,尤其在遇到大流量时,对服务器来说就是一个负担。

另外使用 SSR 架构与逻辑会变得十分复杂,许多程序会需要分别在 server 与 client 做处理,也要特别小心不能在 server 端执行到包含如 window 等 browser 的 API,我认为会将专案复杂度往上提高一个层级。

聪明的你应该还会想到一个问题,如果网页的内容不常变动,用 SSR 架构的话每次都要重新在 server side 产生页面,即便可以做快取来避免重复渲染一样的内容,但感觉还有更好的处理方式才对。这种状况下,SSG 就是一个蛮适合的技术。

Static Site Generation (SSG)

在 CSR, SSR, SSG 三者之中,SSG 是效能最好的。

Static Site Generation 意指在 build time 时就打包成一个拥有完整内容的 HTML 档案,并且在之後的 client request 都会共用这个 HTML,这种方式也被称作 pre-rendering。

采用这种方式的优点为效能方面,因为它有较好的快取机制,例如很适合放到 CDN 上,当然,因为事先产生好内容,所以也有利於 SEO。不过它的限制也十分明显,因为页面资料的抓取(如 API call)只能透过 application build 的时候,如果需要变换内容,就得重新 build 一次,因此比较适合使用在内容不需要经常变换的页面,例如部落格、形象网站这种应用。而当 Web App 越来越肥大时,打包的时间会随之增长也是需要考量的问题。

Incremental Site Rendering (ISR)

SSG 虽然效能很好,但也暴露了一个问题,因为必须在 build time 就打包好网站的页面,所以如果当你的网站有几千个几万个页面...例如陈列商品的网站,一但数据有改动,需要重新打包网站,这个改动是很可怕的,因此後来就出现了一些新的架构想要来解决 SSG 的问题,今天要介绍的 ISR 就是其中一种。

ISR 最早是由 Next.js 9.5 版时提出的概念,是一种混用 SSR 与 SSG 的技术。如果页面有几百页几千页,那其实并不一定要急着在 build time 就打包出所有的页面,有些页面可以在 runtime 时再产生。咦,那这样跟 SSR 差在哪啊?如果是 SSR 的话,每一次发出对页面的 request 时,使用者必须等待页面资料抓取完毕才能看到画面,而如果是 ISR 的话,第一次对页面发出 request 时,会先回传 fallback 的画面给使用者,这个 fallback 页面可能包含一些 placeholder 与 skeleton,在呈现 fallback 页面时,同时也会去抓取需要的资料,等资料准备好後,完整的页面可以被拿到 CDN 做快取,後续的使用者如果对同样页面发出请求,就可以直接到 CDN 拿取,就像使用 SSG 的时候一样。

在 Next.js 里,还可以指定什麽时候要 re-validate data 并更新页面,在 re-validate 的期间,会先回传先前被 cache 的版本,等资料更新後才会切换到更新後的页面,并更新 CDN 中的快取。这样的快取策略也被称作「stale-while-revalidate」。

// 在 Next.js 中要启用 ISR 很简单,跟 SSG 的写法很像,只是要多指定需要 revalidate 的时间,下面的范例代表每 60 秒就会 revalidate 一次页面。

export async function getStaticProps() {
  const res = await fetch('https://...');
  const data = await res.json();

  return { props: { data }, revalidate: 60 };
}

如果读者擅长的框架是 Vue.js 的话,Nuxt.js 也有 ISR 相关的解决方案,有兴趣可以参考这篇文章

初探 Streaming server rendering and Progressive Rehydration

过去混合式 SSR 会遇到一些问题,造成网页效能出现一些瓶颈:

  • 必须先拉取所有需要的 API 资料才能开始组装 html
  • 必须载入完所有的 JavaScript 才能做 hydration
  • 必须 hydration 所有的 Component 之後页面才可以互动

Streaming server rendering 的出现就是为了解决这些问题,Streaming server rendering 让服务器可以用 streaming 的方式传送页面内容,让浏览器可以渐进的去接收 chunks,不用等整份 HTML 渲染完成,会有比较快的 FP, FCP(稍後会提到这些指标)。Progressive Rehydration 则可以做到不用等到所有 component 都 hydrate 完就能够让部分元件与使用者互动,例如以下这张图。

未来预计推出的 React v18 就推出了 Suspense SSR 的概念,藉由 Suspense Component 的帮助来达成以元件为单位的 streaming Sever Side Rendering 与 hydration,提升画面呈现速度跟可互动速度,有兴趣的读者可以看看 React 官方的 Github 讨论串,真的十分精彩。

不同架构对网站效能与指标的影响

还记得在 Day04 的时候介绍过一些跟网站效能相关的指标吗?今天想藉这个机会再补充几个与效能相关的指标:

Time To First Byte (TTFB)

TTFB 这个指标关系到 network speed、server response time,代表发出页面请求後到接收到 response 的第一个 byte 的时间总长度。这个过程包含了 DNS resolve, TCP connection, 发出 HTTP request 到获得 HTTP Response 第一个 byte 的时间。

First Paint (FP)

任何一个 pixel(像素)被浏览器绘制到页面上的时间,例如页面的 backgorund color。

First Contentful Paint (FCP)

中文为「首次内容绘制」,当浏览者到达网站之後,首次显示网站内容需要的时间,也是指浏览器第一次显示文字或图片的时间,测试第一次显示原因是第二次再显示网站的时候,浏览器已经有快取档案了,因此会没有那麽准确。

Time To Interactive (TTI)

页面从不能互动到可以接收事件产生互动性的时间,例如使用者除了可以看到页面外还能进行输入的操作。

了解这些指标後,接着来看看各种渲染架构对这些指标造成的影响。(内容主要出自 Google 非常有名的这篇文章

传统的 SSR

指的是以前由服务器端渲染出完整 HTML 的 SSR 架构。这样的架构会有较快的 FP 与 FCP,另外因为避免在 client 发送大量的 JavaScript,因此也会有较快 TTI。

传统的 SSR 的缺点也很明显,因为要在 server side 生成完整页面,因此 Time To First Byte (TTFB) 的时间会比较久。

Client Side Rendering

CSR 直接使用 JavaScript 在浏览器上渲染页面,如果随着专案扩展, JavaScript 的 bundle size 过於肥大,将会严重影响 FCP 与 TTI,使用者可能长时间看到的是空白或者不完整、还无法互动的页面。

另外如果是在硬体效能比较差的 mobile device 跑 CSR 的网页,有时候在 Client Side 利用 JavaScript 渲染内容会是一个蛮大的负担。

要解决 CSR 的这些问题,可以使用先前介绍过的 Code Splitting 与 Lazy Loading 等拆分 bundle 或延後载入的技术,另外也可以透过 preload resource hint 或是 HTTP2 的 server push 来加快资源的载入时间。

SSG

相信大家应该已经很了解 SSG 与它的优缺点了,这边就不再赘述。

混合式的 Isomorphic SSR

混合式的 SSR 的 FCP 会比 CSR 来得快,但因为需要在前端做 hydration 之後画面才会具有互动性,因此 TTI 可能会变很长。使用这种架构时要注意在 server side 不要处理太多的资料,建议是把 SSR 用在渲染一些可以被 cache 的资源,除了可以简短 TTFB 之外,也有机会达到接近 pre-rendering 的速度。

最後附上一张 Google 发布的图表来统整不同架构的优缺点与相关资讯。

如何选择要使用哪种架构?

在本篇开头有提到说你可能听过这样一句话:「使用 Server Side Rendering 相较於 CSR 可以加快网页的效能。」事情真的是如此吗?

It depands. 需要视情况而论。

使用 SSR 的确有机会让页面载入速度快一点,比起 CSR 更有机会让使用者先看到画面而减少跳出率。不过如果在 server side 要处理的事情太多,反倒会让网页的载入速度变得缓慢,因此建议在使用 SSR 时只需要在 server side 处理对 SEO 相对重要的资料,其他的部分可以在 client side 用 CSR 的方式渲染,毕竟这也是混合式 SSR 的一大特色。

至於 SSR 与 SSG 之间的取舍,SSG 的效能的确比较好,因为它在 build 的时候就已经产生资料,使用者发起对页面的请求时,服务器可以直接回应已经产生好的 HTML 档案,并且这个经过 pre-rendering 的网站还适合放到 CDN 做快取,加快後续请求的回应速度。但当页面是有频繁变动的需求时,就不太适合 SSG 的方式了,此时采用 SSR 呼叫 API 动态产生内容就是比较适合的方式,不过 SSR 架构下就要去维护一个 rendering server 并 handle server 面对大流量的状况,因此我觉得仍然得看自身状况去做架构的取舍。

幸好现在许多的 Server Side Rendering 框架,例如 Next.js,已经支援 page level 的架构选择,也就是说不同页面可以针对需求决定要使用 CSR, SSR, SSG, 还是 ISR 等不同的渲染架构,真的非常方便,如果想更深入了解 Next.js 这种 SSR 框架,可以订阅团队夥伴 Airwaves 的系列文喔!

本日小结

今天大致介绍了几种常见的 Web 前端应用架构(又或者说是技术),每一种架构都有各自的优缺点与适合使用的时机,学会在适合的时机使用适合的架构,对网站的效能会产生很大的影响,这也是我把这些渲染技术列入系列文主题的原因。

通常在谈 Web 渲染架构时,大多只会得到 CSR, SSR, SSG 三种较常见与成熟的架构,近期 ISR 可能也渐渐崭露头角,不过我明天想要再介绍一个较冷门且尚未成熟的架构--ESR,看看它的概念是什麽与对网页效能的影响,明天见罗!

References & 图片来源

https://medium.com/tiny-code-lessons/client-side-static-and-server-side-rendering-e2769c381c09

https://www.netlify.com/blog/2020/04/14/what-is-a-static-site-generator-and-3-ways-to-find-the-best-one/

https://blog.logrocket.com/incremental-static-regeneration-with-next-js/

https://arunoda.me/blog/what-is-nextjs-issg

https://github.com/reactwg/react-18/discussions/37


<<:  今天就改变你的人生!不要寄望将来,立即行动,停止拖延。

>>:  Day24 Gin with Cache

[Lesson9] Firebase

首先到下列的网站 https://console.firebase.google.com/?hl=z...

DAY14:玉山人工智慧挑战赛-中文手写字辨识(OpenCV图像处理)

问题及解决方法 用YOLOv4模型裁切出来的文字,大部分的图档,都有红框等杂讯的存在,如下图。若将含...

Day 30 赛後感想

第一次参加铁人赛,很开心能写完30天的文章,原本还以为会开天窗,不过幸好没有 这两个月的暑假学了许多...

[day6] AES-CBC 内文加密机制(Message)

讯息文本使用AES-CBC模式加密传送,接收的结果亦以相同规则加密 必要的参数 如何取得 JSON讯...

Day-04 JavaScript资料型别(3)

常见的JavaScript资料型别,可概分为基本型别(primitives)与物件型别(object...