什么是流式渲染? 流式渲染主要思想是將HTML文檔分塊(chunk)并逐塊發送到客戶端,而不是等待整個頁面完全生成后再發送。
流式渲染不是什么新鮮的技術。早在90年代,網頁瀏覽器就已經開始使用這種方式來處理HTML文檔。
在 SPA (單頁應用)流行的時代,由于 SPA 的核心是客戶端動態地渲染內容,流式渲染沒有得到太多關注。如今,隨著服務端渲染相關技術的成熟,流式渲染成為可以顯著提升首屏加載性能的利器。
素材來源于文章
Node.js 實現簡單流式渲染 HTTP is a first-class citizen in Node.js, designed with streaming and low latency in mind. This makes Node.js well suited for the foundation of a web library or framework.
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 對 TTFB 的介紹中,加載第一個字節的時間應該在 800ms 內才是良好的 web 網站服務。
我們可以利用流式渲染技術來改善這一點,先通過渲染一個 loading 或者骨架屏之類的東西來改善用戶體驗。查看改進后的代碼:
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 瀏覽器對于何時觸發流式傳輸可能有一些限制(以下內容未找到官方說明,通過實踐總結得到):
聲明式 Shadow DOM,不依賴 javascript 實現 上面的代碼中,我們用到了一些 javascript,本質上我們需要預先渲染一部分 html 標簽作為占位,之后在用新的 html 標簽去替換他們。這用 javascript 很好實現,如果我們禁用了 javascript 呢?
這可能需要一些 Shadow DOM 的技巧!很多組件化設計前端框架都有 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 是一個比較新的特性,可以在這篇文檔中看到更多內容。
react 實現流式渲染 我們換個視角看看 react,react 18 之后在框架層面上支持了流式渲染, 下面是使用 nextjs 改寫上面的代碼:
import { Suspense } from 'react'
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 的流式渲染在這里能看到官方技術層面上的解釋。本文作為對于流式渲染的概覽,不作更細致的講解。
總結 本文從理論上探討了流式渲染相關實現方案,理論上,流式渲染很簡單。HTTP 標準和 Node.js 很早之前就支持了這一特性。但在工程實踐中,它很復雜。例如對于 react 來說,流式渲染不僅僅需要 react 作為 UI 來支持,也需要借助 nextjs 這種元框架(meta framework)提供服務端的能力。
原文鏈接:https://juejin.cn/post/7347009547741495350
作者: 李章魚
該文章在 2024/12/11 11:24:41 編輯過