关于 client-side-rendering
介绍客户端渲染(CSR)的学习技能,由浏览器中的 JavaScript 负责页面渲染、数据获取和路由。内容涵盖适用场景、权衡因素及加载性能优化方法。
主要功能
- 01使用浏览器 JavaScript 渲染页面
- 02支持单页应用导航
- 03说明 SEO 与加载时间的权衡
- 04介绍代码拆分和延迟加载
- 05讲解 Service Worker 外壳缓存
适用场景
- 01构建交互式内部仪表板
- 02创建无需刷新页面的 SPA
- 03通过延迟加载减少初始 JavaScript
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ patternsdev/ skills --skill client-side-rendering安装后在 Claude Code 中输入/client-side-rendering确认已加载。
详细介绍
客户端渲染
在客户端渲染(CSR)中,服务器只渲染页面的基本 HTML 容器。显示页面内容所需的逻辑、数据获取、模板渲染和路由,均由在浏览器/客户端中执行的 JavaScript 代码处理。CSR 作为构建单页应用的方法而广受欢迎,也模糊了网站与已安装应用之间的界限。
适用场景
- 适用于 SEO 优先级不高的内部工具、仪表板或 SPA
- 适用于需要完全交互式单页应用体验的场景
不适用场景
- 对 SEO 至关重要,搜索引擎需要通过服务器渲染的 HTML 为内容建立索引的页面
- 内容量较大、用户必须等到 JavaScript 加载并执行后才能看到内容的网站
- 首次内容绘制时间是关键指标的场景——CSR 会将所有渲染工作推迟到客户端
说明
- 保持初始 JavaScript 包较小(压缩并经过 gzip 后 < 100-170KB),以实现较快的首次内容绘制
- 使用代码拆分和延迟加载,推迟非关键 JavaScript 的加载
- 对于 SEO 和初始加载性能很重要的面向公众页面,可考虑 SSR/SSG
- 使用 Service Worker 和应用 Shell 缓存,提升离线访问和重复访问时的性能
详情
基本结构
考虑这个简单示例:使用 React 在页面上显示并更新当前时间。
import { createRoot } from 'react-dom/client'
import { useEffect, useState } from 'react'
function Clock() {
const [time, setTime] = useState(new Date())
useEffect(() => {
const id = setInterval(() => setTime(new Date()), 1000)
return () => clearInterval(id)
}, [])
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {time.toLocaleTimeString()}.</h2>
</div>
)
}
createRoot(document.getElementById('root')!).render(<Clock />)
<div id="root"></div>
HTML 只包含一个根 <div> 标签。另一方面,内容的显示和更新完全由 JavaScript 处理。此过程不会与服务器往返通信,渲染后的 HTML 会直接在原处更新。这里的时间可以替换为其他实时信息,例如从 API 获取的汇率或股价,并在不刷新页面、也不与服务器往返通信的情况下显示。
JavaScript 包与性能
随着页面复杂度增加,例如需要显示图片、展示数据存储中的数据并包含事件处理,渲染页面所需的 JavaScript 代码也会变得更复杂、体积更大。CSR 会产生较大的 JavaScript 包,导致页面的 FCP 和 TTI 延迟。
随着 bundle.js 体积增大,FCP 和 TTI 会推迟。这意味着用户从 FP 到 FCP 期间看到的将一直是空白屏幕。
优缺点
使用 React 时,大部分应用逻辑都在客户端执行,并通过 API 调用与服务端交互,以获取或保存数据。因此,几乎所有 UI 都由客户端生成。整个 Web 应用会在首次请求时加载。用户点击链接进行导航时,服务端不会收到新的页面渲染请求。客户端代码会运行并更改视图或数据。
CSR 让我们能够构建支持无刷新导航的单页应用,并提供出色的用户体验。由于更改视图时处理的数据有限,页面之间的路由通常更快,因此 CSR 应用看起来响应更灵敏。CSR 还使开发者能够清晰地分离客户端和服务端代码。
尽管 CSR 提供了出色的交互体验,但也存在一些弊端:
-
SEO 方面的考量: 大多数网络爬虫都能以直接明了的方式解析服务端渲染的网站。客户端渲染的情况则稍微复杂一些,因为较大的负载和一连串的网络请求(例如 API 响应)可能导致有意义的内容未能及时渲染,供爬虫索引。爬虫可能能够理解 JavaScript,但也有其局限性。因此,需要采取一些变通方案,让客户端渲染的网站更利于 SEO。
-
性能: 使用客户端渲染时,由于无需往返服务端,交互期间的响应时间会大幅缩短。不过,浏览器首次在客户端渲染内容时,必须先等待 JavaScript 加载并开始处理。因此,用户在初次加载页面前会经历一些延迟。随着 JS 包体积增大,和/或客户端处理能力不足,这可能会影响用户体验。
-
代码可维护性: 某些代码可能会以不同语言在客户端和服务端(API)重复编写。在其他情况下,可能无法清晰地分离业务逻辑。例如,货币和日期字段的校验与格式化逻辑就可能存在这种情况。
-
数据获取: 使用客户端渲染时,数据获取通常由事件触发。页面初次加载时可能没有任何数据。之后可以在页面加载或点击按钮等事件发生时,通过 API 调用获取数据。根据数据量大小,这可能会增加应用的加载或交互耗时。
这些考量对不同应用的重要性可能有所不同。开发者通常希望找到利于 SEO 的解决方案,在不牺牲交互响应时间的前提下更快地提供页面。
提升 CSR 性能
由于 CSR 的性能与 JavaScript 包的大小成反比,因此我们能做的最佳选择就是以优化性能为目标来组织 JavaScript 代码。以下是一些有帮助的建议:
-
为 JavaScript 制定预算:确保初始页面加载的 JavaScript 预算足够合理且严格。初始包压缩并经 gzip 处理后小于 100-170KB,是一个不错的起点。之后,可以在需要相关功能时按需加载代码。
-
预加载:这种技术可用于在页面生命周期的早期,预加载页面所需的关键资源。关键资源可能包括 JavaScript,可通过在 HTML 的
<head>部分加入以下指令来预加载。
<link rel="preload" as="script" href="critical.js" />
这会告知浏览器在页面渲染机制启动前开始加载 critical.js 文件。这样,脚本就能更早准备就绪,且不会阻塞页面渲染机制,从而提升性能。
-
延迟加载:使用延迟加载时,可以识别非关键资源,并仅在需要时加载。通过这种方式,可以减少初始加载的资源大小,从而缩短初始页面加载时间。例如,聊天组件通常不需要在页面加载时立即显示,因此可以延迟加载。
-
代码拆分:为避免 JavaScript 代码包过大,可以开始拆分代码包。Webpack 等打包工具支持代码拆分,可用于创建多个能在运行时动态加载的代码包。代码拆分还能让你延迟加载 JavaScript 资源。
-
使用服务工作线程缓存应用程序外壳:这种技术会缓存应用程序外壳,也就是驱动用户界面的最精简 HTML、CSS 和 JavaScript。服务工作线程可用于离线缓存应用程序外壳。这有助于提供原生单页应用体验,其余内容则会在需要时逐步加载。
通过这些技术,CSR 可以帮助提升单页应用的体验速度,并获得不错的 FCP 和 TTI。