React SSR前世今生!探索 SSR 技術前沿!

高級前端進階 發佈 2024-03-17T21:47:38.181441+00:00

大家好,很高興又見面了,我是"高級前端進階",由我帶著大家一起關注前端前沿、深入前端底層技術,大家一起進步,也歡迎大家關注、點讚、收藏、轉發!今天給大家帶來的是keenwon發布的一篇文章《更好的 React SSR》,原文連結在文末,話不多說,直接開始!

大家好,很高興又見面了,我是"高級前端進階",由我帶著大家一起關注前端前沿、深入前端底層技術,大家一起進步,也歡迎大家關注、點讚、收藏、轉發!

今天給大家帶來的是keenwon發布的一篇文章《更好的 React SSR》,原文連結在文末,話不多說,直接開始!

前言

前段時間,無意中看到了一個新框架:Fresh。宣稱是下一代 Web 框架。它列舉了很多功能亮點,隨便看了幾眼沒太在意,但有一段話成功的吸引了我:

The single biggest architecture decision that Fresh makes is its usage of the islands architecture pattern. This means that Fresh applications ship pure HTML to the client by default. Parts of server-rendered page can then be independently re-hydrated with interactive widgets (islands). This means that the client is only responsible for rendering parts of the page that are interactive enough to warrant the extra effort. Any content that is purely static does not have related client-side Javascript and is thus very lightweight.

簡單理解就是:Fresh 使用了 Islands Architecture,程序響應給客戶端的都是純 HTML,然後,通過可交互的 widgets 對 SSR 的部分內容進行水化(re-hydrated),靜態的部分不需要任何 JS 代碼。—— 簡直是完美!!

為何這麼說呢?這還得從性能優化和 SSR 講起...

Server Side Rendering

SSR(Server Side Rendering)相信大家都不陌生:在服務端直接運行相關框架和代碼,渲染出 HTML,發送給客戶端後再進行水合,以響應用戶的交互。這樣做的好處是大大降低了傳統 SPA 應用(也就是 CSR)的 「白屏」 時間,擁有更好的性能和用戶體驗,同時天然支持 SEO,彌補了 SPA 應用最大的短板。

拋開成本問題不談,目前的 SSR 方案,在帶來更好性能的同時,也存在一些問題。在深入探討這些問題之前,我們先一起明確下性能標準,因為畢竟這些方案和架構,在很大程度上都是為了追求更高的性能。

性能指標

在 2020 年的時候,Google 正式推出了新的 Web Vitals。我們團隊在 2021 年底,基於該標準進行了一次全面的性能優化,對其指標背後所代表的意義十分認同。它的三大核心指標分別是:

  • LCP(Largest Contentful Paint):最大內容繪製,測量加載性能。
  • FID(First Input Delay):首次輸入延遲,測量交互性。
  • CLS(Cumulative Layout Shift):累積布局偏移,測量視覺穩定性。

實踐下來的經驗就是,和以往的 FCP、FMP、TTI、FCI 等性能指標相比,新的 Core Web Vitals 顯然更加 「人性化」。它最大限度的從用戶的視角出發,來測量性能。

基於新標準來衡量頁面的性能,一些傳統方案的短板就暴露無遺了。舉個例子,比如頁面的加載性能,以往使用 DOMContentLoaded(DOM 內容加載完畢)、First Meaningful Paint(首次有效繪製)等,其實都不能有效的測量。常規的 SPA 應用,會第一時間響應一個幾乎空白的 HTML,DOM 加載完畢可能才剛開始請求數據,首次繪製的可能也僅僅是一個 Loading:

<div id="root"> 
  <img src="https://example.com/loading.gif" />
</div>

注意:web vitals 其實也還存在一些不太完善的地方。拿 LCP 來說,可能某些情況下,最大的元素並不是頁面的核心內容。比如背景圖片,在頁面渲染完畢後,背景圖可能只露出很少的部分,但是它面積很大,會被判定為 LCP 元素。

那麼我們基於 Web Vitals 的性能度量,綜合各方面因素重新審視 SSR,它又具體有什麼優勢?有什麼問題呢?

SSR 的優勢

SSR 相比傳統的 SPA 方案,性能上最大的優勢便是 LCP 了。

我們假設測量一個常規的「短視頻作品」頁面,如下:

這個頁面的 LCP 元素是最上方的視頻封面圖片(淺藍色部分)。

對於 SSR,整個頁面是直接響應給客戶端的,客戶端可以直接渲染 HTML,第一時間請求並渲染圖片。而 SPA 呢?要等到 HTML 加載完畢、CSS & JS 解析執行、請求接口並收到響應、視頻相關的 HTML 結構渲染完成、圖片加載&渲染完畢,才算做加載成功。

所以從 LCP 元素的渲染路徑上看,SSR 明顯會比傳統 SPA 更精簡、更高效。

需要特別說明的一點是數據請求,無論是 SSR 還是 CSR,都是常規的 fetch 請求,主要的區別在於網絡環境,理論上 SSR 的請求是伺服器內部的(之間的),必須要優於 CSR 的公網請求,否則 SSR 的優化效果就會大大降低。另外,SSR 還需要注意控制請求的超時時間,做好降級,否則用戶看到的會是一個比 CSR 「更白的白屏」。

CLS 這個指標是測量視覺穩定性的,更多的是和 HTML、CSS 布局相關。舉個例子:<img /> 要有明確的寬高,否則等圖片加載完成後撐開容器,必然產生布局位移。除了一些極特殊的場景外,大部分情況下,布局的時候仔細點,開發完成後多體驗幾次,基本能避免 CLS 問題。

所以 CLS 這個指標和架構選型、方案什麼的,關係都不太大,我也不認為在 CLS 上 SSR 會有多大優勢,這裡就不展開討論了。

LCP 其實可以理解為首屏渲染速度的一個可度量指標,這是 SSR 最明顯的優勢之一。其他的還有 SEO、心智上的前後端統一等...,不過我們這次主要還是圍繞性能問題討論。

SSR 的問題

FID(First Input Delay) 性能

SSR 依然存在一些性能問題,相比 SPA,SSR 在 FID 的表現上,可能會更糟糕

其實這也不難理解,從用戶的角度想,頁面很快呈現在了他的面前,那接下來肯定是操作了,上下左右的劃一划,點擊幾個 Button 或者 Tab,都是再正常不過的交互。而此時 JS 代碼可能還在解析運行中,React 正在水化(re-hydrated)頁面中,來不及或者根本無法響應用戶。

從技術的角度想,SPA 雖然在 LCP 上具有天然的劣勢,有更長的白屏時間,但是在頁面初始化時的 JS 解析執行、請求接口、渲染等操作都是比較 「分散」 的,而且在不同的異步任務中,反而一定程度上避免了 「Long Task」 的產生。SSR 則正好相反,在頁面初始階段需要密集的執行大量代碼。

更壞的情況是,hydrate 異常降級為普通 render。這個還是比較容易發生的,我就遇到過非常多次,有 class 順序不一致導致的,還有 HTML 被意外壓縮(沒用的空格和換行)導致的。這種異常降級在 production 環境通常是靜默的,表面上看沒有影響用戶的使用,但是對性能的損害卻極大。

FID 的問題在簡單的頁面上不會太明顯,但是在企業應用開發中,總是會有埋點、反垃圾、反作弊、異常監控、設備指紋、加解密等操作,會接入一些性能一言難盡的二方&三方 SDK。而且它們中有相當一部分需要在頁面初始化的時候執行。這類型的代碼對用戶也是無感知的,但對性能的影響同樣非常大。

優化 FID 是非常重要的。不少開發者可能日常對交互性能關注較少(除非卡成 PPT),但這個觀念需要轉變了。Lighthouse 8 將 TBT(實驗室版的 FID)的性能分占比,提升到了 30%,CLS 也有大幅度提升。以前優化的大都是速度,對布局穩定性和交互流暢度的重視不夠。


Lighthouse 6

Lighthouse 8

LCP

25%

25%

TBT

25%

30%

CLS

5%

15%

強依賴接口性能

首先聲明,我並不認為把代碼在服務端 renderToString 就是所謂的服務端渲染,我確實看到過這樣做的。我理解的,真正的服務端渲染,一定是包含必要數據的,渲染出的結果也基本能等同於用戶看到的最終呈現(至少是首屏)。

所以數據是非常重要的,SSR 的效果會依賴服務端接口的性能。如果服務端接口遲遲不能響應,並且前端應用沒有相應的超時和降級機制,那麼用戶看到的只比白屏更白。

在實踐中遇到過少量:數據的實時性要求很高,因為其他種種原因而無法緩存,平均響應時間大於 200ms 的接口。這種情況最好直接使用 SSG,再加上 「骨架屏」 之類優雅點的 Loading。

無意義的 hydrate 和冗餘的代碼

在日常開發中,總是會遇到一些靜態,或者大部分區域是靜態的頁面。這些頁面或區域,其實只需要服務端生成好 HTML,客戶端渲染出來就可以了。hydrate 是完全沒必要的,甚至 JS 都沒必要加載,徒增負擔。

除了客戶端運行時的不必要負擔,還增大了整個頁面的體積,因為 JSX 里的內容會重複出現在 HTML 和 JS 里。這個問題看我博客的代碼即可:

實踐中 JSX 的冗餘其實還好,更可怕的是數據,最可怕是 「不做處理,有用沒用都返回」 的數據。這些數據通常是內聯在 HTML 中,保證客戶端 hydrate 的時候可以第一時間拿到。SSR 數據占 HTML 體積 50% 以上也是常事。

複雜度

SSR 的複雜度是來自多方面的。

應用方面,需要確保穩定性,不管是守護進程還是容器伸縮,都要做好必要的保障。同時要能主動或者自動的降級,前文提到過,服務端渲染時要控制接口超時時間,沒有超時降級機制,性能的損害反而是小事。更糟糕的情況是接口長時間無響應,導致頁面也長時間無法響應,請求堆積在 node 層,直到崩潰,甚至 502。

編碼方面,需要一開始就做好規劃,考慮清楚代碼運行在服務端和客戶端的各種情況,明確哪部分必須要 SSR,哪部分可以在客戶端再執行,怎麼設計才能達到最好的性能。SSR 的部分還要兼容降級的場景。此外,還需要設法兼容無法在 server 端執行的類庫。

我個人實踐的經驗是,這兩方面的複雜度都不難克服,框架層面做好穩定性保障,編碼方面只要完全明白了原理,也不會增加太多的開發成本和心智負擔。

文章開頭提到的 Fresh,它的亮點之一就是 「邊緣節點 JIT 渲染」,但是需要部署在 Deno Deploy。使用大廠或者公司內部的雲服務,應用穩定性方面會更有保障。降級機制按需設計,在框架層面做好即可。

實例對比

上面從原理的角度做了些理論上的分析,接下來看一個實例對比。

我找來很久之前的一個活動項目,它本身是 SSR 的,單獨拉了一個分支改造為 CSR,部署了兩套一樣的環境。

其他一些信息:

  • 測試頁面是活動項目,首屏沒有動態數據,SSR 不涉及數據請求。
  • 頁面的埋點、異常監控、性能監控、指紋、加解密(JS 文件會大挺多)、eruda 等都保留著。
  • 構建、部署流程調整起來比較麻煩,這次用的都是開發的配置,production 下的優化全沒有。
  • 主要技術棧就是 React 全家桶 + CSS Modules。
  • 使用 Edge 的 devtools performance 測試,和 Chrome 差別不大。Edge 的本地化做的不錯,而且我沒有裝任何擴展插件,不會產生干擾。

先看 SSR 的結果:

然後是 CSR 的結果:

幾個關鍵信息:

  1. LCP 時長:SSR 120ms 左右,CSR 570ms 左右。即使不包含首屏數據的優化,LCP 上的優勢還是很明顯的。
  2. LCP 元素的渲染路徑:SSR 在渲染前沒有大段的 JS Task,而 CSR 需要等 JS 執行完。與我們前面的分析一致。
  3. Long Task:SSR 最長 199.71ms,而且比較集中;CSR 最長 188.96ms,分散成了 3 段。更糟糕的是 SSR 的長任務就在 LCP 元素渲染完成之後,非常容易阻塞用戶的操作。

其實 web vitals 在實驗室環境(Lighthouse)下測量並不是很準確,波動比較大,LCP 這類型的指標和實際網絡情況有關。對於更加人性化的性能指標,我們應該更加關注用戶側的真實性能數據採集,綜合分析一個時間段內的數據。

可能的解決方案

前面詳細論述了 SSR 的優勢和一些問題。效率和複雜度方面,相信在當下前端工具鏈的加持下,加上一定的學習理解,不會產生太大的影響。

接口性能差的問題,如果很普遍,那就沒必要用 SSR 了,SSG 是更好的選擇;個別情況的話,可以考慮 SSR + SSG 的混合模式,這個並不複雜,SSG 可以看成是 「構建時」 把每個頁面跑一遍 SSR,並將 HTML 存下來(預渲染)。我很久之前介紹的那套 SSR 框架,後來就演變成了這種混合的模式。目前,如果業務、交互、設計等方面允許的話,我們在開發時會更傾向於 SSG,徹底幹掉服務端數據預取這不穩定的一環,也更便於 CDN 緩存,命中率更高。

那麼其他的 FID、無意義的 hydrate、冗餘數據等問題怎麼辦呢?

Suspense SSR Architecture

2022 年 3 月,React 正式發布了 v18.0.0,帶來了新的 Suspense SSR Architecture,以提高 SSR 性能。

功能層面,引入了兩個重要的能力:

  • Streaming HTML:服務端流式 HTML。服務端儘早響應 HTML,後續,其他的 HTML 片段附帶 script 一起流式傳輸給客戶端,script 執行將 HTML 插入到正確的位置。
  • Selective Hydration:在代碼完全下載之前,儘早開始 hydrate。優先 hydrate 用戶交互的部分。

API 層面主要有這麼幾個變化:

  • hydrateRoot:新的 react-dom/client API,替換舊的 hydrate。
  • <Suspense>:不是新 API,但是在 React 18 中,通過 <Suspense> 將頁面拆分為小的、獨立的單元,這些單元可以自主的 hydrate。
  • React.lazy:也不是新 API,但是在 v18 里,支持了 SSR。
  • renderToPipeableStream:實現 Streaming HTML 的核心。

下面結合前面的 「短視頻作品」 示例,來一起解下 Suspense SSR Architecture 的具體功能點,頁面結構大體如下:

const UserInfo = React.lazy(() = >import('./UserInfo.js')) ;
const Explore = React.lazy(() = >import('./Explore.js'));
const App = (
  <Layout>
    {/* 視頻播放器 */} 
    <VideoPlayer />
    {/* 作者信息 */}
    <Suspense fallback={<UserInfoSkeleton />}>
      <UserInfo />
    </Suspense>
    {/ * 更多推薦視頻 * /}
    <Suspense fallback={<ExploreSkeleton />}>
      <Explore />
    </Suspense>
  </Layout>
);

Streaming HTML

前文提到過,SSR 強依賴於接口的性能。假如 <Explore /> 要花很長時間來 fetch data,此時就必須做出取捨,要麼忍受由此帶來的性能損耗,客戶端收到 HTML 的時間變得更長;要麼放棄 SSR,<Explore /> 使用純客戶端渲染。

實踐中我的經驗是,首先判斷是否是頁面的核心內容,或者 LCP,如果是,優先使用 SSR 渲染;其次,判斷內容是否會出現在首屏,例如 <UserInfo>,非核心但是也很重要,屬於首屏很關鍵的一部分,也優先考慮 SSR;第三,相關接口的性能,如果還算勉強合格,偶爾有波動,那麼就控制好 fetch timeout,密切觀察性能指標,通過數據分析,決定後續優化方向;最後,如果接口就是很慢,幾乎難以優化,那麼就直接放棄,打磨好交互、視覺,保證用戶體驗,SSG、Skeleton、Spinner 等各種手段能用則用,具體情況具體分析。

React 18 發布後,情況變得不一樣,我們可以 「既要、又要」,頁面核心的部分不變,直接 SSR 渲染。而其他部分,嵌套在 <Suspense> 中,立即響應 fallback,組件渲染完成之後,流式下發到客戶端,插入對應的位置。

上面的 「短視頻作品」 頁面,在客戶端呈現的過程大致如下圖,直接渲染出 <VideoPlayer /> 和兩個定製 Skeleton,而後視接口響應的速度,分別補齊 <UserInfo /> 和 <Explore />:

(說明:「虛線框」 代表占位的 Skeleton;「彩色實線框」 代表真實的 HTML)

Streaming HTML 很好的解決了服務端渲染頁面時,對接口響應速度的依賴。加載比較慢的組件,不會影響到較快的部分。

Selective Hydration

Streaming HTML 解決了 「服務端」 上的一些問題,那麼 「客戶端」 運行時的問題,還要看 Selective Hydration。

前文的 FID 問題里討論過,SSR 的 hydrate 需要一次性處理完整個頁面,這個過程可能耗時比較長,極容易產生 Long Task,阻塞客戶端對用戶操作的響應。之前處理此類問題的方案是把一部分組件通過 Code Splitting 拆分出去,在客戶端異步的 CSR,說白了就是放棄 SSR。

而在 React 18 中,React.lazy 支持了服務端渲染,可組合使用 React.lazy 和 <Suspense>。就像上面示例的 <UserInfo /> 和 <Explore />,它們的 JS 加載和 hydrate 都是獨立的,互不影響。Selective Hydration 機制打破了之前一次性水合的限制,我們可以根據需求靈活的控制,同時享受 SSR、Code Splitting 和獨立的 hydrate,三倍的快樂。

但不僅僅如此,Selective Hydration 真正的大殺器,其實是基於用戶交互的優先級調度

在 React 18 里,<Suspense> 內執行 hydrate 時,會有極小的間隙來響應用戶事件,看下圖:

(說明:「虛線框」 代表未完成 hydrate 的區塊,還無法響應用戶的交互;「彩色實線框」 代表可交互的區塊)

此時 <UserInfo /> 和 <Explore /> 都還未 hydrate,無法響應用戶的交互。用戶點擊 <Explore /> 組件,React 會認為它是更重要更緊急的部分,在 click 事件的捕獲階段,同步完成 hydrate,然後響應用戶的點擊。點擊這種離散事件是在不同階段處理的,其他連續事件,比如 mouseover,處理邏輯是不一樣的.

<Suspense> 還有一個「隱性」的好處,相信應該有人在使用 React 18 SSR 的時候,看到過這樣的異常:

There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.

它其實還起到了 hydrate 的 error boundaries 的作用,萬一發生了異常,可以避免整頁重新渲染。

Progressive Hydration

Progressive Hydration(漸進式水合)其實提出的時間要比 React 18 更早,社區里不少人很早就意識到了前文提到的 SSR 的問題,於是提出了 Progressive Hydration 的想法。它的名字就很好的闡述了它的本質:「不要一次性 hydrate,要按需 & 依次 & 漸進式的執行」。

Suspense SSR Architecture 其實就是 Progressive Hydration 的一種很好的實現,而且是框架級的支持。較早的時候,社區已經有一些在 React 之上的變通方案了。比如在 Google I/O '19 上,Chrome 團隊就演示了一個 Demo。它的核心代碼如下:

import React from 'react'
import ReactDOM from 'react-dom'

function interopDefault(mod) {
  return (mod && mod.default) || mod
}

export function ServerHydrator({ load, ...props }) {
  const Child = interopDefault(load())
  return (
    <section>
      <Child {...props} />
    </section>
  )
}

export class Hydrator extends React.Component {
  shouldComponentUpdate() {
    return false
  }

  componentDidMount() {
    new IntersectionObserver(async ([entry], obs) => {
      if (!entry.isIntersecting) return
      obs.unobserve(this.root)

      const { load, ...props } = this.props
      const Child = interopDefault(await load())
      ReactDOM.hydrate(<Child {...props} />, this.root)
    }).observe(this.root)
  }

  render() {
    return (
      <section
        ref={(c) => (this.root = c)}
        dangerouslySetInnerHTML={{ __html: '' }}
        suppressHydrationWarning
      />
    )
  }
}

這段代碼過於 hack,在服務端使用 ServerHydrator 正常渲染,在客戶端變成了 Hydrator,同時利用 dangerouslySetInnerHTML 維持 HTML 結構不變,最後使用 IntersectionObserver,在合適的時機執行 ReactDOM.hydrate。

Progressive Hydration 本質上只是一種思想,它的具體實現方案可能有很多,但核心就是 「可控的 hydrate」。要麼完全可控,像上面使用 IntersectionObserver 一樣;要麼完全不用管,框架做到完美,就是 Suspense SSR Architecture 的方向。

Islands Architecture

接下來便是 Islands Architecture,它提出的時間比較短,目前看到有 Fresh 和 astro 提到了自己使用 Islands 模型:

Build faster websites with Astro's next-gen island architecture ✨

具體的概念和優勢,文章開頭的兩篇翻譯里講的很清楚了,此處就不再贅述了。我們直接結合 Fresh 的實例,看一下 Islands Architecture 的具體呈現形式。另外,我在了解 Fresh 的時候整理了一篇簡單的入門文檔,了解 Fresh 的基本使用方法,對理解後面的討論更有幫助。

Fresh 將構成頁面的各種組件,分為 route 和 island 兩類,約定存放在 routes/ 和 islands/ 兩個目錄。Fresh 處理這兩類組件的方式完全不同:

  • Route Component:僅在服務端執行,直接響應 SSR 渲染出的 HTML 給客戶端,在客戶端不會加載和執行任何 JS 代碼,更不用說 hydrate 了。
  • Island Component:不僅在服務端執行,它的 JS 也會在客戶端加載,並且 hydrate,所以 Island 可以響應用戶的交互。

這兩類組件,便是 Islands Architecture 的核心。在這個架構中,Routes 就像是海,它就放在那裡,看得見,但無法在上面活動(交互);而 Islands 就像是島嶼,看得見也摸得著,可以在上面產生交互。Islands 之間是相互獨立的,一個崩潰不會影響另一個,同時它們的 hydrate 過程也是獨立的,hydrate 完成後即可立即響應用戶的交互(只要此時沒被別的任務阻塞)。

Route 和 Island 這兩種組件,本質上都是 Preact 組件。Island 比 Route 更 「正常」 一些,和常規的參與 SSR 的組件沒太大區別。而 Route,更像是被 Fresh 當做模板引擎使用了,JSX + Props => HTML,寫了 useEffect 也不會執行。任何需要在客戶端執行 JS 的區塊,都必須抽成一個 Island Component 獨立出去。

這是官方的一個很簡單的示例,大家可以感受下:

// routes/index.tsx

import Counter from '../islands/Counter.tsx'

export default function Home() {
  return (
    <div>
      <p>
        Welcome to Fresh. Try to update this message in the ./routes/index.tsx file, and
        refresh.
      </p>
      <Counter start={3} />
    </div>
  )
}
// islands/Counter.tsx

import { useState } from 'preact/hooks'
import { IS_BROWSER } from '$fresh/runtime.ts'

interface CounterProps {
  start: number
}

export default function Counter(props: CounterProps) {
  const [count, setCount] = useState(props.start)
  return (
    <div>
      <p>{count}</p>
      {/* 這個 disabled 可以說很細節了 */}
      <button onClick={() => setCount(count - 1)} disabled={!IS_BROWSER}>
        -1
      </button>
      <button onClick={() => setCount(count + 1)} disabled={!IS_BROWSER}>
        +1
      </button>
    </div>
  )
}

運行效果:

可以看到,客戶端加載的 JS 代碼很少:

  • main.js 和 chunk-TDJO6WAF.js 主要是 Fresh 的 runtime 代碼和 preact
  • island-counter.js 就是的 Island 組件

後續如果添加更多的可交互組件,也會類似 island-counter.js 一樣,獨立為一個個互不影響的 island。

Fresh 內部強依賴 Preact,通過 Preact 將所有組件渲染為 HTML,給 Islands 打好標記。同時 JS 的依賴收集根據約定的目錄控制好範圍。在客戶端,使用少量運行時和 Preact,完成 hydrate。

<!-- 這是 SSR 生成的 HTML 片段 -->

<div>
  <p>Welcome to `fresh`. Try updating this message in the ./routes/index.tsx file, and refresh.</p>
  <!--frsh-counter:0-->
  <div>
    <p>3</p>
    <button disabled>-1</button>
    <button disabled>+1</button>
  </div>
  </!--frsh-counter:0-->
</div>

除了 Islands Architecture 之外,Fresh 的 Just-in-time rendering + Edge Runtime 設計,也是對 「強動態化頁面」 很好的優化,雖然有一定的門檻。但這種做法,在當下看,可能有點極端了,有點過於 「next-gen」 了 ,SSG 之類的功能,還有很有必要的。

總結

簡單總結一下,SSR 在特定場景下的必要性毋庸置疑,它雖有優勢但也存在不少問題。FID 性能問題,有時讓人想優化都無從下手;無意義的 hydrate,對於純靜態的頁面,甚至沒有必要加載 JS;性能表現一定程度上依賴數據接口,波動較大等等...

針對這些問題,大家也在構思各種解決方案。比如早期的 Progressive Hydration,主要關注點在 hydrate 問題上;還有 React 官方的解決方案:Suspense SSR Architecture,雖然很強,但依然屬於現有方案的延續,僅解決了部分痛點;比較新穎的 Islands Architecture,徹底脫離了渲染框架,在更高的層面嘗試解決,目前實現起來有一定的成本,成熟的框架較少(就在我快要完成本文的時候發現了這個,大家可以參考下 awesome islands)。

不過 React 18 的新 API <Suspense>、hydrateRoot 使得實現 Islands Architecture 變得可能。也許我們可以既要又要,Suspense SSR Architecture + Islands Architecture 強強聯合。

最後,本文篇幅不短,寫作的時間跨度比較長,涉及的東西也比較多,內容大多是日常經驗所得,如有錯誤之處,歡迎留言討論。

參考資料

原文連結:https://keenwon.com/better-react-ssr/

原文作者:keenwon

關鍵字: