- 原文地址:Dragging React performance forward
- 原文做者:Alex Reardon
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:hexiang
- 校对者:wznonstop,zephyrJS
头图由 James Padolsey 在 Unsplash 拍摄css
我为 React 写了一个拖放库 react-beautiful-dnd 🎉。Atlassian 建立这个库的目的是为网站上的列表提供一种美观且易于使用的拖放体验。你能够阅读介绍文档: 关于拖放的反思。这个库彻底经过状态驱动 —— 用户的输入致使状态改变,而后更新用户看到的内容。这在概念上容许使用任何输入类型进行拖动,可是太多状态驱动拖动将会致使性能上的缺陷。🦑html
咱们最近发布了 react-beautiful-dnd 的第四个版本 version 4
,其中包含了大规模的性能提高。前端
列表中的数据是基于具备 500 个可拖动卡片的配置,在开发版本中启用仪表的状况下进行记录的,开发版本及启用仪表都会下降运行速度。但与此同时,咱们使用了一台性能卓越的机器用于此次记录。确切的性能提高幅度会取决于数据集的大小,设备性能等。react
您看仔细了,咱们看到有 99% 的性能提高 🤘。因为这个库已经通过了极致的优化,因此这些改进更加使人印象深入。你可在大型列表示例或大型面板示例这两个例子中来感觉性能提高的酸爽 😎。android
在本博客中,我将探讨咱们面临的性能挑战以及咱们如何克服它们以得到如此重要的结果。我将谈论的解决方案很是适合咱们的问题领域。有一些原则和技术将会出现 —— 但具体问题可能会在问题领域有所不一样。ios
我在这篇博客中描述的一些技术至关先进,其中大部分技术最好在 React 库的边界内使用,而不是直接在 React 应用程序中使用。git
咱们都很忙!这里是这个博客的一个很是高度的概述:github
尽量避免 render
调用。 另外之前探索的技术 (第一轮, 第二轮),我在这里有一些新的认识:算法
render
不是改变样式的惟一方法react-beautiful-dnd 的大部分状态管理使用 Redux。这是一个实现细节,库的使用者可使用任何他们喜欢的状态管理工具。本博客中的许多具体内容都针对 Redux 应用程序 —— 然而,有一些技术是通用的。为了可以向不熟悉 Redux 的人解释清楚,下面是一些相关术语的说明:redux
context
中,因此被链接的组件能够被注册去更新。若是你感兴趣,这是一些来自 Dan Abramov 的关于这些概念更详细的信息。
做为通常规则,您应该尽量避免调用组件的 render() 函数,render
调用代价很大,有如下缘由:
render
函数调用的进程很费资源Reconciliation 是 React 构建一颗新树的过程,而后用当前的视图(虚拟 DOM)来进行 调和,根据须要执行实际的 DOM 更新。reconciliation 过程在调用一个 render
后被触发。
render
函数的 processing 和 reconciliation 在规模上是代价很大的。 若是你有 100 个或者 10000 个组件,你可能不但愿每一个组件在每次更新时都协调一个 store
中的共享状态。理想状况下,只有须要更新的组件才会调用它的 render
函数。对于咱们每秒 60 次更新(60 fps)的拖放,这尤为如此。
我在前两篇博客 (第一轮, 第二轮) 中探讨了避免没必要要的 render
调用的技巧,React 文档关于这个问题的叙述也讨论了这个主题。就像全部东西都有一个平衡点同样,若是你太过刻意地避免渲染,你可能会引入大量潜在的冗余记忆检查。 这个话题已经在其余地方讨论过了,因此我不会在这里详细讨论。
除了渲染成本以外,当使用 Redux 时,链接的组件越多,您就须要在每次更新时运行更多的状态查询 (mapStateToProps
) 和记忆检查。我在 round 2 blog 中详细讨论了与 Redux 相关的状态查询,选择器和备忘录。
注意从鼠标下的圆圈出现到被选卡片变绿时的时间差。
当点击一个大列表中的卡片时,须要至关长的时间才能开始拖拽,在 500 个卡片的列表中这是 2.6 s 😢!对于那些指望拖放交互是即时的用户来讲,这是一个糟糕的体验。 让咱们来看看发生了什么,以及咱们用来解决问题的一些技巧。
为了执行拖动,咱们将全部相关组件的尺寸(坐标,大小,边距等)的快照放入到咱们的 state 和拖动的开始处。而后,咱们会在拖动过程当中使用这些信息来计算须要移动的内容。 咱们来看看咱们如何完成这个初始快照:
state
发出请求 request
。request
并查看他们是否须要发布任何内容。shouldPublish
属性。publish
回调来发布维度好的,因此这里有一些痛点:
- 当咱们开始拖动时,咱们在
state
上发起了一个request
。- 关联维度发布组件读取此请求并查看他们是否须要发布任何内容
此时,每一个关联的维度发布者都须要针对 store 执行检查,以查看他们是否须要请求维度。不理想,但并不可怕。让咱们继续
- 若是他们须要发布,他们会在未链接的维度发布者上设置一个
shouldPublish
属性
咱们过去使用 shouldPublish
属性来传递消息给组件来执行一个动做。不幸的是,这样作会有一个反作用,它会致使组件进行 render,从而引起该组件自己及其子组件的调和。当你在众多组件上执行这个操做时,代价昂贵。
- 未链接的维度发布者从 DOM 收集维度并使用
publish
回调来发布维度
事情会变得更糟。首先,咱们会当即从 DOM 读取不少维度,这可能须要一些时间。从那里每一个维度发布者将单独 publish
一个维度。 这些维度会被存储到状态中。这种 state
的变化会触发 store 的订阅,从而致使步骤二中的关联组件状态查询和记忆检查被执行。它还会致使应用程序中的其余链接组件相似地运行冗余检查。所以,每当未链接的维度发布者发布维度时,将致使全部其余链接组件的冗余工做。这是一个 O(n²) 算法 - 更糟!哎。
为了解决这些问题,咱们建立了一个新角色来管理维度收集流程:dimension marshal
(维度元帅)。如下是新的维度发布的工做方式:
拖动工做以前:
dimension marshal
,而后把它放到了 context
中。context
中读取 dimension marshal
,并向 dimension marshal
注册本身。Dimension 发布者再也不直接监听 store。 所以,不存在更多未链接的维度发布者。拖动工做开始:
state
发出 request
。dimension marshal
接收 request
并直接向所需维度发布者请求关键维度(拖动卡片及其容器)以便开始拖动。 这些发布到 store 就能够开始拖动。dimension marshal
将在下一个帧中异步请求全部其余 dimension publishers 的 dimensions。这样作会分割从 DOM 中收集维度的成本,并将维度(下一步)发布到单独的帧中。dimension marshal
执行全部收集维度的批量 publish
。在这一点上,state 是彻底混合的,它只须要三帧。这种方法的其余性能优点:
由于 dimension marshal
知道系统中的全部 ID
和 index
,因此它能够直接请求任何维度 O(1)
。这也使其可以决定如何以及什么时候收集和发布维度。 之前,咱们有一个单独的 shouldPublish
信息,它对一切都当即进行回应。dimension marshal
在调整这部分生命周期的性能方面给了咱们很大的灵活性。若是须要,咱们甚至能够根据设备性能实施不一样的收集算法。
咱们经过如下方式改进了维度收集的性能:
当一个拖动开始的时候,咱们须要应用一些样式到每个 Draggable
(例如 pointer-events: none;
)。为此咱们应用了一个行内样式。为了应用行内样式咱们须要 render
每个 Draggable
。当用户试图开始拖动时,这可能会致使潜在的在 100 个可拖动卡片上调用 render
,这会致使 500 个卡片耗费 350 ms。
那么,咱们将如何去更新这些样式而不会产生 render
?
对于全部 Draggable
组件,咱们如今应用共享数据属性(例如 data-react-beautiful-dnd-draggable
)。data
属性历来没有改变过。 可是,咱们经过咱们在页面 head
建立的共享样式元素动态地更改应用于这些数据属性的样式。
这是一个简单的例子:
// 建立一个新的样式元素
const el = document.createElement('style');
el.type = 'text/css';
// 将它添加到页面的头部
const head = document.querySelector('head');
head.appendChild(el);
// 在未来的某个时刻,咱们能够彻底从新定义样式元素的所有内容
const setStyle = (newStyles) => {
el.innerHTML = newStyles;
};
// 咱们能够在生命周期的某个时间点应用一些样式
setStyle(`
[data-react-beautiful-dnd-drag-handle] {
cursor: grab;
}
`);
// 另外一个时刻能够改变这些样式
setStyle(`
body {
cursor: grabbing;
}
[data-react-beautiful-dnd-drag-handle] {
point-events: none;
}
[data-react-beautiful-dnd-draggable] {
transition: transform 0.2s ease;
}
`);
复制代码
若是你感兴趣,你能够看看咱们怎么实施它的。
在拖拽生命周期的不一样时间点上,咱们从新定义了样式规则自己的内容。 您一般会经过切换 class
来改变元素的样式。 可是,经过使用定义动态样式,咱们能够避免应用新的 class
去 render
任何须要渲染的组件。
咱们使用 data
属性而不是 class
使这个库对于开发者更容易使用,他们不须要合并咱们提供的 class
和他们本身的 class
。
使用这种技术,咱们还可以优化拖放生命周期中的其余阶段。 咱们如今能够更新卡片的样式,而无需 render
它们。
注意:您能够经过建立预置样式规则集,而后更改 body
上的 class
来激活不一样的规则集来实现相似的技术。然而,经过使用咱们的动态方法,咱们能够避免在 body
上添加 class
es。并容许咱们随着时间的推移使用具备不一样值的规则集,而不只仅是固定的。
不要惧怕,data
属性的选择器性能很好,与 render
性能差异很大。
当一个拖动开始时,咱们也在 Draggable
上调用 render
来将 canLift
prop 更新为 false
。这用于防止在拖动生命周期中的特定时间开始新的拖动。咱们须要这个 prop ,由于有一些键盘鼠标的组合输入可让用户在已经拖动一些东西的期间开始另外一些东西的拖动。咱们仍然真的须要这个 canLift
检查 —— 可是咱们怎么作到这一点,而无需在全部的 Draggables
上调用 render
?
咱们没有经过 render
更新每一个 Draggable
的 props 来阻止拖动的发生,而是在 context
中添加了 canLift
函数。该函数可以从 store 中得到当前状态并执行所需的检查。经过这种方式,咱们可以执行相同的检查,但无需更新 Draggable
的 props。
此代码大大简化,但它说明了这种方法:
import React from 'react';
import PropTypes from 'prop-types';
import createStore from './create-store';
class Wrapper extends React.Component {
// 把 canLiftFn 放置在 context 上
static childContextTypes = {
canLiftFn: PropTypes.func.isRequired,
}
getChildContext(): Context {
return {
canLiftFn: this.canLift,
};
}
componentWillMount() {
this.store = createStore();
}
canLift = () => {
// 在这个位置咱们能够进入 store
// 因此咱们能够执行所需的检查
return this.store.getState().canDrag;
}
// ...
}
class DraggableHandle extends React.Component {
static contextTypes = {
canLiftFn: PropTypes.func.isRequired,
}
// 咱们能够用它来检查咱们是否被容许开始拖拽
canStartDrag() {
return this.context.canLiftFn();
}
// ...
}
复制代码
很明显,你只想很是谨慎地作到这一点。可是,咱们发现它是一种很是有用的方法,能够在不更新 props 的状况下向组件提供 store 信息。鉴于此检查是针对用户输入而进行的,而且没有渲染影响,咱们能够避开它。
在拥有 500 个卡片的列表中进行拖动马上就拖动了
经过使用上面介绍的技术,咱们能够将在一个有 500 个可拖动卡片的拖动时间从 2.6 s 拖动到到 15 ms(在一个帧内),这是一个 99% 的减小 😍!。
移动大量卡片时帧速降低。
从一个大列表移动到另外一个列表时,帧速率显著降低。 当有 500 个可拖动卡片时,移入新列表将花费大约 350 ms。
react-beautiful-dnd 的核心设计特征之一是卡片在发生拖拽时会天然地移出其它卡片的方式。可是,当您进入新列表时,您一般能够一次取代大量卡片。 若是您移动到列表的顶部,则需移动下整个列表中的全部内容才能腾出空间。离线的 CSS 变化自己代价不大。然而,与 Draggables
沟通,经过 render
来告诉他们移动出去的方式,对于同时处理大量卡片来讲是很昂贵的。
咱们如今只移动对用户来讲部分可见的东西,而不是移动用户看不到的卡片。 所以彻底不可见的卡片不会移动。这大大减小了咱们在进入大列表时须要作的工做量,由于咱们只须要 render
可见的可拖动卡片。
当检测可见的内容时,咱们须要考虑当前的浏览器视口以及滚动容器(带有本身滚动条的元素)。一旦用户滚动,咱们会根据如今可见的内容更新位移。在用户滚动时,确保这种位移看起来正确,有一些复杂。他们不该该知道咱们没有移动那些看不见的卡片。如下是咱们提出的一些规则,以建立在用户看起来是正确的体验。
所以咱们只移动可见卡片,因此无论当前的列表有多大,从性能的角度看移动都没有问题,由于咱们只移动了用户可见的卡片。
一个来自 react-virtualized 的拥有 10000 卡片的虚拟列表。
避免离屏工做是一项艰巨的任务,您使用的技术将根据您的应用程序而有所不一样。咱们但愿避免在拖放交互过程当中移动和动画显示不可见的已挂载元素。这与避免彻底使用诸如 react-virtualized 之类的某种虚拟化解决方案渲染离屏组件彻底不一样。虚拟化是使人惊奇的,可是增长了代码库的复杂性。它也打破了一些原生的浏览器功能,如打印和查找(command / control + f
)。咱们的决定是为 React 应用程序提供卓越的性能,即便它们不使用虚拟化列表。这使得添加美观,高性能的拖放操做变得很是简单,并且只需不多的开销便可将其拖放到现有的应用程序中。也就是说,咱们也计划支持 supporting virtualised lists - 所以开发者能够选择是否要使用虚拟化列表减小大型列表 render
时间。 若是您有包含 1000 个卡片的列表,这将很是有用。
当用户拖动 Droppable
列表时,咱们经过更新 isDraggingOver
属性让用户知道。可是,这样作会致使 Droppable
的 render
- 这反过来会致使其全部子项 render
- 多是 100 个 Draggable
卡片!
为了不这种状况,咱们针对 react-beautiful-dnd 的使用者,建立了性能优化的建议建议文档,以免渲染不须要渲染的 Droppable
的子元素。库自己并不控制 Droppable
的子元素的渲染,因此咱们能作的最好的是提供一个建议的优化。 这个建议容许用户在拖拽时设置 Droppable
,同时避免在其全部子项上调用 render
。
import React, { Component } from 'react';
class Student extends Component<{ student: Person }> {
render() {
// 渲染一个可拖动的元素
}
}
class InnerList extends Component<{ students: Person[] }> {
// 若是子列表没有改变就不要从新渲染
shouldComponentUpdate(nextProps: Props) {
if(this.props.students === nextProps.students) {
return false;
}
return true;
}
// 你也不能够作你本身的 shouldComponentUpdate 检查,
// 只能继承自 React.PureComponent
render() {
return this.props.students.map((student: Person) => (
<Student student={student} />
))
}
}
class Students extends Component {
render() {
return (
<Droppable droppableId="list">
{(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => (
<div
ref={provided.innerRef}
style={{ backgroundColor: provided.isDragging ? 'green' : 'lightblue' }}
>
<InnerList students={this.props.students} />
{provided.placeholder}
</div>
)}
</Droppable>
)
}
}
复制代码
在大的列表之间的平滑移动。
经过实施这些优化,咱们能够减小在包含 500 个卡片的列表之间移动的时间,这些卡片的位移时间从 380 ms 减小到 8 ms 每帧!这是另外一个 99% 的减小。
这种优化并非针对 React 的 - 但在处理有序列表时很是有用
在 react-beautiful-dnd 中咱们常用数组去存储有序的数据。可是,咱们也但愿快速查找此数据以检索条目,或查看条目是否存在。一般你须要作一个 array.prototype.find
或相似的方法来从列表中获取条目。 若是这样的操做过于频繁,对于庞大的数组来讲可能会是场灾难。
有不少技术和工具来解决这个问题(包括 normalizr)。一种经常使用的方法是将数据存储在一个 Object
映射中,并有一个 id
数组来维护顺序。若是您须要按期查看列表中的值,这是一个很是棒的优化,而且能够加快速度。
咱们作了一些不一样的事情。咱们用 memoize-one
(只记住最新参数的记忆函数) 去建立懒 Object
映射来进行实时地按需查找。这个想法是你建立一个接受 Array
参数并返回一个 Object
映射的函数。若是屡次将相同的数组传递给该函数,则返回以前计算的 Object
映射。 若是数组更改,则从新计算映射。 这使您拥有一张当即查找表,而无需按期从新计算或者须要将其明确存储在 state
中。
const getIdMap = memoizeOne((array) => {
return array.reduce((previous, current) => {
previous[current.id] = array[current];
return previous;
}, {});
});
const foo = { id: 'foo' };
const bar = { id: 'bar' };
// 咱们喜欢的有序结构
const ordered = [ foo, bar ];
// 懒惰地计算出快速查找的映射
const map1 = getMap(ordered);
map1['foo'] === foo; // true
map1['bar'] === bar; // true
map1['baz'] === undefined; // true
const map2 = getMap(ordered);
// 像以前同样返回相同的映射 - 不须要从新计算
const map1 === map2;
复制代码
使用查找表大大加快了拖动动做,咱们在每次更新(系统中的 O(n²)
)时检查每一个链接的 Draggable
组件中是否存在某个卡片。经过使用这种方法,咱们能够根据状态变化计算一个 Object
映射,并让链接的 Draggable
组件使用共享映射进行 O(1)
查找。
我但愿你发现这个博客颇有用,能够考虑一些能够应用于本身的库和应用程序的优化。看看 react-beautiful-dnd,也能够试着玩一下咱们的示例。
感谢 Jared Crowe 和 Sean Curtis 提供优化帮助,Daniel Kerris,Jared Crowe,Marcin Szczepanski,Jed Watson,Cameron Fletcher,James Kyle,Ali Chamas 和其余 Atlassian 人将博客放在一块儿。
我在 React Sydney 发表了一篇关于这个博客的主要观点的演讲。
YouTube 视频连接:这儿
在 React Sydney 上优化 React 性能。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。