React 并发渲染与 Streaming SSR 深入
并发模式核心机制
1. Time Slicing 时间切片
- Scheduler 将渲染任务切成 5ms 小片,每片执行完检查是否有高优先级任务
- 利用 MessageChannel 而非 setTimeout,避免 4ms 最小延迟
- 有更高优先级任务(如用户输入)则中断当前渲染,先处理紧急任务
2. Lane 优先级模型(React 18)
// 简化版 lane 定义(用二进制位表示优先级)
const SyncLane = 0b0000001 // 最高:同步
const InputContinuousLane = 0b000100 // 用户连续输入(scroll/drag)
const DefaultLane = 0b010000 // 普通更新
const TransitionLane = 0b001000000 // transition 标记的更新
const IdleLane = 0b100000000000000 // 最低:空闲
// 多个优先级合并
const pendingLanes = SyncLane | TransitionLane
// 取最高优先级
const nextLanes = getHighestPriorityLanes(pendingLanes)
3. Automatic Batching(自动批量更新)
// React 17 之前:setTimeout 中不批量
setTimeout(() => {
setCount(c => c + 1) // 触发一次渲染
setFlag(f => !f) // 再触发一次渲染
}, 1000)
// React 18:所有场景自动批量
setTimeout(() => {
setCount(c => c + 1)
setFlag(f => !f)
// 只触发一次渲染!
}, 1000)
// 需要强制非批量时
import { flushSync } from 'react-dom'
flushSync(() => setCount(c => c + 1)) // 立即渲染
4. useTransition 与 useDeferredValue
// useTransition:标记低优先级状态更新
const [isPending, startTransition] = useTransition()
function handleSearch(value: string) {
setInputValue(value) // 高优先级:立即更新输入框
startTransition(() => {
setSearchQuery(value) // 低优先级:可被中断
})
}
// isPending 可用于显示过渡状态
return (
<>
<input value={inputValue} onChange={e => handleSearch(e.target.value)} />
{isPending ? <Spinner /> : <SearchResults query={searchQuery} />}
</>
)
// useDeferredValue:延迟某个值的更新,类似防抖但不固定时间
function SearchPage() {
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
const isStale = query !== deferredQuery
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<Suspense fallback={<Loading />}>
<Results query={deferredQuery} />
</Suspense>
</div>
)
}
5. Streaming SSR
传统 SSR vs Streaming SSR
传统 SSR(All-or-Nothing):
服务端获取所有数据 -> 渲染完整 HTML -> 发送 -> 客户端 Hydration
瓶颈:必须等最慢的数据请求完成才能发送第一个字节
Streaming SSR(React 18):
服务端流式发送 HTML -> 客户端逐步 Hydration
优势:TTFB 更低,用户更快看到内容
// 服务端使用 renderToPipeableStream
import { renderToPipeableStream } from 'react-dom/server'
const { pipe, abort } = renderToPipeableStream(
<App />,
{
bootstrapScripts: ['/static/js/main.js'],
onShellReady() {
// Shell 就绪(不含 Suspense 边界内容)立即发送
response.statusCode = 200
response.setHeader('Content-type', 'text/html')
pipe(response)
},
onError(error) {
response.statusCode = 500
console.error(error)
}
}
)
// 客户端组件使用 Suspense 分割边界
function App() {
return (
<Layout> {/* Shell:立即渲染 */}
<Suspense fallback={<ArticleSkeleton />}>
<Article /> {/* 数据就绪后流式注入 */}
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments /> {/* 独立加载,不阻塞其他 */}
</Suspense>
</Layout>
)
}
6. Selective Hydration(选择性水合)
- 用户与某个还未 hydrate 的组件交互时,React 优先 hydrate 该组件
- 其他组件继续排队,不会阻塞用户交互
- 需要配合
React.lazy+Suspense使用
7. Server Components 数据流
1. 请求到达服务端
2. Server Components 执行(可直接访问 DB)
3. 生成 RSC Payload(特殊序列化格式,非 HTML)
4. Client Components 占位符插入 Payload
5. Payload 流式传输到客户端
6. React 在客户端重建组件树并 Hydrate Client Components
RSC Payload 格式示例:
0:["$","div",null,{"children":["$","$L1","1",{}]}]
1:I{"id":"./ClientComponent","chunks":["client"]}
8. 资深面试题
- useSyncExternalStore 解决了什么问题?
- 并发渲染中,一次渲染可能被中断后重新执行,期间外部 store 可能变化导致 UI 撕裂(tearing)
useSyncExternalStore强制同步读取,确保一次渲染内数据一致
- startTransition 如何实现「可中断」?
- 更新被标记为 TransitionLane(低优先级)
- 执行过程中如有高优先级更新(SyncLane)到来,当前渲染丢弃重新来过
- 因此 transition 内的更新可能执行多次,需保证纯函数
- Streaming SSR 中如何处理 SEO?
- Shell(包含关键内容)立即发送,搜索引擎可读取
- 关键 meta 信息放在 Shell 中,非关键内容放在 Suspense 边界内
- Google 支持 JavaScript 渲染,但 Streaming 方式更友好