03 渲染机制、性能优化与现代 React

本章覆盖 React 的高级机制:Render、Commit、Fiber、Reconciliation、并发渲染、Suspense、SSR、Hydration、React Server Components、React Compiler 和 React DOM。

1. 一次更新发生了什么

text 复制代码
触发更新 -> Render 阶段 -> Reconciliation -> Commit 阶段 -> 浏览器绘制

触发更新来源:

  • setState
  • Props 变化
  • Context value 变化
  • 外部 store 通知

组件函数重新执行不等于 DOM 一定变化。React 会比较前后 UI 描述,只提交必要 DOM 变更。

2. Render 阶段

Render 阶段调用组件函数,计算新的 React Element 树。

jsx 复制代码
function Counter({ count }) {
  console.log('render');
  return <span>{count}</span>;
}

Render 必须纯粹,不应该:

  • 修改 DOM。
  • 发请求。
  • 写 localStorage。
  • 启动定时器。
  • 修改外部变量。

这些属于 Effect 或事件。

3. Commit 阶段

Commit 阶段把变化应用到真实 DOM,并调度 Effect。

你看到页面变化,是 Commit 后的结果。

4. Fiber

Fiber 是 React 内部的工作单元。它让 React 可以:

  • 暂停和恢复渲染工作。
  • 为更新分配优先级。
  • 支持并发渲染。
  • 更细粒度地协调 UI 更新。

结论:组件渲染必须纯粹,因为 React 可能多次调用、暂停、丢弃某次渲染结果。

5. Reconciliation

同类型节点复用:

jsx 复制代码
// 从
<button className="primary">保存</button>

// 到
<button className="danger">删除</button>

React 复用 button DOM,更新属性和文本。

不同类型节点替换:

jsx 复制代码
// 从
<button>保存</button>

// 到
<a>保存</a>

React 卸载 button,创建 a。

列表中通过 key 判断身份。

6. 批处理更新

React 会批量处理同一事件中的多个状态更新。

jsx 复制代码
function handleClick() {
  setA(1);
  setB(2);
  setC(3);
}

通常只触发一次渲染。

当依赖旧状态:

jsx 复制代码
setCount((count) => count + 1);

7. memo、useMemo、useCallback

memo

jsx 复制代码
const CourseCard = memo(function CourseCard({ course, onSelect }) {
  return (
    <article onClick={() => onSelect(course.id)}>
      <h3>{course.title}</h3>
    </article>
  );
});

useMemo

jsx 复制代码
const visibleItems = useMemo(
  () => expensiveFilter(items, keyword),
  [items, keyword],
);

useCallback

jsx 复制代码
const handleSelect = useCallback((id) => {
  setSelectedId(id);
}, []);

不要盲目缓存。优化前先定位瓶颈。

8. useTransition

区分紧急更新和非紧急更新。

jsx 复制代码
const [isPending, startTransition] = useTransition();

function onInput(value) {
  setInputValue(value);
  startTransition(() => {
    setSearchKeyword(value);
  });
}

输入框显示是紧急更新,大列表筛选可以是非紧急更新。

9. useDeferredValue

jsx 复制代码
const deferredKeyword = useDeferredValue(keyword);
const results = useMemo(
  () => search(items, deferredKeyword),
  [items, deferredKeyword],
);

适合输入框驱动昂贵列表或图表。

10. 列表虚拟化

jsx 复制代码
function VirtualList({ rows, rowHeight, height }) {
  const [scrollTop, setScrollTop] = useState(0);
  const start = Math.floor(scrollTop / rowHeight);
  const visibleCount = Math.ceil(height / rowHeight);
  const visibleRows = rows.slice(start, start + visibleCount + 5);

  return (
    <div
      style={{ height, overflow: 'auto' }}
      onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}
    >
      <div style={{ height: rows.length * rowHeight, position: 'relative' }}>
        {visibleRows.map((row, index) => (
          <div
            key={row.id}
            style={{
              position: 'absolute',
              top: (start + index) * rowHeight,
              height: rowHeight,
            }}
          >
            {row.title}
          </div>
        ))}
      </div>
    </div>
  );
}

真实项目优先使用 react-windowreact-virtualized 或 TanStack Virtual。

11. 代码分割与 lazy

jsx 复制代码
const SettingsPage = lazy(() => import('./SettingsPage'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <SettingsPage />
    </Suspense>
  );
}

适合低频页面、图表、编辑器、地图、PDF 预览等重型模块。

12. Suspense

jsx 复制代码
<Suspense fallback={<CourseSkeleton />}>
  <CourseContent />
</Suspense>

Suspense 处理等待,Error Boundary 处理失败。

嵌套边界:

jsx 复制代码
<Suspense fallback={<PageShellSkeleton />}>
  <Header />
  <Suspense fallback={<ChartSkeleton />}>
    <Chart />
  </Suspense>
  <Suspense fallback={<TableSkeleton />}>
    <Table />
  </Suspense>
</Suspense>

13. Error Boundary

jsx 复制代码
class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    reportError(error, info);
  }

  render() {
    if (this.state.hasError) {
      return <Fallback onReset={() => this.setState({ hasError: false })} />;
    }

    return this.props.children;
  }
}

错误边界层级:

  • 应用级。
  • 路由级。
  • 模块级。
  • 第三方组件级。

14. Profiler

组件:

jsx 复制代码
<Profiler id="KnowledgeHub" onRender={onRender}>
  <KnowledgeHub />
</Profiler>

性能排查顺序:

  1. 用 React DevTools Profiler 找慢组件。
  2. 检查状态是否放太高。
  3. 检查 key 是否稳定。
  4. 检查大列表和昂贵计算。
  5. 再使用 memo、缓存、虚拟化或架构调整。

15. React DOM

客户端入口:

jsx 复制代码
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'));
root.render(<App />);

卸载:

jsx 复制代码
root.unmount();

Hydration:

jsx 复制代码
hydrateRoot(document.getElementById('root'), <App />);

Portal:

jsx 复制代码
function Modal({ children }) {
  return createPortal(
    <div role="dialog">{children}</div>,
    document.body,
  );
}

flushSync 强制同步提交,低频使用:

jsx 复制代码
flushSync(() => {
  setOpen(true);
});
measureLayout();

16. React DOM 资源预加载

js 复制代码
preconnect('https://cdn.example.com');
prefetchDNS('https://api.example.com');
preload('/font.woff2', { as: 'font' });
preloadModule('/editor.js');
preinit('/analytics.js', { as: 'script' });
preinitModule('/route-module.js');

现代框架通常自动处理,手动调用前确认收益。

17. SSR、CSR、SSG

CSR:

text 复制代码
下载 HTML -> 下载 JS -> 执行 React -> 请求数据 -> 渲染页面

适合后台系统、强交互应用。

SSR:

text 复制代码
服务器请求数据 -> 生成 HTML -> 浏览器看到内容 -> 下载 JS -> hydrate

适合 SEO、首屏性能要求高的页面。

SSG:构建时生成 HTML,适合文档、博客、营销页。

18. Hydration mismatch

常见原因:

  • 服务端和客户端时间不同。
  • 随机数不同。
  • 首屏读取 window
  • 用户区域、语言不同。

错误:

jsx 复制代码
return <p>{Date.now()}</p>;

修正:

jsx 复制代码
function Clock() {
  const [now, setNow] = useState(null);

  useEffect(() => {
    setNow(Date.now());
  }, []);

  return <p>{now ?? '...'}</p>;
}

19. Streaming SSR

Node.js:

jsx 复制代码
const { pipe, abort } = renderToPipeableStream(<App />, {
  bootstrapScripts: ['/main.js'],
  onShellReady() {
    response.setHeader('content-type', 'text/html');
    pipe(response);
  },
  onError(error) {
    console.error(error);
  },
});

Web Streams:

jsx 复制代码
const stream = await renderToReadableStream(<App />, {
  bootstrapScripts: ['/main.js'],
});

return new Response(stream, {
  headers: { 'content-type': 'text/html' },
});

20. React Server Components

Server Component 可以在服务端读取数据:

jsx 复制代码
async function Notes() {
  const notes = await db.notes.findMany();

  return (
    <ul>
      {notes.map((note) => (
        <li key={note.id}>{note.title}</li>
      ))}
    </ul>
  );
}

它不能使用:

  • useState
  • useEffect
  • 浏览器事件
  • window

Client Component:

jsx 复制代码
'use client';

function Expandable({ children }) {
  const [open, setOpen] = useState(false);

  return (
    <section>
      <button onClick={() => setOpen((value) => !value)}>切换</button>
      {open && children}
    </section>
  );
}

21. 'use client''use server'

'use client' 标记客户端边界:

jsx 复制代码
'use client';

export function LikeButton() {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(true)}>Like</button>;
}

'use server' 标记 Server Function:

jsx 复制代码
export async function deleteCourse(id) {
  'use server';

  const user = await requireUser();
  if (!user.permissions.includes('course:delete')) {
    throw new Error('Forbidden');
  }

  await db.course.delete({ id });
}

Server Function 必须校验身份、权限和参数。

22. 可序列化边界

Server 和 Client 之间可以传:

  • string、number、boolean。
  • plain object。
  • array。
  • Date 等受支持类型。
  • Server Function 引用。

不要传:

  • 普通函数。
  • class 实例。
  • DOM 节点。
  • 任意 React Element 数据。

23. React Compiler

React Compiler 试图自动完成很多 memoization,但要求组件纯粹、代码可分析。

纯组件:

jsx 复制代码
function Component({ count }) {
  return <div>{count}</div>;
}

不纯:

jsx 复制代码
function Component() {
  globalCounter++;
  return <div>{globalCounter}</div>;
}

指令:

jsx 复制代码
'use memo';
'use no memo';

Compiler 不替代架构优化。它解决局部复用,不解决状态放错、大列表无虚拟化、服务端状态混乱。

24. Activity

<Activity> 用于隐藏和恢复子树 UI 及内部状态,适合后台标签页、切换面板等需要保留状态的场景。

概念:

jsx 复制代码
<Activity mode={active ? 'visible' : 'hidden'}>
  <Editor />
</Activity>

使用前确认 React 版本和框架支持。

25. 性能预算

专家不是到处加 memo,而是建立预算:

  • 首屏 JS gzip 不超过目标值。
  • 关键交互在目标时间内响应。
  • 大列表必须分页或虚拟化。
  • CI 检查包体积变化。
  • 性能问题有监控和回归验证。

26. 渲染问题案例库

父组件输入导致整页重渲染:

jsx 复制代码
function Page() {
  const [keyword, setKeyword] = useState('');

  return (
    <>
      <SearchBox value={keyword} onChange={setKeyword} />
      <ExpensiveChart />
      <BigTable keyword={keyword} />
    </>
  );
}

优化思路:

  • 把搜索状态下沉到只影响的区域。
  • 对昂贵但无关的子树使用 memo
  • 大列表使用虚拟化。
  • 输入驱动昂贵筛选时使用 useDeferredValue 或 transition。

Context 导致整树重渲染:

jsx 复制代码
<AppContext.Provider value={{ theme, user, keyword }}>

优化:拆分 Context,高频状态留局部,Provider value 用 useMemo

memo 失效:

jsx 复制代码
<Card options={{ compact: true }} onClick={() => open(id)} />

每次都是新引用。如果 Card 昂贵且 memo 有价值,再稳定引用。

27. Suspense 与 Hydration 专题

Suspense 边界要按用户体验设计。关键内容优先展示,次要内容可渐进加载,fallback 高度尽量稳定,错误边界和 Suspense 成对设计。

Hydration mismatch 常见代码:

jsx 复制代码
function ThemeText() {
  const theme = localStorage.getItem('theme');
  return <p>{theme}</p>;
}

服务端没有 localStorage。修正:

jsx 复制代码
function ThemeText() {
  const [theme, setTheme] = useState('system');

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

  return <p>{theme}</p>;
}

更好的方案是服务端通过 cookie 注入首屏主题。

28. SSR 与缓存策略

SSR 不等于每次请求都全量计算。常见缓存包括 CDN HTML 缓存、API 缓存、数据库查询缓存、静态生成和增量再生成。

判断维度:

  • 数据实时性。
  • 用户个性化。
  • 权限。
  • SEO。
  • 缓存失效路径。
  • 服务端成本。

29. RSC 边界反例

Server Component 没有客户端事件和 state。

jsx 复制代码
// 错误
async function LikeButton() {
  const liked = await getLiked();
  return <button onClick={() => setLiked(true)}>Like</button>;
}

正确拆分:

jsx 复制代码
async function Post() {
  const post = await getPost();
  return <LikeButton initialLiked={post.liked} />;
}

'use client';
function LikeButton({ initialLiked }) {
  const [liked, setLiked] = useState(initialLiked);
  return <button onClick={() => setLiked(!liked)}>Like</button>;
}

30. 性能诊断模板

text 复制代码
问题:哪个交互慢?
用户感知:首屏、输入、滚动、切页还是提交?
证据:Profiler、指标、包体积、日志。
根因:状态范围、大列表、昂贵计算、网络、图片、hydration。
修复:局部状态、memo、deferred、virtualization、code splitting。
验证:优化前后数据对比。

高级问题:

  • Render 和 Commit 的区别是什么?
  • memo 为什么会失效?
  • useTransition 和 debounce 有什么区别?
  • RSC 和 SSR 是什么关系?
  • React Compiler 不能解决哪些性能问题?

31. 性能知识点索引

  1. 重新渲染不等于 DOM 更新。
  2. Render 阶段必须纯。
  3. Commit 阶段才会修改 DOM。
  4. key 影响组件实例复用。
  5. 父组件渲染默认会调用子组件。
  6. memo 只做 Props 浅比较。
  7. useMemo 缓存计算,不保证永久缓存。
  8. useCallback 缓存函数引用,不让函数更快。
  9. Context value 变化会通知消费者。
  10. 大列表优先虚拟化。
  11. 图片和字体影响首屏。
  12. 代码分割减少首包。
  13. Suspense 是等待边界。
  14. Error Boundary 是失败边界。
  15. useTransition 标记非紧急更新。
  16. useDeferredValue 延迟非关键值。
  17. SSR 改善首屏和 SEO,但增加服务端复杂度。
  18. Hydration 要求首屏一致。
  19. RSC 减少客户端 JS,但引入 server/client 边界。
  20. Compiler 要求代码可分析。

32. 性能优化反模式

  • 没有测量就优化。
  • 所有组件都 memo。
  • 所有函数都 useCallback。
  • 复杂计算放渲染主路径。
  • Context 存高频输入。
  • 列表项使用复杂阴影和布局。
  • SSR 页面读取客户端随机值。
  • Suspense fallback 造成布局跳动。
  • RSC 中混入客户端交互。
  • 忽略包体积。

33. 专家定位流程

text 复制代码
1. 明确慢的交互。
2. 复现并记录环境。
3. Profiler 定位慢组件。
4. 网络面板排除接口问题。
5. Bundle analyzer 排除包体积问题。
6. 检查状态范围和 Context。
7. 检查列表和图片。
8. 做最小修复。
9. 用同一指标复测。
10. 写下结论和防回退措施。

面试题完整答案总集:渲染、性能与现代 React

Render 和 Commit 的区别是什么?

Render 阶段是 React 调用组件函数,计算新的 UI 描述。这个阶段必须纯粹,可能被中断、重复或丢弃。Commit 阶段是 React 把变化应用到真实 DOM,并执行布局相关逻辑和 Effect 调度。组件重新 render 不等于 DOM 一定更新。

为什么 key 不能用 index?

index 表示当前位置,不表示业务身份。当列表插入、删除或排序时,同一个 index 会对应不同数据,React 会错误复用组件实例,导致输入状态、动画状态、展开状态错位。只有完全静态、不会变化的列表才可勉强使用 index。

memo 为什么会失效?

memo 默认浅比较 Props。如果传入对象、数组或函数每次渲染都是新引用,即使内容相同也会被认为变化。例如 options={``{ compact: true }}onClick={() => open(id)}。需要稳定引用,或者接受重新渲染,因为不是所有组件都值得优化。

useTransition 和 debounce 有什么区别?

debounce 是延迟触发操作,常用于减少请求或输入处理次数。useTransition 是告诉 React 某些状态更新不是紧急的,可以让更紧急的输入响应先更新。debounce 改变事件触发时机,transition 改变渲染调度优先级。

Suspense fallback 应该放在哪里?

fallback 应放在用户体验可接受的等待边界。页面骨架适合包住整个页面,局部慢模块适合局部 Suspense。边界太大容易让整个页面变成 loading,边界太碎会增加复杂度。Suspense 应与 Error Boundary 配合。

Hydration mismatch 如何定位?

先找服务端和客户端首屏渲染不一致的来源:时间、随机数、浏览器 API、localStorage、用户语言、权限、窗口尺寸。修复方式是让首屏确定性一致,或把客户端专属逻辑放到 Effect 中,或通过 cookie / server data 注入首屏信息。

RSC 和 SSR 是什么关系?

SSR 是把组件在服务端渲染成 HTML,主要解决首屏和 SEO。RSC 是一种组件模型,让部分组件只在服务端执行,不进入客户端 bundle,并通过 RSC payload 与客户端组件组合。二者可以一起使用,但不是同一件事。

React Compiler 不能解决哪些性能问题?

React Compiler 可以自动化部分 memoization,但不能解决状态放错位置、大列表无虚拟化、图片过大、网络慢、服务端状态混乱、组件边界糟糕、业务模型缺失等问题。它要求代码纯粹可分析,架构问题仍需要人工设计。

useDeferredValue 适合什么场景?

适合某个值变化频繁,但使用它计算出的 UI 不必立刻同步的场景,如搜索输入驱动大列表筛选。输入框可以立即响应,列表使用 deferred value 稍后更新,从而降低输入卡顿。

性能优化的正确顺序是什么?

先确认用户感知问题,再用 Profiler、性能指标或日志定位瓶颈;然后判断是状态范围、大列表、昂贵计算、网络、包体积、图片还是 hydration;最后做最小修复,并用同一指标验证。不要没有证据就到处加 memo。

相关推荐
ChalesXavier4 小时前
Fetch API 的基本用法
javascript
是上好佳佳佳呀4 小时前
【前端(十三)】JavaScript 数组与字符串笔记
前端·javascript·笔记
yanchGod5 小时前
CSS page-break-before 属性详解:控制打印分页的艺术
前端·javascript·css·html·css3·html5
卜凡.5 小时前
Vue是对HTML、CSS、JS的标准化、组件化和响应式的上层抽象与增强
javascript·vue.js·html
冰暮流星5 小时前
javascript之默认事件
开发语言·javascript·ecmascript
前端之虎陈随易5 小时前
为什么今天还会有新语言?MoonBit 想解决什么问题?
大数据·linux·javascript·人工智能·算法·microsoft·typescript
kyriewen6 小时前
你等的Babel编译,够喝三杯咖啡了——用Rust重写的SWC,只需眨个眼
前端·javascript·rust
张元清6 小时前
SSR 状态管理陷阱:defineStore vs defineContextStore
前端·javascript·面试