跳至主内容

性能

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

Redux 常见问题:性能篇

Redux 在性能和架构方面的“扩展性”如何?

这个问题没有单一确定的答案,但在大多数情况下,这两方面通常都不会成为问题。

Redux 的工作主要涉及几个方面:在中间件和 reducer 中处理 action(包括为不可变更新进行对象复制)、在 action 分发后通知订阅者、以及根据状态变化更新 UI 组件。虽然在足够复杂的场景中,这些环节都可能成为性能瓶颈,但 Redux 的实现本身并没有固有的低效问题。事实上,特别是 React Redux 经过深度优化以减少不必要的重新渲染,React-Redux v5 相比早期版本有显著改进。

与其他库相比,Redux 在开箱即用时可能不够高效。要在 React 应用中实现最佳渲染性能,状态应以规范化形式存储,应让更多独立组件直接连接到 store(而非仅连接少量组件),列表组件应向子列表项传递项目 ID(允许列表项通过 ID 自行查找数据)。这些做法能最大限度减少整体渲染量。使用记忆化的选择器函数也是重要的性能优化手段。

在架构方面,实际经验表明 Redux 能适应不同规模和团队体量的项目。目前有数百家公司和数千名开发者使用 Redux,每月通过 NPM 安装量达数十万次。一位开发者报告称:

从规模看,我们有约 500 种 action 类型、约 400 个 reducer 分支、约 150 个组件、5 个中间件、约 200 个 action 以及约 2300 个测试用例

扩展阅读

文档

文章

讨论

为每个动作调用“所有reducer”会不会很慢?

需要明确的是,Redux store 实际上只有一个 reducer 函数。store 会将当前状态和已分发的动作传递给这个 reducer 函数,由 reducer 进行适当的处理。

显然,尝试在单个函数中处理所有可能的动作,在函数规模和可读性方面难以扩展。因此将实际工作拆分给顶层 reducer 调用的独立函数是合理的做法。特别推荐的做法是使用独立的子 reducer 函数来管理特定状态片段(对应特定键)的更新。Redux 内置的 combineReducers() 是实现此目的的多种方式之一。强烈建议保持 store 状态尽可能扁平化和规范化。不过最终,如何组织 reducer 逻辑完全由您决定。

然而,即使您组合了多个 reducer 函数甚至存在深度嵌套状态,reducer 速度通常也不会成为问题。JavaScript 引擎每秒能执行大量函数调用,且大多数 reducer 可能只是使用 switch 语句,并在响应多数动作时默认返回现有状态。

若确实担心 reducer 性能,可使用 redux-ignorereduxr-scoped-reducer 等工具,确保仅特定 reducer 监听特定动作。也可使用 redux-log-slow-reducers 进行性能基准测试。

扩展阅读

讨论

必须在 reducer 中深拷贝状态吗?复制状态会不会很慢?

不可变地更新状态通常意味着进行浅拷贝而非深拷贝。浅拷贝比深拷贝快得多,因为需要复制的对象和字段更少,本质上只是移动一些指针的引用关系。

此外,深克隆状态会为每个字段创建新的引用。由于 React-Redux 的 connect 函数依赖引用比较来判断数据是否变化,这将导致即使其他数据未发生实质变化,UI 组件也会被迫进行不必要的重新渲染。

但您确实需要为受影响的每个嵌套层级创建拷贝并更新对象。虽然这不应造成显著开销,但也是建议尽可能保持状态规范化和扁平化的另一个重要原因。

常见 Redux 误解:必须深克隆状态。事实:如果内部内容未改变,保持其引用不变即可!

扩展阅读

文档

讨论

如何减少 store 更新事件的次数?

Redux 会在每次成功派发 action 后(即 action 到达 store 并被 reducer 处理)通知订阅者。某些情况下,减少订阅者调用次数很有必要,特别是当 action 创建者连续派发多个独立 action 时。

可通过多种插件实现批处理能力,例如:redux-batched-actions(高阶 reducer,支持将多个 action 作为单个 action 派发并在 reducer 中解包)、redux-batched-subscribe(store 增强器,支持对多次派发的订阅者调用进行防抖)、或 redux-batch(store 增强器,支持通过单次订阅者通知处理 action 数组)。

特别针对 React-Redux,从 React-Redux v7 开始提供了新的公共 API batch,用于在 React 事件处理程序之外派发 action 时最小化重新渲染次数。该 API 封装了 React 的 unstable_batchedUpdate(),允许将同一事件循环 tick 中的所有 React 更新批处理为单次渲染。React 自身已在事件处理回调中使用此机制。需注意此 API 实际属于 ReactDOM 和 React Native 等渲染器包,而非 React 核心。

由于 React-Redux 需同时兼容 ReactDOM 和 React Native 环境,我们已在构建时从正确的渲染器导入该 API 供内部使用。同时我们将其重命名为 batch() 并公开导出。您可像这样使用它确保在 React 外部派发的多个 action 仅触发单次渲染更新:

import { batch } from 'react-redux'

function myThunk() {
return (dispatch, getState) => {
// should only result in one combined re-render, not two
batch(() => {
dispatch(increment())
dispatch(increment())
})
}
}

扩展阅读

讨论

相关库

使用“单一状态树”会导致内存问题吗?派发大量 action 会占用内存吗?

首先,在原始内存使用方面,Redux 与其他 JavaScript 库没有区别。唯一的不同在于所有对象引用都嵌套在单一树结构中,而不是像 Backbone 那样保存在多个独立的模型实例中。其次,典型的 Redux 应用可能比同等的 Backbone 应用内存使用更少,因为 Redux 鼓励使用普通 JavaScript 对象和数组,而不是创建模型和集合的实例。最后,Redux 每次仅保留一个状态树引用。不再被树引用的对象会像往常一样被垃圾回收。

Redux 本身不存储 action 历史记录。不过 Redux DevTools 会存储 action 以便重放,但这些功能通常仅在开发环境中启用,不会在生产环境中使用。

扩展阅读

文档

讨论

缓存远程数据会导致内存问题吗?

浏览器中运行的 JavaScript 应用可用内存是有限的。当缓存大小接近可用内存量时,缓存数据会导致性能问题。当缓存数据特别大或会话运行时间特别长时,这往往成为问题。虽然意识到这些潜在问题很有必要,但不应因此阻碍你高效缓存合理数量的数据。

以下是高效缓存远程数据的几种方法:

首先,仅缓存用户需要的数据量。如果你的应用显示分页记录列表,不一定需要缓存整个集合。只需缓存用户可见的数据,当用户立即需要(或即将需要)更多数据时再添加到缓存中。

其次,尽可能缓存记录的简化形式。有时记录包含与用户无关的数据。如果应用不依赖这些数据,可以将其从缓存中省略。

第三,每条记录只缓存单一副本。当记录包含其他记录的副本时,这点尤为重要。应为每条记录缓存唯一副本,并用引用替换所有嵌套副本。这种方法称为规范化。规范化是存储关系数据的首选方法,具有多项优势,包括高效的内存使用。

扩展阅读

讨论