什么是流式渲染? 流式渲染主要思想是將HTML文檔分塊(chunk)并逐塊發(fā)送到客戶(hù)端,而不是等待整個(gè)頁(yè)面完全生成后再發(fā)送。
流式渲染不是什么新鮮的技術(shù)。早在90年代,網(wǎng)頁(yè)瀏覽器就已經(jīng)開(kāi)始使用這種方式來(lái)處理HTML文檔。
在 SPA (單頁(yè)應(yīng)用)流行的時(shí)代,由于 SPA 的核心是客戶(hù)端動(dòng)態(tài)地渲染內(nèi)容,流式渲染沒(méi)有得到太多關(guān)注。如今,隨著服務(wù)端渲染相關(guān)技術(shù)的成熟,流式渲染成為可以顯著提升首屏加載性能的利器。
素材來(lái)源于文章
Node.js 實(shí)現(xiàn)簡(jiǎn)單流式渲染 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 中的一等公民,其設(shè)計(jì)時(shí)考慮到了流式傳輸和低延遲。這使得 Node.js 非常適合作為 Web 庫(kù)或框架的基礎(chǔ)。
———— Node.js官網(wǎng)
Node.js 在設(shè)計(jì)之初就考慮到了流式傳輸數(shù)據(jù),考慮如下代碼:
const Koa = require ('koa' );const app = new Koa ();// 假設(shè)數(shù)據(jù)需要 5 秒的時(shí)間來(lái)獲取 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' ); });
這是一個(gè)簡(jiǎn)化的業(yè)務(wù)場(chǎng)景,運(yùn)行起來(lái)后,會(huì)在5秒的白屏后顯示一段 hello world 文字。
沒(méi)有用戶(hù)會(huì)喜歡一個(gè)會(huì)白屏5秒的網(wǎng)頁(yè)!在 web.dev 對(duì) TTFB 的介紹中,加載第一個(gè)字節(jié)的時(shí)間應(yīng)該在 800ms 內(nèi)才是良好的 web 網(wǎng)站服務(wù)。
我們可以利用流式渲染技術(shù)來(lái)改善這一點(diǎn),先通過(guò)渲染一個(gè) loading 或者骨架屏之類(lèi)的東西來(lái)改善用戶(hù)體驗(yàn)。查看改進(jìn)后的代碼:
const Koa = require ('koa' );const app = new Koa ();const Stream = require ('stream' );// 假設(shè)數(shù)據(jù)需要 5 秒的時(shí)間來(lái)獲取 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' ); });
使用流式渲染后,這個(gè)頁(yè)面最初顯示 "loading...",然后在 5 秒后更新為 "Hello World"。
需要注意的是:Safari 瀏覽器對(duì)于何時(shí)觸發(fā)流式傳輸可能有一些限制(以下內(nèi)容未找到官方說(shuō)明,通過(guò)實(shí)踐總結(jié)得到):
聲明式 Shadow DOM,不依賴(lài) javascript 實(shí)現(xiàn) 上面的代碼中,我們用到了一些 javascript,本質(zhì)上我們需要預(yù)先渲染一部分 html 標(biāo)簽作為占位,之后在用新的 html 標(biāo)簽去替換他們。這用 javascript 很好實(shí)現(xiàn),如果我們禁用了 javascript 呢?
這可能需要一些 Shadow DOM 的技巧!很多組件化設(shè)計(jì)前端框架都有 slot 的概念,在 Shadow DOM 中也提供了 slot 標(biāo)簽,可以用于創(chuàng)建可插入的 Web Components。在 chrome 111 版本以上,我們可以使用聲明式 Shadow DOM,不依賴(lài) javascript,在服務(wù)器端使用 shadow DOM。一個(gè)聲明式 Shadow DOM 的例子:
<template shadowrootmode ="open" > <header > Header</header > <main > <slot name ="hole" > </slot > </main > <footer > Footer</footer > </template > <div slot ="hole" > 插入一段文字!</div >
渲染結(jié)果如下:
可以看到,我們的文字被插入在了 slot 標(biāo)簽中,利用聲明式 Shadow DOM,我們可以改寫(xiě)上面的例子:
const Koa = require ('koa' );const app = new Koa ();const Stream = require ('stream' );// 假設(shè)數(shù)據(jù)需要 5 秒的時(shí)間來(lái)獲取 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' ); });
運(yùn)行這段代碼,和之前的代碼結(jié)果完全一致,不同的,當(dāng)我們禁用掉瀏覽器的 javascript,代碼也一樣正常運(yùn)行!
聲明式 Shadow DOM 是一個(gè)比較新的特性,可以在這篇文檔中看到更多內(nèi)容。
react 實(shí)現(xiàn)流式渲染 我們換個(gè)視角看看 react,react 18 之后在框架層面上支持了流式渲染, 下面是使用 nextjs 改寫(xiě)上面的代碼:
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 > ) }
運(yùn)行這段代碼,和之前的代碼結(jié)果完全一致,同樣也不需要運(yùn)行任何客戶(hù)端的 javascript 代碼。
關(guān)于 react 的流式渲染在這里能看到官方技術(shù)層面上的解釋。本文作為對(duì)于流式渲染的概覽,不作更細(xì)致的講解。
總結(jié) 本文從理論上探討了流式渲染相關(guān)實(shí)現(xiàn)方案,理論上,流式渲染很簡(jiǎn)單。HTTP 標(biāo)準(zhǔn)和 Node.js 很早之前就支持了這一特性。但在工程實(shí)踐中,它很復(fù)雜。例如對(duì)于 react 來(lái)說(shuō),流式渲染不僅僅需要 react 作為 UI 來(lái)支持,也需要借助 nextjs 這種元框架(meta framework)提供服務(wù)端的能力。
原文鏈接:https://juejin.cn/post/7347009547741495350
作者: 李章魚(yú)
該文章在 2024/12/11 11:24:41 編輯過(guò)