关于 React 性能优化和数栈产品中的实践
我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。
本文作者:的卢
引入
在日常开发过程中,我们会使用很多性能优化的 API,比如像使用 memo、useMemo优化组件或者值,再比如使用 shouldComponentUpdate减少组件更新频次,懒加载等等,都是一些比较好的性能优化方式,今天我将从组件设计、结构上来谈一下 React 性能优化以及数栈产品内的实践。
如何设计组件会有好的性能?
先看下面一张图:

这是一颗 React 组件树,App 下面有三个子组件,分别是 Header、Content、Footer,在 Content组件下面又分别有 FolderTree、WorkBench、SiderBar三个子组件,现在如果在 WorkBench 中触发一次更新,那么 React 会遍历哪些组件呢?Demo1

function FolderTree() {
console.log('render FolderTree');
return <p>folderTree</p>;
}
function SiderBar() {
console.log('render siderBar');
return <p>i'm SiderBar</p>;
}
export const WorkBenchGrandChild = () => {
console.log('render WorkBenchGrandChild');
return <p>i'm WorkBenchGrandChild</p>
};
export const WorkBenchChild = () => {
console.log('render WorkBenchChild');
return (
<>
<p>i'm WorkBenchChild</p>
<WorkBenchGrandChild />
</>
);
};
function WorkBench() {
const [num, setNum] = useState<number>(1);
console.log('render WorkBench');
return (
<>
<input
value={num}
onChange={(e) => {
setNum(+e.target.value || 0);
}}
/>
<p>num is {num}</p>
<WorkBenchChild />
</>
);
}
function Content() {
console.log('render content');
return (
<>
<FolderTree />
<WorkBench />
<SiderBar />
</>
);
};
function Footer() {
console.log('render footer');
return <p>i'm Footer</p>
};
function Header() {
console.log('render header');
return <p>i'm Header</p>;
}
// Demo1
function App() {
// const [, setStr] = useState<string>();
return (
<>
<Header />
<Content />
<Footer />
{/* <input onChange={(e) => { setStr(e.target.value) }} /> */}
</>
);
};

根据上面断点和日志就可以得到下面的结论:
- 子孙组件每触发一次更新,
React都会重新遍历整颗组件树
当 input 输入数字,引起 updateNum变更状态后,react-dom中 beginWork的 current由顶层组件依次遍历
React更新时会过滤掉未变化的组件,达到减少更新的组件数的目的
在更新过程中,虽然 React重新遍历了组件树,但 没有打印没有变化的 Header、Footer、FolderTree、SiderBar组件内的日志
- 父组件状态变化,会引起子组件更新
WorkBenchChild属于 WorkBench的子组件,虽然 WorkBenchChild没有变化,但仍被重新渲染,打印了输入日志,如果更近一步去断点会发现 WorkBenchChild的 oldProps 和 newProps是不相等的,会触发 updateFunctionComponent更新。
综上我们可以得出一个结论,就是 React自身会有一些性能优化的操作,会尽可能只更新变化的组件,比如 Demo1 中 WorkBench、WorkBenchChild、WorkBenchGrandChild组件,而会绕开 不变的 Header、Footer等组件,那么尽可能的让 React更新的粒度就是性能优化的方向,既然尽可能只更新变化的组件,那么如何定义组件是否变化?
如何定义组件是否变化?
React是以数据驱动视图的单向数据流,核心也就是数据,那么什么会影响数据,以及数据的承载方式,有以下几点:
- props
- state
- context
- 父组件不变!
父组件与当前组件其实没有关联性,放到这里是因为,上面的例子中 WorkBenchChild组件中没有 state、props、context,理论上来说就不变,实际上却重新 render 了,因为 其父组件 WorkBench有状态的变动,所以这里也提了一下,在不使用性能优化 API 的前提下,只要保证 props、state、context & 其父组件不变,那么组件就不变
还是回到刚刚的例子 Demo WorkBench
export const WorkBenchGrandChild = () => {
console.log('render WorkBenchGrandChild');
return <p>i'm WorkBenchGrandChild</p>
};
export const WorkBenchChild = () => {
console.log('render WorkBenchChild');
return (
<>
<p>i'm WorkBenchChild</p>
<WorkBenchGrandChild />
</>
);
};
function WorkBench() {
const [num, setNum] = useState<number>(1);
console.log('render WorkBench');
return (
<>
<input
value={num}
onChange={(e) => {
setNum(+e.target.value || 0);
}}
/>
<p>num is {num}</p>
<WorkBenchChild />
</>
);
}
export default WorkBench;
看一下这个 demo,WorkBench组件有一个 num状态,还有一个 WorkBenchChild的子组件,没有状态,纯渲染组件,同时 WorkBenchChild组件也有一个 纯渲染组件 WorkBenchGrandChild子组件,当输入 input改变 num的值时,WorkBenchChild组件 和 WorkBenchGrandChild组件都重新渲染。我们来分析一下在 WorkBench 组件中,它的子组件 WorkBenchChild 自始至终其实都没有变化,有变化的其实是 WorkBench 中的 状态,但是就是因为 WorkBench 中的 状态发生了变化,导致了其子组件也一并更新,这就带来了一定的性能损耗,找到了问题,那么就需要解决问题。
如何优化?
使用性能优化 API
export const WorkBenchGrandChild = () => {
console.log('render WorkBenchGrandChild');
return <p>i'm WorkBenchGrandChild</p>
};
export const WorkBenchChild = React.memo(() => {
console.log('render WorkBenchChild');
return (
<>
<p>i'm WorkBenchChild</p>
<WorkBenchGrandChild />
</>
);
});
// Demo WorkBench
function WorkBench() {
const [num, setNum] = useState<number>(1);
console.log('render WorkBench');
return (
<>
<input
value={num}
onChange={(e) => {
setNum(+e.target.value || 0);
}}
/>
<p>num is {num}</p>
<WorkBenchChild />
</>
);
}
export default WorkBench;


我们可以使用 React.memo()包裹 WorkBenchChild组件,在其 diff的过程中 props改为浅对比的方式达到性能优化的目的,通过断点可以知道 通过 memo包裹的组件在 diff时 oldProps和 newProps仍然不等,进入了 updateSimpleMemoComponent中了,而 updateSimpleMemoComponent 中有个 shallowEqual浅比较方法是结果相等的,因此没有触发更新,而是复用了组件。
状态隔离(将状态隔离到子组件中)
function ExchangeComp() {
const [num, setNum] = useState<number>(1);
console.log('render ExchangeComp');
return (
<>
<input
value={num}
onChange={(e) => {
setNum(+e.target.value || 0);
}}
/>
<p>num is {num}</p>
</>
);
};
// Demo WorkBench
function WorkBench() {
// const [num, setNum] = useState<number>(1);
console.log('render WorkBench');
return (
<>
<ExchangeComp />
<WorkBenchChild />
</>
);
}
export default WorkBench;


上面 Demo1 的结论,父组件更新,会触发子组件更新,就因为 WorkBench状态改变,导致 WorkBenhChild也更新了,这个时候可以手动创造条件,让 WorkBenchChild的父组件也就是 WorkBench组件剥离状态,没有状态改变,这种情况下 WorkBenchChild 满足了 父组件不变的前提,且没有 state、props、context,那么也能够达到性能优化的结果。
对比
- 结果一样,都是对
WorkBenchChild进行了优化,在WorkBench组件更新时,WorkBenchChild、WorkBenchGrandChild没有重新渲染 - 出发点不一样,用
memo性能优化 API 是直接作用到子组件上面,而状态隔离是在父组件上面操作,而受益的是其子组件
结论
- 只要结构写的好,性能不会太差
- 父组件不变,子组件可能不变
性能优化方向
- 找到项目中性能损耗严重的组件(节点)
在业务项目中,找到卡顿、崩溃 的组件(节点)
- 在根组件(节点)上使用性能优化 API
在根组件上使用的目的就是避免其祖先组件如果没有做好组件设计会给根组件带来无效的重复渲染,因为上面提到的,父组件更新,子组件也会更新
- 在其他节点上使用 状态隔离的方式进行优化
优化祖先组件,避免给子组件造成无效的重复渲染
总结
我们从 组件结构 和 性能优化 API 上介绍了性能优化的两种不同的优化方式,在实际项目使用上,也并非使用某一种优化方式,而是多种优化方式结合着来以达到最好的性能
产品中的部分实践
将状态隔离到子组件内部,避免引起不必要的更新
import React, { useCallback, useEffect, useState } from 'react';
import { connect } from 'react-redux';
import type { SelectProps } from 'antd';
import { Select } from 'antd'; import { fetchBranchApi } from '@/api/project/optionsConfig'; const BranchSelect = (props: SelectProps) => {
const [list, setList] = useState<string[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const { projectId, project, tenantId, ...otherProps } = props;
const init = useCallback(async () => {
try {
setLoading(true);
const { code, data } = await fetchBranchApi(params);
if (code !== 1) return;
setList(data);
} catch (err) {
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
init();
}, [init]); return (
<Select
showSearch
optionFilterProp="children"
filterOption={(input, { label }) => {
return ((label as string) ?? '')
?.toLowerCase?.()
.includes?.(input?.toLowerCase?.());
}}
options={list?.map((value) => ({ label: value, value }))}
loading={loading}
placeholder="请选择代码分支"
{...otherProps}
/>
);
}; export default React.memo(BranchSelect);
比如在中后台系统中很多表单型组件
Select、TreeSelect、Checkbox,其展示的数据需要通过接口获取,那么此时,如果将获取数据的操作放到父组件,那么每次请求数据不仅会导致需要数据的那个表单项组件更新,同时,其他的表单项也会更新,这就有一定的性能损耗,那么按照上面的例子这样将其状态封装到内部,避免请求数据影响其他组件更新,就可以达到性能优化的目的,一般建议在外层再加上memo性能优化 API,避免因为外部组件影响内部组件更新。Canvas render & Svg render

// 画一个小十字
export function createPlus(
point: { x: number; y: number },
{ radius, lineWidth, fill }: { radius: number; lineWidth: number; fill: string }
) {
// 竖 横
const colWidth = point.x - (1 / 2) * lineWidth;
const colHeight = point.y - (1 / 2) * lineWidth - radius;
const colTop = 2 * radius + lineWidth;
const colBottom = colHeight;
const rowWidth = point.x - (1 / 2) * lineWidth - radius;
const rowHeight = point.y - (1 / 2) * lineWidth;
const rowRight = 2 * radius + lineWidth;
const rowLeft = rowWidth;
return `
<path d="M${colWidth} ${colHeight}h${lineWidth}v${colTop}h-${lineWidth}V${colBottom}z" fill="${fill}"></path>
<path d="M${rowWidth} ${rowHeight}h${rowRight}v${lineWidth}H${rowLeft}v-${lineWidth}z" fill="${fill}"></path>
`;
} renderPlusSvg = throttle(() => {
const plusBackground = document.getElementById(`plusBackground_${this.randomKey}`);
const { scrollTop, scrollLeft, clientHeight, clientWidth } = this._container || {};
const minWidth = scrollLeft;
const maxWidth = minWidth + clientWidth;
const minHeight = scrollTop;
const maxHeight = minHeight + clientHeight;
const stepping = 30;
const radius = 3;
const fillColor = '#EBECF0';
const lineWidth = 1;
let innerHtml = '';
try {
// 根据滚动情况拿到容器的四个坐标点, 只渲染当前滚动容器内的十字,实时渲染
for (let x = minWidth; x < maxWidth; x += stepping) {
for (let y = minHeight; y < maxHeight; y += stepping) {
// 画十字
innerHtml += createPlus({ x, y }, { radius, fill: fillColor, lineWidth });
}
}
plusBackground.innerHTML = innerHtml;
} catch (e) {}
});
问题源于在大数据情况下,由 canvas 渲染的 小十字背景渲染失败,经测试,业务数据在 200条左右 canvas 画布绘制宽度就已经达到了 70000px,需要渲染的小十字 数量级在 10w 左右,canvas 不适合绘制尺寸过大的场景(超过某个阀值就会出现渲染失败,具体阀值跟浏览器有关系),而 svg 不适合绘制数量过多的场景,目前的业务场景却是 画布尺寸大,绘制元素多,后面的解决方式就是 采用 svg 渲染,将 画布渲染出来,同时监听容器的滚动事件,同时只渲染滚动容器中可视区域内的背景,实时渲染,渲染数量在 100 左右,实测就无卡顿现象,问题解决
参考:
最后
欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈UED团队持续为广大开发者分享技术成果,相继参与开源了欢迎star
- 大数据分布式任务调度系统——Taier
- 轻量级的 Web IDE UI 框架——Molecule
- 针对大数据领域的 SQL Parser 项目——dt-sql-parser
- 袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices
- 一个速度更快、配置更灵活、使用更简单的模块打包器——ko
关于 React 性能优化和数栈产品中的实践的更多相关文章
- React性能优化之PureComponent 和 memo使用分析
前言 关于react性能优化,在react 16这个版本,官方推出fiber,在框架层面优化了react性能上面的问题.由于这个太过于庞大,我们今天围绕子自组件更新策略,从两个及其微小的方面来谈rea ...
- react性能优化
前面的话 本文将详细介绍react性能优化 避免重复渲染 当一个组件的props或者state改变时,React通过比较新返回的元素和之前渲染的元素来决定是否有必要更新实际的DOM.当他们不相等时,R ...
- 关于React性能优化
这几天陆陆续续看了一些关于React性能优化的博客,大部分提到的都是React 15.3新加入的PureComponent ,通过使用这个类来减少React的重复渲染,从而提升页面的性能.使用过Rea ...
- React性能优化记录(不定期更新)
React性能优化记录(不定期更新) 1. 使用PureComponent代替Component 在新建组件的时候需要继承Component会用到以下代码 import React,{Componen ...
- React 性能优化 All In One
React 性能优化 All In One Use CSS Variables instead of React Context https://epicreact.dev/css-variables ...
- React 与 Redux 在生产环境中的实践总结
React 与 Redux 在生产环境中的实践总结 前段时间使用 React 与 Redux 重构了我们360netlab 的 开放数据平台.现将其中一些技术实践经验总结如下: Universal 渲 ...
- react 性能优化
React 最基本的优化方式是使用PureRenderMixin,安装工具 npm i react-addons-pure-render-mixin --save,然后在组件中引用并使用 import ...
- React性能优化总结(转)
原文链接: https://segmentfault.com/a/1190000007811296?utm_source=tuicool&utm_medium=referral 初学者对Rea ...
- 推荐收藏系列:一文理解JVM虚拟机(内存、垃圾回收、性能优化)解决面试中遇到问题(图解版)
欢迎一起学习 <提升能力,涨薪可待篇> <面试知识,工作可待篇 > <实战演练,拒绝996篇 > 欢迎关注我博客 也欢迎关注公 众 号[Ccww笔记],原创技术文章 ...
- react性能优化要点
1.减少render方法的调用 1.1继承React.PureComponent(会自动在内部使用shouldComponentUpdate方法对state或props进行浅比较.)或在继承自Reac ...
随机推荐
- Linux中常用数据库管理系统之MariaDB
Linux中常用数据库管理系统之MariaDB 我们生活在信息化时代,经常要跟数据打交道,它在我们的日常生活中无处不在,比如手机支付,微信聊天,淘宝购物,使用的这些在后台都会对应一个叫数据库的存在.数 ...
- 音视频开发进阶——YUV与RGB的采样与存储格式
在上一篇文章中,我们带大家了解了视频.图像.像素和色彩之间的关系,还初步认识了两种常用的色彩空间,分别是大家比较熟悉的 RGB,以及更受视频领域青睐的 YUV.今天,我们将继续深入学习 RGB.YUV ...
- dash构建多页应用
dash 构建多页面应用一种方案 本方案对dash官网多页面案例使用dash_bootstrap_components案例进行优化与测试,效果如下 项目代码结构如下 │ app.py │ ├─asse ...
- Linux设置字符编码
一.Linux设置字符编码 1.什么是字符编码 字符编码可以实现对非英文字符的支持,防止非英文字符的乱码. 2.国内常用的字符编码 UTF-8 GBK 3.设置字符编码 我们可以对Linux系统的字符 ...
- Ubuntu Ctrl + Alt + [F1~F6] 图形化终端与命令行终端
在20.04的版本中,F1和F2是两个图形化终端,可以登陆不同的用户.(如果是相同的用户登陆,则进入的是同一个终端.) F4-F6都是命令行终端,即便使用相同的用户登陆,也是打开不同的终端. 说明,命 ...
- 2021-3-9 xml序列化和反序列化
class XmlHelp { #region 调用 /// <summary> /// xml添加 /// </summary> /// <param name=&qu ...
- vs(visual stuiod)中vc++工程的Filter和Folder及vcxproj知识
vs中创建Filter 在一个新项目中右键 - Add - New,默认只有一选项 New Filter. 创建出来的Filter可以理解为是VS的过滤器(虚拟目录),它不会在本地的磁盘上新建目录,而 ...
- 来会会babel这个重要且神奇的工具
babel 在前端工程化开发中发挥着至关重要的作用,它能将较高级的语法转成浏览器可识别的代码,无论中 es6 中 const .promise 还是 React.TypeScript. 以下babel ...
- [db2]缓冲池管理
简介 缓冲池指的是从硬盘读取表和索引数据时,数据库管理器分配的用于高速缓存这些表和索引数据的内存区域.每个数据库都必须具有至少一个缓冲池,创建数据库时会自动创建一个名为IBMDEFAULTBP的缓冲池 ...
- 【Unity3D】反射和折射
1 前言 立方体纹理(Cubemap)和天空盒子(Skybox)中介绍了生成立方体纹理和制作天空盒子的方法,本文将使用立方体纹理进行采样,实现反射.菲涅耳反射和折射效果.另外,本文还使用了 Gra ...