很多 React 开发者学习 React 的方式是:

记忆 API。

知道:

  • useState
  • useEffect
  • useMemo
  • useTransition
  • Server Component
  • Suspense

然后在不同场景寻找“最佳实践”。

但是随着 React 进入 Server Components、Concurrent Rendering、Streaming、Hydration 时代,这种方式越来越不够。

因为 React 已经不只是一个 UI Library。

它更像一个:

运行在复杂环境中的 UI Runtime。

它需要同时面对:

  • Server Rendering
  • Browser Runtime
  • Concurrent Updates
  • Async Data
  • Multiple Rendering Environments
  • External Systems
  • Browser Rendering Pipeline

所以真正高级的 React 能力,不是知道更多 API。

而是:

能够从 React Runtime 的约束中推导正确的设计。

这篇文章尝试总结我理解 React 的五个核心 Law。

但是今天,React 工程能力的分水岭正在发生变化。

很多机械性的工作已经可以被 Agent 辅助完成:

  • 重构 useEffect
  • 添加 memo/cache
  • 优化 import
  • 调整 Client Boundary
  • 修复明显的 hydration 问题

这些任务本质上属于:

Pattern Matching。

看到某种代码模式,然后应用已有规则。

但是复杂工程问题并不是:

“这段代码有没有最佳实践?”

而是:

当 Runtime 改变时,这个组件是否仍然正确?

React Component 需要面对:

  • Server Rendering
  • Hydration
  • Concurrent Rendering
  • Suspense Retry
  • Portal
  • iframe
  • Multiple Instances

真正优秀的组件,不是只在当前页面正常工作。

而是在未知 Runtime 条件下,依然保持正确。


React 的本质:Description 到 Reality

首先需要建立一个基本模型。

React 并不是直接操作屏幕。

它经历:

User Interaction



JavaScript Runtime



React Runtime



Fiber Tree



Render



Commit



DOM



Browser Rendering Pipeline



Pixels

React 负责的是:

描述 UI 应该是什么。

Browser 负责的是:

将这个描述变成真实世界。

因此 React 永远存在两个世界:

React Description



Browser Reality

所有高级问题,本质都围绕:

这两个世界是否保持一致。

更深层来看,React 解决的问题不是让 UI 静态不变。

而是在不断变化的系统中保持确定性。

State 会变化。

Environment 会变化。

Update Priority 会变化。

Async Boundary 会变化。

但是对于用户可观察的行为:

React 必须保持 deterministic。

这也是 React Runtime 的核心:

在动态系统中维护稳定结果。


Law 1 — Environment Ownership

这段代码到底属于 Server 还是 Client?

现代 React 最大的变化之一:

UI 不再只存在于浏览器。

现在一个 React 应用可能同时运行:

  • Server
  • Browser
  • Worker
  • iframe
  • popup window

所以第一个问题永远应该是:

这个资源属于哪个 Environment?

例如:

window
document
localStorage
navigator

它们不是全局变量。

它们属于:

Browser Environment

而:

Database
File System
Server Secret

属于:

Server Environment

错误:

function ThemeProvider() {
  const theme = localStorage.getItem('theme')

  return <div>{theme}</div>
}

问题不是:

“SSR 不能使用 localStorage。”

更底层的问题:

Server 访问了一个 Client-owned resource。

这是 Environment Ownership violation。


React Server Components 本质也是 Environment Boundary。

Server Component



Server Runtime


Client Component



Browser Runtime

"use client" 并不是简单表示:

“这个组件会在浏览器执行。”

它实际上定义:

一个模块进入 Client Environment。

更准确地说:

"use client" 定义的是一个 Environment Boundary。

它决定:

哪些代码属于 Client Runtime。

哪些代码只能存在 Server Runtime。

哪些数据可以穿越边界。

哪些能力必须被隔离。

React Server Components 的核心并不是:

“Server Component 更快。”

而是:

通过构建阶段建立 Environment Ownership。

让 Server 和 Client 成为两个不同的执行世界。


Law 2 — Description vs Reality

React 描述的 UI 和浏览器真实 UI 是否一致?

React render 阶段产生:

Description

Commit 阶段修改:

Reality

理想状态:

Description === Reality

但是很多问题来自:

Description != Reality

例如:

  • Hydration mismatch
  • UI Flash
  • Unexpected mutation
  • Concurrent rendering bugs

SSR:

Server Render



HTML



Browser Reality



Hydration

Hydration 的目标:

让 React 的描述重新接管浏览器现实。


为什么下面代码可能产生闪烁?

function ThemeProvider() {
  const [theme, setTheme] = useState('light')

  useEffect(() => {
    setTheme(localStorage.getItem('theme'))
  }, [])

  return <div className={theme} />
}

因为:

Paint



Effect



Mutation

用户已经看到旧 Reality。

React 后续才修改。

于是:

Initial Description



Painted Reality

这个 Law 也解释:

为什么 render 必须纯。

因为 React 允许:

  • interrupt
  • restart
  • abandon

如果 render 修改现实:

React 无法保证 Description 和 Reality 的一致性。

Render 阶段产生的是:

Potential Reality。

而不是:

Actual Reality。

React 可以无限重新计算 Description。

但是无法回滚已经发生的外部 Mutation。

所以:

Render 必须保持纯。

Commit 才拥有修改 Reality 的权利。


Law 3 — Fiber Ownership

这个副作用是谁创建的?谁负责清理?

React Component 不只是 UI。

它也是一个资源 owner。

这里最重要的概念不是 cleanup。

而是 Ownership。

React 只能管理它拥有的资源。

如果一个 Effect 修改了 Fiber 外部的世界:

那么必须明确:

谁创建?

谁负责维护?

谁负责恢复?

否则这个状态就逃离了 React 的 ownership graph。

一个 Fiber 可以拥有:

Fiber

 ├── State

 ├── DOM reference

 ├── Effects

 ├── Subscription

 └── External Resources

当 Fiber 创建资源:

Acquire

当 Fiber 消失:

Release

生命周期必须闭合。


例如:

useEffect(() => {
  window.addEventListener('resize', handler)

  return () => {
    window.removeEventListener('resize', handler)
  }
}, [])

完整:

create



use



destroy

所有 external side effect 都应该有 inverse。

例如:

addEventListener



removeEventListener
subscribe



unsubscribe
setInterval



clearInterval
WebSocket



close

更深层的问题:

Global Scope 不代表 Ownership。

例如:

window.addEventListener(...)

很多 bug 不是因为没有 cleanup。

而是 ownership 错误。

例如:

global window 并不一定是组件真正运行的 window。

当组件进入:

  • iframe
  • popup window
  • multiple document

环境时:

global window

owner window

真正的资源关系应该从组件所在 DOM Context 推导:


Fiber



DOM Node



ownerDocument



defaultView

普通页面没问题。

但是:

  • iframe
  • popup
  • multiple windows

可能错误。

正确的 ownership chain:

Fiber



DOM Node



ownerDocument



defaultView

Law 4 — Lane

这个更新到底是什么优先级?

React Concurrent Rendering 的核心:

不是同时执行很多事情。

而是:

React 可以控制工作什么时候发生。

React 将:

What



When

分离。


Update:

What should change?

Lane:

When should it happen?

Scheduler:

How should work execute?

在 React 内部,Lane 并不是简单的优先级数字。

它是一个 bitmask priority system。

每个 update 会携带 lane。

React 会将这些 lane 从 Fiber 向上汇总到 root。

然后决定:

  • 哪些 subtree 有 pending work
  • 哪些任务可以跳过
  • 哪些更新应该优先提交

Concurrent Rendering 的能力,本质来自:

React 可以控制不同 work 的时间顺序。


例如:

输入:

Urgent

搜索结果:

Transition

后台刷新:

Low Priority

startTransition 并不是:

“让代码更快。”

而是:

告诉 React 这个更新的重要程度。


Concurrent Rendering 的核心:

Render 可以:

  • pause
  • resume
  • restart
  • discard

但是:

Commit 必须保持一致。

因此:

Current Tree



Work In Progress Tree



Commit

永远保持:

用户看到的是稳定世界。


Law 5 — Browser Cost

到底是 React 慢,还是浏览器本身慢?

很多性能优化失败,是因为优化错了目标。

React Cost:

Render

Reconciliation

Scheduler

Commit

Browser Cost:

JavaScript

Style Calculation

Layout

Paint

Composite

Memory

例如:

React render

5ms


Layout

30ms


Paint

20ms

优化:

useMemo()

可能:

5ms → 2ms

但是用户没有任何感觉。

因为真正成本不是 React。

React Commit 并不是渲染结束。

它只是进入 Browser Physics。

完整链路:

React Commit



DOM Mutation



Style Calculation



Layout



Paint



Composite



Pixels

很多性能问题,并不发生在 React Runtime。

而发生在浏览器 Rendering Pipeline。


所以性能优化原则:

Optimize the dominant cost.

先找到最大成本。

再决定优化 React 还是 Browser。


五个 Law 的统一模型

最终可以把 React 的复杂性压缩成五个问题。

Where?

Environment Ownership

这段代码运行在哪里?

Server?

Client?

iframe?

popup?

What?

Description vs Reality

React 描述和浏览器现实是否一致?

Who?

Fiber Ownership

谁创建?

谁拥有?

谁销毁?

When?

Lane

这个工作什么时候执行?

Cost?

Browser Cost

真正消耗在哪里?


React Audit Checklist

以后设计一个 React Component,可以这样审查:

Environment

  • 是否访问 browser-only API?
  • 是否跨 Server / Client boundary?
  • 是否支持 iframe / popup?

Description

  • SSR 后 hydration 是否一致?
  • 是否存在 paint 后 mutation?

Ownership

  • 每个 effect 是否有 cleanup?
  • 多实例是否安全?
  • 生命周期结束资源是否释放?

Scheduling

  • 这个更新是否应该 transition?
  • 是否会阻塞 urgent interaction?

Browser Cost

  • 真正成本在哪里?
  • 是否触发 layout?
  • 是否产生 long task?

结语:大道至简

React 看起来越来越复杂。

Fiber。

Scheduler。

RSC。

Suspense。

Hydration。

Concurrency。

但是这些复杂性背后,其实都是为了维护几个简单的不变量。

最终可以压缩成:

Environment

Description

Ownership

Scheduling

Cost

真正理解 React,不是知道更多 API。

而是:

当面对一个未知问题时,可以从 Runtime 的约束推导答案。

这就是 Thinking in React。

复杂系统的最高境界,不是把它变简单。

而是理解足够深之后,能够用简单的模型驾驭复杂。