厘清概念
CSR
Client-Side Rendering,客户端渲染。
核心定义:页面主要内容由浏览器中的 JavaScript 执行后渲染出来,数据来源可以是:
- fetch;
- 数据写死在 JS 中;
- 其他类似;
CSR = HTML 的主要 UI 内容依赖浏览器 JS 执行后产生。
bundle
原意:捆、束、一包东西;在前端工程里,可以理解为:经过构建工具处理后,打包给浏览器的一组 JavaScript 代码/资源。
比如:
import React from 'react'
import Button from './Button'
import { formatDate } from './utils'这些代码散落在:
page.tsx
Button.tsx
utils.ts
React
第三方依赖
...构建工具会分析这些模块之间的依赖关系:
源代码模块
↓
dependency graph
↓
编译 / 转换 / 优化 / 拆分
↓
一个或多个 JS 文件最终交付给浏览器的代码,通常就是:JavaScript bundle、client bundle、bundle size等;
Next.js 不会粗暴地把所有东西真的塞进一个 bundle.js;实际上会进行 code splitting/chunking,产生多个 chunk。
因此,bundle 的理解就是:打包后的客户端代码集合。
prerender
pre + render
预先 + 渲染在用户真正访问页面之前,提前把页面渲染出来。是 Web / SSR / SSG 领域的通用概念。
Hydration
hydrate 本身有:给……补充水分。
可以想象 build 得到的 HTML 是一个:干的页面,已经有:标签、文字、结构、内容,但是缺少 React 客户端行为。
浏览器加载 JS 后,React 对已有 HTML 进行 hydration:不是把页面推倒重建,而是在已有 HTML 基础上注入/恢复客户端能力。
hydration 的过程中,客户端第一次 render 的结果确实需要和服务端/预渲染得到的 HTML 对得上。
因此翻译为:水合,是 React SSR/预渲染体系里的核心机制,React 在浏览器中接管已经存在的 HTML,并把它变成可交互的 React 应用。
已有 HTML
<button>0</button>
│
│ 下载 Client JS
↓
React 执行
│
↓
Hydration
│
├── 建立 React 组件树
├── 将 React 与已有 DOM 对应起来
├── 建立事件处理
└── 恢复交互能力
↓
button 可以点击
理解
最直白的理解:声明 Client Component 边界;
- 与 "Client Component" 和 "CSR 纯客户端渲染"分开,不是一回事;
- 表示这个组件需要客户端 JavaScript 能力,不是表示这个组件只能在客户端生成 HTML;
如何理解?
静态导出时,Client Components 也会在 next build 阶段 prerender;最终仍然会产生用于首次访问的静态 HTML
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
)
}这里的 'use client' 本质是在告诉 Next.js:从这个文件开始,建立一个 Server / Client 的边界;这个模块及其客户端依赖需要进入浏览器 JavaScript bundle。
- Next.js 的 page.tsx 和 layout.tsx 默认都是 Server Components。只有当你需要客户端能力时,才应该建立 'use client' 边界。
比如:
page.tsx Server Component
│
├── Article.tsx Server Component
│
└── LikeButton.tsx Client Component
↑
'use client'大致流程如下:
next build
│
├── Server Components 执行
│
├── Client Components 也参与预渲染
│
↓
生成静态 HTML
+
生成 Client Component 对应 JS
↓
浏览器
│
├── 先显示 HTML
└── JS hydration
↓
onClick / state / effect 生效以下情况需要 Client Component:
- useState
- useEffect
- 事件处理,例如 onClick、onChange
- 浏览器 API,例如 window、document、localStorage、Geolocation
- 依赖这些能力的 custom hooks
凡是依赖浏览器运行时状态、生命周期、事件或 Browser API 的 UI,一般需要位于 Client Component 边界之内。
'use client'
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
console.log('browser')
}, [])
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
)
}分成:
BUILD
────────────────
Counter render
↓
<button>0</button>
↓
写入 HTML
BROWSER
────────────────
HTML 立即显示
↓
下载 Counter JS
↓
React hydration
↓
useEffect 执行
↓
onClick 开始工作
↓
useState 可以变化影响 SEO、首屏内容与性能不在 use client,而在:核心内容是不是 build 时就已经进入 HTML
'use client'
function Article() {
return (
<article>
<h1>TypeScript 基础</h1>
<p>这是一篇文章……</p>
</article>
)
}仍然会 prerender 成 HTML,搜索引擎拿到的 HTML 本身就可能包含:
<h1>TypeScript 基础</h1>
<p>这是一篇文章……</p>需要注意的是如下的写法:
'use client'
function Article() {
const [content, setContent] = useState('')
useEffect(() => {
fetch('/article.json')
.then(...)
.then(setContent)
}, [])
return <article>{content}</article>
}build 阶段可能只有:
<article></article>浏览器执行 JS 后才变成:
<article>
很多正文……
</article> Next.js App Router
Component
│
┌───────────┴───────────┐
│ │
Server Component Client Component
默认 'use client'
│ │
build/server 执行 需要 browser JS
│ │
└───────────┬───────────┘
│
prerender
│
HTML
│
浏览器加载
│
Client JS hydration
│
可交互