关于 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 ...
随机推荐
- 基于JavaFX的扫雷游戏实现(五)——设置和自定义控件
它来了它来了,最后一期终于来了.理论上该讲的全都讲完了,只剩下那个拖了好几期的自定义控件和一个比较没有存在感的设置功能没有讲.所以这次就重点介绍它们俩吧. 首先我们快速浏览下设置的实现,上图: ...
- 西门子PS on eMS Standalone《导入FANUC机器人TP程序》
导入TP程序到PDPS中 右键点击左侧项目树的 "程序" --> 点击 "创建TP程序" 打开示教器 --> 点击"SELECT" ...
- log4j2---基于vulhub的log4j2漏洞复现---反弹shell
基于vulhub的log4j2漏洞复现---反弹shell 1.方法一 环境准备: 和我上一篇fastjson1.2.24漏洞复现是一样的环境,方法也差别不大 声明:遵纪守法,仅作学习记录用处,部分描 ...
- Axios向后段请求数据GET POST两种方法的不同之处
GET请求 向后端请求时,通过URL向后端传递参数 axios({ url:'http://127.0.0.1:9000/get-user-list/', type:'json', //GET方法携带 ...
- openpyxl 设置单元格自动换行
解决方案 openpyxl的alignment函数中的参数:wrapText=True,就可以了 from openpyxl.styles import Alignment worksheet.cel ...
- 从 Pulsar Client 的原理到它的监控面板
背景 前段时间业务团队偶尔会碰到一些 Pulsar 使用的问题,比如消息阻塞不消费了.生产者消息发送缓慢等各种问题. 虽然我们有个监控页面可以根据 topic 维度查看他的发送状态,比如速率.流量.消 ...
- [ansible]常用内置模块
前言 ansible内置了很多模块,常用的并不多,可以通过ansible -l命令列出所有模块,使用 ansible-doc module-name 查看指定模块的帮助文档,例如:ansible-do ...
- CentOS7系统初始化个人配置
以下内容为个人最小化安装后的配置步骤 更换yum源为阿里云 yum install -y epel-release lrzsz wget yum-axelget mv /etc/yum.repos.d ...
- 一些不错的VSCode设置和插件
设置 同步设置 我们做的各项设置,不希望再到其他机器的时候还得再重新配置一次.VSCode中我们可以登陆微软账号或者GitHub账号,登陆后我们可以开启同步设置.开启设置同步,根据提示登陆即可. 允许 ...
- 如何爆破js加密后的密码?
如何爆破js加密后的密码? 1.首先burp中安装插件: https://github.com/whwlsfb/BurpCrypto 安装插件完毕后,分析进行js加密的算法. 2.分析加密过程: 找到 ...