React 渲染与重渲染:什么时候会重算,怎么避免白算

「组件为什么又渲染了」「为什么这个列表卡」——这类问题的根源都是没搞清 React 的渲染时机。这篇把「什么时候渲染、什么时候重渲染、重渲染有多贵」讲透。

一、render 与 re-render 是两个概念

阶段触发条件做什么
首次渲染(mount)组件第一次出现在树里执行函数体,生成元素树
重渲染(re-render)state 改变 / props 改变 / context 改变 / 父组件重渲染再执行一次函数体
提交(commit)渲染结果与上次不同更新真实 DOM

关键:重渲染不等于重新渲染 DOM。React 会先 diff,只把真正变化的节点提交到 DOM。所以「重渲染 1000 次」不一定慢,慢的是「每次都要做大量 diff 或大量 DOM 更新」。

二、重渲染的四个触发源

// 1. state 改变
const [n, setN] = useState(0)
setN(1)              // 触发

// 2. props 改变(父组件传新值)
<Child value={newValue} />

// 3. context 改变
const theme = useContext(ThemeContext)   // Provider 的 value 变了就触发

// 4. 父组件重渲染(即使 props 没变!)
function Parent() {
  const [x, setX] = useState(0)
  return <Child />    // Parent 重渲染时,Child 也会重渲染
}

第四条最容易被忽略:父组件重渲染会带动所有子组件重渲染,即使子组件的 props 完全没变。这是「列表卡顿」的最常见原因。

三、memo 能挡住什么,挡不住什么

// React.memo:props 浅比较相等时跳过重渲染
const Child = React.memo(function Child({ value }) {
  return <div>{value}</div>
})

// 但这样写 memo 就失效了:
<Child onClick={() => {}} />       // 每次都是新函数引用

// 必须配 useCallback:
const handler = useCallback(() => {}, [])
<Child onClick={handler} />
memo 能挡memo 挡不住
父组件重渲染带动子组件重渲染(props 没变)自身 state 改变导致的重渲染
props 浅比较相等时跳过props 里嵌套对象变了(浅比较只看引用)
—context 变化导致的重渲染(memo 无效)

四、浅比较的具体规则

// 这些会「相等」(跳过重渲染)
<Child n={1} />                     // 原始值
<Child s="abc" />
<Child obj={sameRef} />             // 同一引用

// 这些会「不等」(重渲染)
<Child obj={{ a: 1 }} />            // 每次新对象
<Child arr={[1, 2]} />              // 每次新数组
<Child fn={() => {}} />             // 每次新函数
<Child obj={{ ...prev, x: 1 }} />   // 新对象,即使内容相同

所以避免无效重渲染的核心是:让传给子组件的非原始值保持引用稳定。

五、context 的重渲染问题

// 问题:Provider value 是对象字面量,每次渲染都是新引用
<ThemeContext.Provider value={{ theme, setTheme }}>
  <App />
</ThemeContext.Provider>
// 结果:任何 context 消费者都重渲染,即使只用了 theme

// 解法 1:把 value 稳定住
const ctx = useMemo(() => ({ theme, setTheme }), [theme])
<ThemeContext.Provider value={ctx}>

// 解法 2:拆成多个 context(按更新频率分)
<ThemeContext.Provider value={theme}>
  <ThemeSetterContext.Provider value={setTheme}>

context 的粒度设计很重要:一个大的 context 里混了「高频变化的」和「低频变化的」值,会导致所有消费者跟着高频重渲染。按更新频率拆开,或用状态库的 selector 订阅。

六、怎么测出哪里在重渲染

// 1. 最简单的:渲染时打日志(带组件名)
function Comp() {
  console.log('render Comp')   // 看它被执行了几次
  return <div />
}

// 2. React DevTools Profiler
//    Profiler 标签 → 录制 → 看每个组件的渲染耗时与次数

// 3. why-did-you-render(开发期跑)
// npm i @welldone-software/why-did-you-render
import whyDidYouRender from '@welldone-software/why-did-you-render'
whyDidYouRender(React, { trackAllPureComponents: true })

七、性能问题的判断顺序

现象先查常见根因
整个页面卡Profiler 里有哪个组件耗时最长大列表未虚拟化 / 昂贵计算未 memo
列表滚动卡每行是否都在重渲染key 用 index / 行组件未 memo
输入框打字卡onChange 是否触发全局重渲染受控组件带动大范围更新
只有某个组件慢它自身计算量useMemo 缺失或依赖过宽
内存持续增长组件是否泄漏Effect 清理缺失 / 定时器未清

八、不要过度优化

  • React 的重渲染比你想的便宜:不做 DOM 更新的重渲染通常几毫秒
  • 先测再加 memo:React 官方的建议,滥用 memo 会让代码难读且收益为零
  • 真正的性能问题通常在别处:大列表没虚拟化、同步阻塞主线程、过度请求
  • memo 有成本:每次都要做浅比较,props 多时比较开销可观

总结

  • 重渲染 ≠ 重画 DOM:React 会 diff,只有真实变化才提交
  • 父组件重渲染会带动子组件,这是列表卡顿的头号原因
  • memo 只能挡「props 没变的父级带动」,挡不住自身 state 和 context 变化
  • 非原始值要保引用稳定:对象/数组/函数每次新建都会让 memo 失效
  • 先 Profiler 后优化:没有测量数据的优化都是猜
暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇