导读:本期聚焦于阿狸创作的《React重新渲染深度解析:为何children组件会被重复渲染及优化策略》,敬请观看详情。为什么明明props没变,子组件还是跟着父组件一起渲染了?这是不少写React的朋友都遇到过的困惑。这篇文章从React的渲染机制讲起,解释默认情况下父组件更新会递归触发所有children组件重新渲染的原因,分析JSX中内联函数、内联对象如何让props每次都是新引用,进而导致React.memo与shouldComponentUpdate完全失效。文中还会给出useMemo、useCallback、组合组件、children属性传函数等实用优化手段,并对比它们的适用场景和注意事项,帮助你写出渲染次数更少、性能更稳的React应用。

先看一个最常见的场景:一个列表页组件维护着一个输入框的state,用户每敲一个字,整个列表的几十行数据都跟着重新渲染,页面开始明显卡顿。你翻遍代码,发现列表的props明明没有任何变化,为什么children还是渲染了?要回答这个问题,得先回到React的渲染机制本身。

React重新渲染深度解析:为何children组件会被重复渲染及优化策略

一、React渲染机制:父组件更新必然带动children

React中一个容易被忽视的事实是:组件函数被重新调用,并不代表DOM被更新。渲染指的是React调用你的函数组件获取新的虚拟DOM树,之后通过协调算法对比新旧树,最终只把必要的变更提交到真实DOM。所以children组件被重复调用,很多时候并不直接等于性能问题,React本身就是在多调用几次函数这个前提下设计的。

但关键在于默认行为:当父组件重新渲染时,无论props是否变化,React默认会递归渲染整棵子树。也就是说,只要父组件的函数体执行了一次,写在JSX里的所有子组件函数都会跟着执行一遍。这是因为React默认采用宁可多渲染、不能漏更新的保守策略,它不假设你的组件是纯函数,也就无法自动跳过任何一次渲染。

function Parent() {
  const [count, setCount] = useState(0);
  console.log('Parent 渲染');
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>加一</button>
      <Child />
    </div>
  );
}

// Child 没有接收任何 props,但每次点击按钮它都会打印日志
function Child() {
  console.log('Child 渲染');
  return <div>我是子组件</div>;
}

上面的代码里,点击按钮后控制台会同时输出两条日志。这就是children被重复渲染的根源:不是props变了,而是父组件重新执行,子组件作为父组件渲染结果的一部分被顺带重新调用了。理解这一点之后,优化才有明确的方向:要么阻断这条默认的递归渲染链路,要么减少父组件自身不必要的渲染。

二、React.memo失效的常见元凶:内联函数与内联对象

既然默认会递归渲染,那用React.memo包一层是不是就解决了?原理上是的:memo会对props做浅比较,只有props变化时才重新渲染。但在实际项目中,很多人加了memo之后发现毫无效果,问题几乎都出在props的引用相等性上。

JSX中直接写的内联函数和内联对象,每次父组件渲染时都会创建全新的引用。对浅比较来说,引用不同就等于值不同,memo的比较直接失效。下面是一个典型反面案例:

function Parent() {
  const [count, setCount] = useState(0);
  // 每次渲染都创建新对象,浅比较必然失败
  const style = { color: 'red' };
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>加一</button>
      <MemoChild onClick={() => console.log('点击')} style={style} />
    </div>
  );
}

const MemoChild = React.memo(function Child({ onClick, style }) {
  console.log('Child 渲染');
  return <div>子组件</div>;
});

这段代码里memo完全不起作用:onClick是箭头函数内联写的,每次父组件渲染都产生新函数;style是渲染函数体内新建的对象字面量。两者在浅比较时都不等于上一次的值,于是memo每次都放行渲染。

修复手段是配合useCallbackuseMemo把引用稳定下来:

function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => {
    console.log('点击');
  }, []);
  const style = useMemo(() => ({ color: 'red' }), []);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>加一</button>
      <MemoChild onClick={handleClick} style={style} />
    </div>
  );
}

这样处理之后,memo的浅比较每次都能通过,子组件就真正跳过了渲染。需要提醒的是,如果回调依赖了变化的值,就要正确设置依赖数组,否则会拿到过期的闭包值,这是useCallback最常见的坑。

三、children作为属性:天然稳定的优化利器

除了memo,还有一个更优雅的思路:利用children的位置关系阻断渲染传播。当子组件内容以children的形式传入时,只要该内容的结构没有重新生成,就能有效控制渲染范围。

举个实际例子。假设页面有一个频繁更新的数据面板,旁边有一个不常变化的侧边栏。如果写成数据面板包裹侧边栏,侧边栏必然跟着渲染。换个思路,把布局组件提到外层,让易变内容和稳定内容各自独立:

const Layout = React.memo(function Layout({ children }) {
  console.log('Layout 渲染');
  return <div className="layout">{children}</div>;
});

function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>加一</button>
      <Layout>
        <SlowSidebar />
      </Layout>
      <Counter value={count} />
    </div>
  );
}

React官方文档中提到的把state下放到真正使用它的组件,以及把内容作为children传递这两种模式,本质都是缩短渲染的传播链条:state变了,只重新渲染依赖它的那一小块,而不是从顶到底整棵树刷新。当state被限制在底层组件内部时,上层组件不受影响;当易变部分被隔离在特定分支时,稳定分支的children元素引用保持不变,配合memo就能跳过整块子树的渲染。

另外值得一提的是Context。Context的value如果写成内联对象,所有消费该Context的组件都会在每次Provider渲染时重新渲染。给value配合useMemo稳定引用,是Context性能优化的标配操作。

四、优化策略的取舍:不要为了memo而memo

看到这里容易产生一个误区:给所有组件都包上memo、所有函数都套上useCallback。实际上memo本身有比较成本,useCallback和useMemo也有内存与依赖维护成本。如果组件本身很轻,渲染一次的开销还不如浅比较的开销大,那么这些优化反而是负收益。

比较务实的做法是:先用React DevTools的Profiler或者why-did-you-render这类工具定位真正的渲染热点,确认某个组件渲染耗时可观、且渲染来源是父组件传播,再针对性地加memo和引用稳定化。状态设计上优先遵循state就近原则,让数据变化的影响范围天然收窄,很多时候根本不需要走到memo这一步。

最后总结几个要点:父组件渲染默认会递归带动所有children,这是机制不是bug;内联函数和内联对象会让memo失效,需要useCallback和useMemo配合;children模式和状态下放可以从结构上阻断渲染传播;优化前先用工具测量,把力气花在真正昂贵的那几个组件上。理解了渲染传播的原理,React的性能问题就不再神秘。

React重新渲染children组件渲染React性能优化修改时间:2026-09-06 21:42:53

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260906/51814.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。