如何通過流式渲染提升用戶體驗?

什么是流式渲染?

流式渲染的核心理念是將 HTML 文檔分割成小塊(chunk),并逐步地發送給客戶端,而非等待整個頁面完整生成后再進行傳輸。這種方式能夠極大地提升用戶的初始加載體驗,特別是在網絡條件不佳或者頁面內容復雜的情況下。

流式渲染并非新興技術,早在 90 年代,網頁瀏覽器就已開始運用這種模式來處理 HTML 文檔。不過,在 SPA(單頁應用)大行其道的時期,由于其核心在于客戶端動態渲染內容,流式渲染未能引起廣泛關注。然而,現今隨著服務端渲染技術的日臻成熟,流式渲染已成為顯著優化首屏加載性能的有力手段。

Node.js 實現簡單流式渲染

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(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 瀏覽器對于何時觸發流式傳輸可能存在一些限制(以下內容未找到官方說明,而是通過實踐總結得出):

  • 傳輸的 chunk 大小需大于 512 字節。若小于此值,可能無法有效觸發流式傳輸,影響用戶體驗。
  • 傳輸的內容必須能夠在屏幕上實際渲染。例如,傳輸<div style="display:none;">...</div>這樣隱藏的內容可能是無效的,無法實現流式渲染的預期效果。

聲明式 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 '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 的流式渲染,您可以在官方的技術層面解釋中獲取更深入的信息。在本文中,僅作為對流式渲染的概要介紹,不對其進行更為細致的講解。

總結

本文從理論層面深入探討了流式渲染的相關實現方案。理論上,流式渲染的概念和實現相對簡單。HTTP 標準和 Node.js 早在很久以前就對這一特性提供了支持。然而,在實際的工程應用中,流式渲染并非易事。以 React 為例,要實現流式渲染,不僅需要 React 自身作為用戶界面(UI)框架提供支持,還需要借助像 nextjs 這樣的元框架(meta framework)來賦予服務端相應的能力。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/38122.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/38122.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/38122.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【從零開始學架構 架構基礎】四 架構設計的復雜度來源:可擴展性復雜度來源

架構設計的復雜度來源其實就是架構設計要解決的問題&#xff0c;主要有如下幾個&#xff1a;高性能、高可用、可擴展、低成本、安全、規模。復雜度的關鍵&#xff0c;就是新舊技術之間不是完全的替代關系&#xff0c;有交叉&#xff0c;有各自的特點&#xff0c;所以才需要具體…

新書速覽|Linux C與C++一線開發實踐

《Linux C與C一線開發實踐》 本書內容 Linux C/C編程在Linux應用程序開發中占有重要的地位&#xff0c;掌握這項技術將在就業競爭中立于不敗之地。《Linux C與C一線開發實踐》內容針對初中級讀者&#xff0c;貼近軟件公司一線開發實踐。全書厚達620多頁&#xff0c;知識點豐富…

Java中String和StringBuilder的區別

當然可以&#xff0c;我們可以通過面試問答的形式來探討String和StringBuilder的區別。 面試官&#xff1a;請解釋一下Java中String和StringBuilder的區別。 面試回答&#xff1a; 1. 不可變性&#xff08;Immutability&#xff09; String&#xff1a;String對象是不可變的…

微信小程序添加點擊事件

在微信小程序中&#xff0c;給<view>組件添加點擊事件非常直接&#xff0c;你可以使用bindtap屬性來綁定一個事件處理函數。下面是添加點擊事件的基本步驟和示例代碼&#xff1a; 步驟&#xff1a; 在WXML文件中&#xff1a;給需要添加點擊事件的<view>標簽添加bi…

第六周周報

摘要 本周重點跟著網課學習了pytorch框架下張量的各種常用操作API&#xff0c;為后面跑模型做準備&#xff0c;因為看的視頻比較偏向原理&#xff0c;現在對張量有了一個新的認識。其次在時序的研究上&#xff0c;最近我在看圖神經網絡跟時序結合的方向&#xff0c;所以本周學…

Qt自定義類型

概述 在使用Qt創建用戶界面時&#xff0c;特別是那些具有特殊控件和特性的界面時&#xff0c;開發人員有時需要創建新的數據類型&#xff0c;以便與Qt現有的值類型集一起使用或代替它們。 QSize、QColor和QString等標準類型都可以存儲在QVariant對象中&#xff0c;作為基于qo…

51單片機第6步_stdlib.h庫函數

本章重點學習stdlib.h庫函數。 #include <REG51.h> //包含頭文件REG51.h,使能51內部寄存器; #include <stdlib.h> //float atof (char *s1); //參數s1字符串可包含正負號,小數點或E(e)來表示指數部分,如123.456或123e-2; //若首字符是非數據字符,或為正負號…

es6語法復習一

es6語法 1.var 變量提升 2.let 不存在變量提升&#xff0c;只能定義一次 3.const 先定義再使用&#xff0c;定義好來不能修改 4.解構賦值 [a,b,c][1,2,3],{a,b,c}{a:1,b:2,c:3} 5.模版字符串 let aaa; ${a} is ok 6.對象簡化寫法 const school{ name, change, improve(){ cons…

力扣2438.二的冪數組中查詢范圍內的乘積

力扣2438.二的冪數組中查詢范圍內的乘積 lowbit求所有2的冪 accumulate函數(begin,end,start,way)求和/積的方式求積并取模 const int N 1e9 7;class Solution {public:int lowbit(int x){return x & -x;}vector<int> productQueries(int n, vector<vector&l…

[NSSCTF]-Reverse:[SWPUCTF 2021 新生賽]easyapp(安卓逆向,異或)

無殼 把后綴名改為zip&#xff0c;找到apk 查看jadx 這里調用了MainActivity的lambda$onCreate$0$MainActivity&#xff0c;然后又調用了Encoder進行異或。 exp&#xff1a; result棿棢棢棲棥棷棊棐棁棚棨棨棵棢棌 key987654321 flag for i in range(len(result)):flagchr(…

HarmonyOS開發:應用完整性校驗

簡介 為了確保應用的完整性和來源可靠&#xff0c;OpenHarmony需要對應用進行簽名和驗簽。 應用開發階段&#xff1a; 開發者完成開發并生成安裝包后&#xff0c;需要開發者對安裝包進行簽名&#xff0c;以證明安裝包發布到設備的過程中沒有被篡改。OpenHarmony的應用完整性校…

Foxit Reader與PDF交互性:探索高級功能

引言 PDF&#xff08;Portable Document Format&#xff09;文件格式以其跨平臺的一致性和豐富的多媒體支持而廣受歡迎。Foxit Reader作為一款功能全面的PDF閱讀器&#xff0c;不僅提供了基本的查看和導航功能&#xff0c;還支持PDF文件中的多種交互式元素。本文將深入探討Fox…

SQL Server中 MERGE 語句

在 SQL Server 中,MERGE 語句用于根據兩個表之間的條件來插入、更新或刪除記錄。它通常用于同步兩個表的數據,其中一個表是源表(包含要插入或更新的數據),另一個是目標表(數據要插入或更新的表)。 1、本文內容 語法參數備注觸發器的實現權限有關索引的最佳做法MERGE 的…

探索sklearn的貝葉斯奧秘:樸素貝葉斯分類器全解析

&#x1f680; 探索sklearn的貝葉斯奧秘&#xff1a;樸素貝葉斯分類器全解析 樸素貝葉斯分類器是一類基于貝葉斯定理的簡單概率分類器&#xff0c;它們在文本分類、垃圾郵件識別等領域表現出色。在Python的sklearn庫中&#xff0c;樸素貝葉斯分類器以其實現簡單和效率高效而受…

關于響應式編程的理解與SpringCloudGateway的理解

關于響應式編程的理解與SpringCloudGateway的理解 一. 響應式編程與函數式編程的區別二. 響應式編程中常用的組件2.1 RxJava定義2.2 Rxjava基本概念2.3 RxJava 用法 三 SpringcloudGateway四 常見的四種限流規則 一. 響應式編程與函數式編程的區別 總的來說&#xff0c;響應式編…

qt中的枚舉值-QMetaEnum

QMetaEnum 測試代碼hcpp 講解 測試代碼 h #include <QMainWindow> #include <QDebug>QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACEclass MainWindow : public QMainWindow {Q_OBJECTpublic:MainWindow(QWidget *parent nullptr);~M…

GPIO和PIN

文章目錄 1 GPIO和Pin1.1 GPIO和Pin基礎概念1.2 GPIO輸入模式1.3 GPIO輸出模式1.4 GPIO的HAL庫1.4.1 一些HAL庫表示1.4.2 HAL庫常用GPIO函數1.4.3 GPIO點亮led燈程序例子 1 GPIO和Pin 1.1 GPIO和Pin基礎概念 ? 單片機有很多的引腳&#xff0c;為了操控每一個引腳&#xff0c…

grpc學習golang版( 四、多服務示例 )

系列文章目錄 第一章 grpc基本概念與安裝 第二章 grpc入門示例 第三章 proto文件數據類型 第四章 多服務示例 第五章 多proto文件示例 第六章 服務器流式傳輸 第七章 客戶端流式傳輸 第八章 雙向流示例 文章目錄 一、前言二、定義proto文件三、編寫server服務端四、編寫Client客…

MySQL之可擴展性(九)

可擴展性 直接連接 2.修改應用的配置 還有一個分發負載的辦法是重新配置應用。例如&#xff0c;你可以配置多個機器來分擔生成大報表操作的負載。每臺機器可以配置成連接到不同的MySQL備庫&#xff0c;并為第N個用戶或網站生成報表。 這樣的系統很容易實現&#xff0c;但如果…

使用Python自動化收集和處理視頻資源的教程

在這篇教程中&#xff0c;我們將介紹如何利用Python腳本自動化收集和處理視頻資源。這篇文章將幫助您掌握基本的網絡自動化技術&#xff0c;并使用相關庫進行視頻資源的獲取和保存。以下是具體的實現步驟和代碼示例。 環境準備 在開始之前&#xff0c;請確保您的工作環境中已…