React 的响应速度始于组件树的结构优化。
重新渲染并非问题所在,真正的问题在于广泛的状态传播。应在进行微记忆化处理之前先将数据源下放到叶子节点。
React会按照树形结构的设计要求来运行。当应用显得缓慢时,通常是因为框架正在遵循较长的状态/更新传播路径——而非出现了莫名其妙的故障。
重新渲染并非敌人
渲染是保持UI一致性的方式。真正的敌人是在每次按键时都在大型子树结构中进行不必要的处理。
// ❌ count lives in Parent — expensive siblings re-render on every click
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<ExpensiveComponent />
<AnotherExpensiveComponent />
</div>
);
}
// ✅ count lives in Counter — expensive siblings are untouched
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
function Parent() {
return (
<div>
<Counter />
<ExpensiveComponent />
<AnotherExpensiveComponent />
</div>
);
}
// ❌ SearchLayout owns state — ExpensiveList re-renders on every keystroke
function SearchLayout() {
const [query, setQuery] = useState('');
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<ExpensiveList />
</div>
);
}
/* ✅ StatefulInput receives expensive content as children
— its reference stays stable */
function StatefulInput({ children }) {
const [query, setQuery] = useState('');
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
{children}
</div>
);
}
function SearchLayout() {
return (
<StatefulInput>
<ExpensiveList />
</StatefulInput>
);
}
传播应在何处停止
将数据源置于合适位置
应将状态放在负责读写它的组件附近。表单的草稿文本无需存储在应用根目录中。
阻断传播路径
那些接收稳定属性的子组件可以避免参与不必要的处理。组合式架构以及将子组件作为插槽使用的方式,往往比全局缓存更有效。
树形结构作为设计决策
架构设计是一种选择:功能文件夹、容器与视图层的分离,以及本地提供者能够限制更新带来的影响范围。
从数据结构入手
在着手进行微观优化之前,先规划好哪些状态应放在何处,以及在频繁更新时哪些子树需要保持稳定。大多数“React运行缓慢”的问题其实都源于数据结构的问题。
先进行性能分析,再重新组织代码;最后才考虑使用记忆化技术。
上下文提供者也是数据结构的一部分:如果每次渲染都会改变全局上下文,就会使叶子节点的记忆化失效。应根据更新频率来划分不同的上下文。
路由级别的代码分割可以减少首次渲染时所需处理的数据量;结合本地状态使用,能让导航更加轻量。
列表虚拟化就像是数据结构的“手术”——直接移除屏幕外的行,而无需处理成千上万个处于休眠状态的节点。
文档所有权:哪个团队负责管理哪个子树状态。共享的全局存储只是以另一种形式重现了广泛传播的问题。
使用 React Profiler 来记录状态变更前后的提交时间,并将差异保存在 PR 中以便审查。
服务端组件与客户端岛会改变数据树的存储位置,但客户端岛上仍需遵循传播规则。
上下文提供者也是数据树的一部分:每次渲染都会变化的超级上下文会使叶子节点的缓存失效,应按照更新频率来拆分上下文。
路由级别的代码分割可以缩小首次渲染时所需处理的数据树规模,结合本地状态使用可让导航更加轻量。
列表虚拟化相当于对数据树进行“手术”——直接卸载屏幕外的行,而非去处理成千上万个处于休眠状态的节点。
文档所有权:哪个团队负责管理哪个子树状态。共享的全局存储只是以另一种形式重现了广泛传播的问题。
使用 React Profiler 来记录状态变更前后的提交时间,并将差异保存在 PR 中以便审查。
服务端组件与客户端岛会改变数据树的存储位置,但客户端岛上仍需遵循传播规则。
上下文提供者也是数据树的一部分:每次渲染都会变化的超级上下文会失效叶子节点的缓存效果。应根据更新频率来拆分上下文。
路由级别的代码分割可以缩小首次渲染时所需处理的数据树规模;结合本地状态使用可让导航更加轻量。
列表虚拟化相当于对数据树进行“手术”——直接卸载屏幕外的行,而非去处理成千上万个处于休眠状态的节点。
文档所有权:哪个团队负责管理哪个子树状态。共享的全局存储只是以另一种形式重现了广泛传播的问题。
使用 React Profiler 来记录状态变更前后的提交时间,并将差异保存在 PR 中以便审查。
服务端组件与客户端岛会改变数据树的存储位置,但客户端岛上仍需遵循传播规则。
上下文提供者也是数据树的一部分:每次渲染都会变化的超级上下文会使叶子节点的缓存失效,应按照更新频率来拆分上下文。
路由级别的代码分割可以缩小首次渲染时所需处理的数据树规模,结合本地状态使用可让导航更加轻量。
列表虚拟化相当于对数据树进行“手术”——直接卸载屏幕外的行,而非去处理成千上万个处于休眠状态的节点。
文档所有权:哪个团队负责管理哪个子树状态。共享的全局存储只是以另一种形式重现了广泛传播的问题。
使用 React Profiler 来记录状态变更前后的提交时间,并将差异保存在 PR 中以便审查。
服务端组件与客户端岛会改变数据树的存储位置,但客户端岛上仍需遵循传播规则。
上下文提供者也是数据树的一部分:每次渲染都会变化的超级上下文会使叶子节点的缓存失效,应按照更新频率来拆分上下文。
路由级别的代码分割可以缩小首次渲染时所需处理的数据树规模,结合本地状态使用可让导航更加轻量。
列表虚拟化相当于对数据树进行“手术”——直接卸载屏幕外的行,而非去处理成千上万个处于休眠状态的节点。
文档所有权:哪个团队负责管理哪个子树状态。共享的全局存储只是以另一种形式重现了广泛传播的问题。
使用 React Profiler 来记录状态变更前后的提交时间,并将差异保存在 PR 中以便审查。
服务端组件与客户端岛会改变数据树的存储位置,但客户端岛上仍需遵循传播规则。
上下文提供者也是数据树的一部分:每次渲染都会变化的超级上下文会使叶子节点的缓存失效,应按照更新频率来拆分上下文。
路由级别的代码分割可以缩小首次渲染时所需处理的数据树规模,结合本地状态使用可让导航更加轻量。
列表虚拟化相当于对数据树进行“手术”——直接卸载屏幕外的行,而非去处理成千上万个处于休眠状态的节点。
文档所有权:哪个团队负责管理哪个子树状态。共享的全局存储只是以另一种形式重现了广泛传播的问题。
使用 React Profiler 来记录状态变更前后的提交时间,并将差异保存在 PR 中以便审查。
服务端组件与客户端岛会改变数据树的存储位置,但客户端岛上仍需遵循传播规则。
上下文提供者也是数据树的一部分:每次渲染都会变化的超级上下文会使叶子节点的缓存失效,应按照更新频率来拆分上下文。
路由级别的代码分割可以缩小首次渲染时所需处理的数据树规模,结合本地状态使用可让导航更加轻量。
列表虚拟化相当于对数据树进行“手术”——直接卸载屏幕外的行,而非去处理成千上万个处于休眠状态的节点。
文档所有权:哪个团队负责管理哪个子树状态。共享的全局存储只是以另一种形式重现了广泛传播的问题。
使用 React Profiler 来记录状态变更前后的提交时间,并将差异保存在 PR 中以便审查。
服务端组件与客户端岛会改变数据树的存储位置,但客户端岛上仍需遵循传播规则。
上下文提供者也是数据树的一部分:每次渲染都会变化的超级上下文会使叶子节点的缓存失效,应按照更新频率来拆分上下文。
路由级别的代码分割可以缩小首次渲染时所需处理的数据树规模,结合本地状态使用可让导航更加轻量。
列表虚拟化相当于对数据树进行“手术”——直接卸载屏幕外的行,而非去处理成千上万个处于休眠状态的节点。
文档所有权:哪个团队负责管理哪个子树状态。共享的全局存储只是以另一种形式重现了广泛传播的问题。
使用 React Profiler 来记录状态变更前后的提交时间,并将差异保存在 PR 中以便审查。
服务端组件与客户端岛会改变数据树的存储位置,但客户端岛上仍需遵循传播规则。
上下文提供者是数据树的一部分:每次渲染都会变化的超级上下文会使叶子节点的缓存失效。应根据更新频率来拆分上下文。
路由级别的代码分割可以缩小首次渲染时所需处理的数据树规模;结合本地状态使用可让导航更加轻量。
那些在每次按键时都会更新的提供者,应仅包裹那些确实需要该更新频率的组件——而非整个页面结构。
派生数据应存储在选择器或已缓存的子元素中,而非反映到兄弟节点的状态中,以免引发更多渲染。
将数据结构可视化:用方框表示状态拥有者,用箭头表示属性。从根节点向外延伸的宽分支是典型的结构特征。
并发功能虽能提升效率,但无法解决那些掌控着复杂表单中所有字段的“巨型组件”问题。
迁移策略:每次提交代码前后的性能分析后,逐步将一个状态节点向叶子节点移动。
那些在每次按键时都会更新的提供者,应仅包裹需要该更新频率的消费者,而非整个页面结构。
派生数据应存储在选择器或已缓存的子元素中,而非反映到兄弟节点的状态中,以免引发更多渲染。
将数据结构可视化:用方框表示状态拥有者,用箭头表示属性。从根节点向外延伸的宽分支是典型的结构特征。
并发特性虽能提升处理效率,却无法解决那种掌控着复杂界面所有字段的“上帝组件”问题。
迁移策略:每次提交代码时,使用性能分析工具在修改前后分别检测,逐步将状态节点向叶子层级移动。
那些在每次按键时都会更新的提供者,应仅包裹那些确实需要如此高频更新的消费组件,而非整个页面结构。
派生数据应存储在选择器或已缓存的后代组件中,而非复制到会导致更多渲染操作的兄弟状态节点中。
将数据结构可视化:用方框表示状态的所有者,用箭头表示属性传递关系。从根节点向外延伸的宽大分支通常是问题所在。
并发特性虽能提升处理效率,却无法解决那种掌控着复杂界面所有字段的“上帝组件”问题。
迁移策略:每次提交代码时,使用性能分析工具在修改前后分别检测,逐步将状态节点向叶子层级移动。
那些在每次按键时都进行更新的提供者,应当仅包裹那些需要该更新频率的消费者,而非整个页面结构。
派生数据应存储在选择器或已缓存的子元素中,而非反映到会导致更多渲染的兄弟节点状态中。
将数据结构可视化:为状态拥有者绘制方框,用箭头表示属性传递关系。从根节点向外延伸的宽分支通常是正常现象。