流式渲染的核心理念是將 HTML 文檔分割成小塊(chunk),并逐步地發送給客戶端,而非等待整個頁面完整生成后再進行傳輸。這種方式能夠極大地提升用戶的初始加載體驗,特別是在網絡條件不佳或者頁面內容復雜的情況下。
流式渲染并非新興技術,早在 90 年代,網頁瀏覽器就已開始運用這種模式來處理 HTML 文檔。不過,在 SPA(單頁應用)大行其道的時期,由于其核心在于客戶端動態渲染內容,流式渲染未能引起廣泛關注。然而,現今隨著服務端渲染技術的日臻成熟,流式渲染已成為顯著優化首屏加載性能的有力手段。
HTTP 是 Node.js 中的一等公民,其在設計時就充分考慮了流式傳輸和低延遲特性。這使得 Node.js 極為適合作為 Web 庫或框架的構建基礎。 ———— Node.js 官網
Node.js 從設計之初就將流式傳輸數據納入考量,以下是一個簡單的示例代碼:
const Koa = require('koa');const app = new Koa();// 假設數據需要 5 秒的時間來獲取renderAsyncString = async () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve('<h1>Hello World</h1>'); }, 5000); })}app.use(async (ctx, next) => { ctx.type = 'html'; ctx.body = await renderAsyncString(); await next();});app.listen(3000, () => { console.log('App is listening on port 3000');});
這是一個簡化的業務場景,運行之后,會出現長達 5 秒的白屏,然后才顯示出"Hello World"這段文字。
毫無疑問,沒有用戶會愿意忍受一個長達 5 秒的白屏網頁!在 web.dev[1] 對于 TTFB(Time To First Byte,首字節時間)的介紹中提到,加載第一個字節的時間應當控制在 800ms 以內,才能稱得上是優質的 Web 網站服務。
為了改善這種情況,我們可以借助流式渲染技術。比如,先向用戶呈現一個加載中的提示或者骨架屏,以此來優化用戶體驗。下面是改進后的代碼:
const Koa = require('koa');const app = new Koa();const Stream = require('stream');// 假設數據需要 5 秒的時間來獲取renderAsyncString = async () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve('<h1>Hello World</h1>'); }, 5000); })}app.use(async (ctx, next) => { const rs = new Stream.Readable(); rs._read = () => {}; ctx.type = 'html'; rs.push('<h1>loading...</h1>'); ctx.body = rs; renderAsyncString().then((string) => { rs.push(`<script> document.querySelector('h1').innerHTML = '${string}'; </script>`); })});app.listen(3000, () => { console.log('App is listening on port 3000');});
采用流式渲染后,頁面最初會顯示"loading...",然后在 5 秒后更新為"Hello World"。
需要特別注意的是,Safari 瀏覽器對于何時觸發流式傳輸可能存在一些限制(以下內容未找到官方說明,而是通過實踐總結得出):
<div style="display:none;">...</div>
這樣隱藏的內容可能是無效的,無法實現流式渲染的預期效果。在上述的代碼中,我們運用了一定的 JavaScript 代碼。本質上,我們需要預先渲染一部分 HTML 標簽作為占位,隨后再用新的 HTML 標簽對其進行替換。使用 JavaScript 來實現這一過程相對容易,但如果禁用了 JavaScript 呢?
這就可能需要借助一些 Shadow DOM[2] 的技巧!眾多組件化設計的前端框架都包含了 slot(插槽)的概念,在 Shadow DOM 中也提供了 slot 標簽,其可用于創建可插入的 Web Components。在 Chrome 111 及以上版本中,我們能夠使用聲明式 Shadow DOM,無需依賴 JavaScript,在服務器端就能實現 shadow DOM 的功能。以下是一個聲明式 Shadow DOM 的示例:
<template shadowrootmode="open"> <header>Header</header> <main> <slot name="hole"></slot> </main> <footer>Footer</footer> </template> <div slot="hole">插入一段文字!</div>
從中可以清晰地看到,我們的文字成功插入到了 slot 標簽之中。利用聲明式 Shadow DOM,我們能夠對之前的示例進行改寫:
const Koa = require('koa');const app = new Koa();const Stream = require('stream');// 假設數據需要 5 秒的時間來獲取renderAsyncString = async () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve('<h1>Hello World</h1>'); }, 5000); })}app.use(async (ctx, next) => { const rs = new Stream.Readable(); rs._read = () => {}; ctx.type = 'html'; rs.push(` <template shadowrootmode="open"> <slot name="hole"><h1>loading</h1></slot> </template> `); ctx.body = rs; renderAsyncString().then((string) => { rs.push(`<h1 slot="hole">${string}</h1>`); rs.push(null); })});app.listen(3000, () => { console.log('App is listening on port 3000');});
運行這段改寫后的代碼,其結果與之前完全相同。更為重要的是,即便我們禁用了瀏覽器的 JavaScript,代碼依然能夠正常運行!
聲明式 Shadow DOM 是一個相對較新的特性,您可以在這篇文檔[3]中獲取更多詳細信息。
現在讓我們轉換視角,來看看 React 框架中的流式渲染。自 React 18 版本之后,在框架層面上開始支持流式渲染。下面是使用 nextjs 對之前的示例進行改寫的代碼:
import { Suspense } from 'eact'const renderAsyncString = async () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve('Hello World!'); }, 5000); })}async function Main() { const string = await renderAsyncString(); return <h1>{string}</h1>}export default async function App() { return ( <Suspense fallback={<h1>loading...</h1>} > <Main /> </Suspense> )}
運行這段代碼,其效果與之前的示例完全一致,并且同樣無需運行任何客戶端的 JavaScript 代碼。
關于 React 的流式渲染,您可以在官方的技術層面[4]解釋中獲取更深入的信息。在本文中,僅作為對流式渲染的概要介紹,不對其進行更為細致的講解。
本文從理論層面深入探討了流式渲染的相關實現方案。理論上,流式渲染的概念和實現相對簡單。HTTP 標準和 Node.js 早在很久以前就對這一特性提供了支持。然而,在實際的工程應用中,流式渲染并非易事。以 React 為例,要實現流式渲染,不僅需要 React 自身作為用戶界面(UI)框架提供支持,還需要借助像 nextjs 這樣的元框架(meta framework)來賦予服務端相應的能力。
本文鏈接:http://www.tebozhan.com/showinfo-26-96975-0.html我們一起聊聊如何通過流式渲染提升用戶體驗?
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 面試官:Transient關鍵字修飾的變量當真不可序列化?
下一篇: 純 CSS 檢測滾動的速度和方向