如何优化图形化React代码库——优化d3-zoom和dnd-kit代码
Miro和Figma是在疫情期间变得非常流行的在线协作画布工具。你可以在虚拟画布上添加虚拟帖子和其他各种元素,让团队以熟悉的方式进行虚拟协作。
我之前写过一篇文章,展示如何用React和TypeScript创建Figma/Miro克隆版。那篇文章中的代码设计得尽可能易于理解,而本文将对其进行优化。原代码使用DndKit进行拖放操作,使用D3 Zoom进行平移和缩放。包含四个组件(App、Canvas、Draggable和Addable),约250行代码。你不需要阅读原文就能理解本文。
如何测量React应用性能
测量React应用性能有三种常见方法:
- React Dev Tools性能分析器
- Chrome Dev Tools性能分析器,特别是使用自定义轨道
- Profiler组件
这些工具都很棒,但在这种情况下都不太合适。在大多数代码库中,执行JavaScript代码(包括我们的代码和React框架)的时间是主要问题。然而,在所有代码运行完毕且React更新了DOM之后,浏览器仍然有很多工作要做:
在这种情况下,浏览器的布局和渲染时间很显著,而React性能分析没有计入这部分时间。
如何优化画布的平移和缩放
原始Canvas元素使用CSS变换translate3d(x, y, k)来平移和缩放画布。这可以工作,但它不会缩放子元素,因此当缩放级别改变时,画布上的所有卡片都必须使用新的CSS变换重新渲染。
|
|
我将其改为使用translateX(x) translateY(y) scale(k),效果相同但会缩放子元素。这样,当缩放改变时,所有卡片都不会被重新渲染。
如何优化在画布上拖拽卡片
开始拖拽
DndContext的useDraggable钩子在开始拖拽操作时会导致一些重新渲染。
可以通过将Draggable组件改为仅包含此钩子(及其使用的内容),并为其他所有内容创建DraggableInner组件(包装在memo中)来改善这一点。这能有效减少重新渲染,DraggableInner几乎不会被重新渲染,并提高了开始拖拽操作的速度。
更好的选择是创建一个新的NonDraggable组件,它看起来与Draggable组件完全相同,但不与DndContext连接。这些卡片显示在画布上,并具有onMouseEnter事件,用于为活动卡片交换Draggable组件。
|
|
为了进一步优化,我们创建一个AllCards组件,包含画布上所有作为NonDraggable组件的卡片。因为它总是渲染所有卡片,所以几乎不需要重新渲染,并且与memo一起使用。
完成拖拽
完成拖拽操作很难优化,因为卡片的位置发生了变化,这确实需要重新渲染,这意味着AllCards组件也必须重新渲染。
即使使用memo与Draggable组件,在10万张卡片的情况下,结束拖拽操作仍然需要2500ms,主要是由于Draggable组件的复杂性及其与DndKit的集成。
结果
未优化的基础版本在1000到5000张卡片之间开始变慢。标准优化将其提高到约10,000张卡片,而更深入的优化将其提高到约100,000张卡片。代价是代码变得明显更复杂,这使得理解和修改变得更加困难,特别是对于代码库的新手。
| 卡片数量 | 平移(ms) | 缩放(ms) | 开始拖拽(ms) | 结束拖拽(ms) | 卡片悬停(ms) |
|---|---|---|---|---|---|
| 1,000 | 10 | 10 | 7 | 15 | 2 |
| 5,000 | 10 | 10 | 25 | 40 | 2 |
| 10,000 | 25 | 25 | 50 | 50 | 2 |
| 50,000 | 150 | 150 | 150 | 250 | 5 |
| 100,000 | 150 | 250 | 300 | 500 | 15 |
总结
在标准的React应用中显示10万或更多项目是不常见的,但在高度图形化的代码库中,这种情况变得很有可能。
对于这种数量级,浏览器渲染引擎可能会花费大量时间,因此最好使用性能API来测量性能,而不是通常的React工具。
标准的React优化策略确实有效并能改善情况,但需要更进一步,通过找到避免渲染的方法,甚至避免过多的memo比较。