原文链接:React Re-Renders
你好,react Andy!
核心问题:所有组件被无差别重渲染
我使用 React 已有六年多时间,发现即便是资深开发者,也常常难以理解 React 的底层运行机制,这很容易导致代码性能低下,进而影响代码质量、运行速度和用户体验。
React 官方文档非常适合入门学习,市面上也有大量面向初学者的书籍、课程和博客,但入门之后该如何深入?怎样才能真正理解其底层原理?如果你的 React 开发经验已有一定积累,基础甚至中级课程早已无法满足需求,而高阶的学习资源却十分稀缺。
这也是我推出本系列文章的初衷,希望能填补这一知识空白。
重渲染简介
理解 React 的重渲染机制,是实现应用性能优化的关键。你需要清楚重渲染的触发条件、重渲染在应用中的传播方式,以及重渲染的执行过程和实际影响。
实际开发中的问题
想象一下这样的场景:你在一家大厂实习,接到的第一个任务是开发一个 React 组件,需要在仪表盘顶部添加一个简单的按钮,点击后能打开设置面板。
初始的仪表盘组件代码如下:
const Dashboard = () => {
// 此处为原有业务代码
return <div className="container">
{/* 按钮需要放在这个区域 */}
<ExpensiveDataGrid />
<AnalyticsWidget />
<ActivityFeed />
</div>;
};
接下来你开始实现需求,这个任务看起来十分简单,相信大家都做过无数次类似的开发:
const Dashboard = () => {
// 新增状态管理
const [isExpanded, setIsExpanded] = useState(false);
// 状态更新时,此处返回的所有内容都会被重渲染
return <div>
{/* 新增触发按钮 */}
<IconButton onClick={() => setIsExpanded(true)}>
显示设置面板
</IconButton>
{/* 新增设置面板 */}
{isExpanded && <SettingsPanel onDismiss={() => setIsExpanded(false)} />}
<ExpensiveDataGrid />
<AnalyticsWidget />
<ActivityFeed />
</div>;
};
你添加了状态来跟踪设置面板的展开 / 收起状态,新增了触发状态变更的按钮,并根据状态来控制面板的渲染。
但运行应用后你发现,点击按钮后,设置面板要等将近一秒钟才会显示,延迟感非常明显。
有经验的 React 开发者可能会立刻给出建议:
你这是让整个应用都重渲染了,给所有组件包一层 React.memo,再用 useCallback 缓存方法,就能避免不必要的重渲染。
这个建议并非毫无道理,但在动手优化前,我们不妨先停下来思考 —— 这个场景下,缓存优化其实并非必需,甚至盲目使用还可能影响性能。
首先,我们来仔细分析一下问题的本质,以及延迟产生的原因。
当点击按钮时,会触发setIsExpanded状态更新函数,将isExpanded从false改为true。而持有这个状态的Dashboard组件,会因此触发自身的重渲染。
在状态更新、组件完成重渲染后,React 需要将新的数据传递给所有依赖的子组件,会自动重渲染该组件渲染出的所有子组件,并沿着组件树自上而下持续传播,直到叶子节点。
如果把一个典型的 React 应用看作树形结构,你会发现:从状态更新的触发节点开始,其下方的所有组件都会被重渲染。
理解重渲染机制是掌握 React 的关键。
这是理解 React 重渲染的关键,需要牢记的一点是:React 永远不会向上触发重渲染。即便状态更新发生在组件树的中间节点,也只有该节点下方的组件会被重渲染。
当组件被React.memo包裹后,React 会中断默认的重渲染流程,先判断组件的属性是否发生变化。如果属性没有任何变更,就会终止重渲染;但只要有任意一个属性被修改,重渲染就会照常执行。
需要注意的是,通过缓存机制有效阻止重渲染是一个复杂的话题,涉及诸多考量因素。想要深入理解的话,建议大家继续学习相关内容(新文章即将推出,也可先阅读元素、作为属性的 Children 与重渲染一文)。
在某些场景下,用React.memo包裹组件确实能避免不必要的重渲染,但这种方式本身也存在不少复杂的细节和使用陷阱,这部分内容会在后续文章中探讨。更高效的解决方案是:将依赖特定状态的组件进行隔离,把状态和相关组件封装到一个独立的小型组件中。这种方式不仅能提升性能,还能让组件结构更清晰。
状态下推解决方案
接下来,我们将设置面板的相关逻辑抽离为独立组件:
const SettingsToggle = () => {
const [isExpanded, setIsExpanded] = useState(false);
return <>
<IconButton onClick={() => setIsExpanded(true)}>
显示设置面板
</IconButton>
{isExpanded && <SettingsPanel onDismiss={() => setIsExpanded(false)} />}
</>;
};
然后在原有的Dashboard组件中引入这个新组件即可:
const Dashboard = () => {
return <div>
{/* 引入新封装的组件 */}
<SettingsToggle />
<ExpensiveDataGrid />
<AnalyticsWidget />
<ActivityFeed />
</div>;
};
经过这样的改造后,点击按钮,设置面板能瞬间弹出。我们仅通过一种简单的组件组合技巧,就解决了这个严重的性能问题!