跳至主要内容

Redux 常见问题解答:性能

目录

性能

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

虽然没有一个明确的答案,但在大多数情况下,这两种情况下都不应该成为问题。

Redux 的工作通常分为几个方面:在中间件和 reducer 中处理动作(包括为了不可变更新而进行对象复制)、在动作分发后通知订阅者,以及根据状态变化更新 UI 组件。虽然在足够复杂的情况下,这些方面中的每一个都可能成为性能问题,但 Redux 的实现本身并没有什么固有的缓慢或低效之处。事实上,特别是 React Redux 经过了大量优化,以减少不必要的重新渲染,而 React-Redux v5 比早期版本显示出明显的改进。

与其他库相比,Redux 在开箱即用时可能效率不高。为了在 React 应用程序中获得最佳渲染性能,状态应存储在规范化的形状中,许多单个组件应连接到存储,而不是仅连接几个组件,并且连接的列表组件应将项目 ID 传递给其连接的子列表项(允许列表项通过 ID 查找它们自己的数据)。这将最大程度地减少需要执行的整体渲染量。使用记忆化选择器函数也是一个重要的性能考虑因素。

关于架构,轶事证据表明 Redux 适用于各种项目和团队规模。Redux 目前被数百家公司和数千名开发人员使用,每月从 NPM 获得数十万次安装。一位开发人员报告说

为了规模,我们有大约 500 种动作类型,大约 400 种 reducer 案例,大约 150 个组件,5 个中间件,大约 200 个动作,大约 2300 个测试

更多信息

文档

文章

讨论

每次操作都调用“所有 reducer”会很慢吗?

需要注意的是,Redux 存储实际上只有一个 reducer 函数。存储将当前状态和分派的 action 传递给该 reducer 函数,并让 reducer 适当地处理。

显然,尝试在一个函数中处理所有可能的 action 扩展性不佳,仅仅从函数大小和可读性来看,将实际工作拆分成可以由顶层 reducer 调用的单独函数是有意义的。特别是,常见的建议模式是使用一个单独的子 reducer 函数,负责管理特定键处特定状态切片的更新。Redux 附带的 combineReducers() 是实现此目的的众多方法之一。强烈建议尽可能保持存储状态扁平化和规范化。最终,您负责以任何您想要的方式组织 reducer 逻辑。

但是,即使您碰巧组合了多个不同的 reducer 函数,即使状态深度嵌套,reducer 速度也不太可能成为问题。JavaScript 引擎能够每秒运行大量函数调用,并且大多数 reducer 可能只是使用 switch 语句,并在大多数 action 的响应中默认返回现有状态。

如果您确实担心 reducer 性能,可以使用 redux-ignorereduxr-scoped-reducer 等工具来确保只有某些 reducer 监听特定 action。您还可以使用 redux-log-slow-reducers 进行一些性能基准测试。

更多信息

讨论

我是否必须在 reducer 中深度克隆我的状态?复制我的状态不会很慢吗?

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

此外,深度克隆状态会为每个字段创建新的引用。由于 React-Redux 的 connect 函数依赖于引用比较来确定数据是否已更改,这意味着即使其他数据没有发生实质性更改,UI 组件也会被迫不必要地重新渲染。

但是,您确实需要为受影响的每个嵌套级别创建一个复制并更新的对象。虽然这应该不会特别昂贵,但这也是您应该尽可能保持状态规范化和浅层化的另一个很好的理由。

常见的 Redux 误解:您需要深度克隆状态。现实:如果内部的东西没有改变,就保持它的引用不变!

更多信息

文档

讨论

如何减少存储更新事件的数量?

Redux 在每次成功分派的 action 之后通知订阅者(即 action 抵达存储并由 reducer 处理)。在某些情况下,减少订阅者被调用的次数可能很有用,特别是如果 action 创建者连续分派多个不同的 action。

有几种插件以不同的方式添加批处理功能,例如:redux-batched-actions(一个高阶 reducer,它允许你将多个 action 分派为一个 action,并在 reducer 中“解包”它们),redux-batched-subscribe(一个 store 增强器,它允许你为多个分派操作去抖动订阅者调用),或 redux-batch(一个 store 增强器,它处理使用单个订阅者通知分派 action 数组)。

特别是对于 React-Redux,从 React-Redux v7 开始,一个新的 batch 公共 API 可用于帮助在分派 React 事件处理程序之外的 action 时最小化 React 重新渲染次数。它包装了 React 的 unstable_batchedUpdate() API,允许事件循环滴答声中的任何 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 应用程序可用的内存是有限的。缓存数据会导致性能问题,当缓存的大小接近可用内存量时。当缓存的数据特别大或会话特别长时间运行时,这往往是一个问题。虽然应该意识到这些问题的可能性,但这种意识不应该阻止你有效地缓存合理数量的数据。

以下是一些有效缓存远程数据的方案

首先,只缓存用户需要的那么多数据。如果你的应用程序显示一个分页的记录列表,你并不需要缓存整个集合。相反,缓存对用户可见的内容,并在用户需要更多数据时(或即将需要更多数据时)添加到缓存中。

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

第三,只缓存记录的单个副本。当记录包含其他记录的副本时,这一点尤其重要。为每个记录缓存一个唯一的副本,并将每个嵌套副本替换为一个引用。这被称为规范化。规范化是存储关系数据的首选方法,原因有几个,包括高效的内存消耗。

更多信息

讨论