我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

本文作者:的卢

引入

在日常开发过程中,我们会使用很多性能优化的 API,比如像使用 memouseMemo优化组件或者值,再比如使用 shouldComponentUpdate减少组件更新频次,懒加载等等,都是一些比较好的性能优化方式,今天我将从组件设计、结构上来谈一下 React 性能优化以及数栈产品内的实践。

如何设计组件会有好的性能?

先看下面一张图:

这是一颗 React 组件树,App 下面有三个子组件,分别是 HeaderContentFooter,在 Content组件下面又分别有 FolderTreeWorkBenchSiderBar三个子组件,现在如果在 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) }} /> */}
</>
);
};

根据上面断点和日志就可以得到下面的结论:

  1. 子孙组件每触发一次更新,React都会重新遍历整颗组件树

input 输入数字,引起 updateNum变更状态后,react-dombeginWorkcurrent由顶层组件依次遍历

  1. React更新时会过滤掉未变化的组件,达到减少更新的组件数的目的

在更新过程中,虽然 React重新遍历了组件树,但 没有打印没有变化的 HeaderFooterFolderTreeSiderBar组件内的日志

  1. 父组件状态变化,会引起子组件更新

WorkBenchChild属于 WorkBench的子组件,虽然 WorkBenchChild没有变化,但仍被重新渲染,打印了输入日志,如果更近一步去断点会发现 WorkBenchChildoldPropsnewProps是不相等的,会触发 updateFunctionComponent更新。

综上我们可以得出一个结论,就是 React自身会有一些性能优化的操作,会尽可能只更新变化的组件,比如 Demo1 中 WorkBenchWorkBenchChildWorkBenchGrandChild组件,而会绕开 不变的 HeaderFooter等组件,那么尽可能的让 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;

看一下这个 demoWorkBench组件有一个 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包裹的组件在 diffoldPropsnewProps仍然不等,进入了 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 满足了 父组件不变的前提,且没有 statepropscontext,那么也能够达到性能优化的结果。

对比

  1. 结果一样,都是对 WorkBenchChild进行了优化,在 WorkBench组件更新时, WorkBenchChildWorkBenchGrandChild没有重新渲染
  2. 出发点不一样,用 memo 性能优化 API 是直接作用到子组件上面,而状态隔离是在父组件上面操作,而受益的是其子组件

结论

  1. 只要结构写的好,性能不会太差
  2. 父组件不变,子组件可能不变

性能优化方向

  1. 找到项目中性能损耗严重的组件(节点)

在业务项目中,找到卡顿、崩溃 的组件(节点)

  1. 在根组件(节点)上使用性能优化 API

在根组件上使用的目的就是避免其祖先组件如果没有做好组件设计会给根组件带来无效的重复渲染,因为上面提到的,父组件更新,子组件也会更新

  1. 在其他节点上使用 状态隔离的方式进行优化

优化祖先组件,避免给子组件造成无效的重复渲染

总结

我们从 组件结构 和 性能优化 API 上介绍了性能优化的两种不同的优化方式,在实际项目使用上,也并非使用某一种优化方式,而是多种优化方式结合着来以达到最好的性能

产品中的部分实践

  1. 将状态隔离到子组件内部,避免引起不必要的更新

    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);

    比如在中后台系统中很多表单型组件 SelectTreeSelectCheckbox,其展示的数据需要通过接口获取,那么此时,如果将获取数据的操作放到父组件,那么每次请求数据不仅会导致需要数据的那个表单项组件更新,同时,其他的表单项也会更新,这就有一定的性能损耗,那么按照上面的例子这样将其状态封装到内部,避免请求数据影响其他组件更新,就可以达到性能优化的目的,一般建议在外层再加上 memo性能优化 API,避免因为外部组件影响内部组件更新。

  2. 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 左右,实测就无卡顿现象,问题解决

参考:

  1. React 性能优化的一切
  2. React 源码解析之 Fiber渲染
  3. 魔术师卡颂

最后

欢迎关注【袋鼠云数栈UED团队】~

袋鼠云数栈UED团队持续为广大开发者分享技术成果,相继参与开源了欢迎star

关于 React 性能优化和数栈产品中的实践的更多相关文章

  1. React性能优化之PureComponent 和 memo使用分析

    前言 关于react性能优化,在react 16这个版本,官方推出fiber,在框架层面优化了react性能上面的问题.由于这个太过于庞大,我们今天围绕子自组件更新策略,从两个及其微小的方面来谈rea ...

  2. react性能优化

    前面的话 本文将详细介绍react性能优化 避免重复渲染 当一个组件的props或者state改变时,React通过比较新返回的元素和之前渲染的元素来决定是否有必要更新实际的DOM.当他们不相等时,R ...

  3. 关于React性能优化

    这几天陆陆续续看了一些关于React性能优化的博客,大部分提到的都是React 15.3新加入的PureComponent ,通过使用这个类来减少React的重复渲染,从而提升页面的性能.使用过Rea ...

  4. React性能优化记录(不定期更新)

    React性能优化记录(不定期更新) 1. 使用PureComponent代替Component 在新建组件的时候需要继承Component会用到以下代码 import React,{Componen ...

  5. React 性能优化 All In One

    React 性能优化 All In One Use CSS Variables instead of React Context https://epicreact.dev/css-variables ...

  6. React 与 Redux 在生产环境中的实践总结

    React 与 Redux 在生产环境中的实践总结 前段时间使用 React 与 Redux 重构了我们360netlab 的 开放数据平台.现将其中一些技术实践经验总结如下: Universal 渲 ...

  7. react 性能优化

    React 最基本的优化方式是使用PureRenderMixin,安装工具 npm i react-addons-pure-render-mixin --save,然后在组件中引用并使用 import ...

  8. React性能优化总结(转)

    原文链接: https://segmentfault.com/a/1190000007811296?utm_source=tuicool&utm_medium=referral 初学者对Rea ...

  9. 推荐收藏系列:一文理解JVM虚拟机(内存、垃圾回收、性能优化)解决面试中遇到问题(图解版)

    欢迎一起学习 <提升能力,涨薪可待篇> <面试知识,工作可待篇 > <实战演练,拒绝996篇 > 欢迎关注我博客 也欢迎关注公 众 号[Ccww笔记],原创技术文章 ...

  10. react性能优化要点

    1.减少render方法的调用 1.1继承React.PureComponent(会自动在内部使用shouldComponentUpdate方法对state或props进行浅比较.)或在继承自Reac ...

随机推荐

  1. 了解O2OA(翱途)开发平台中的VIP应用

    使用O2OA(翱途)开发平台可以非常方便地进行项目的业务需求开发与实施,O2OA(翱途)开发平台并不限制实现的系统类型,所以能实现的系统很多,最终呈现的项目成果也是多样性的,可能是OA系统,可能是人力 ...

  2. 即构✖叮咚课堂:行业第一套AI课堂解决方案是怎么被实现的?

    AI走进教育,是传统教育的一次迭代进化 在教育问题上,我们看到两类话题最容易引发公众讨论:教育公平和个性化教育,"互联网+教育"有可能解决第一类话题,"AI教育" ...

  3. MAUI Blazor如何隐藏滚动条

    MAUI Blazor如何隐藏滚动条 Windows 在Windows上是最简单的,改css就可以了,把下面这段添加到app.css中 ::-webkit-scrollbar { display: n ...

  4. study the docker network of macvlan

    Introduce: 在 Macvlan 出现之前,我们只能为一块以太网卡添加多个 IP 地址,却不能添加多个 MAC 地址,因为 MAC 地址正是通过其全球唯一性来标识一块以太网卡的,即便你使用了创 ...

  5. openpyxl 统一表格样式

    # 统一表格样式 rows = ws.max_row columns = ws.max_column # print(rows) # print(columns) for row in range(1 ...

  6. 为什么list.sort()比Stream().sorted()更快?

    昨天写了一篇文章<小细节,大问题.分享一次代码优化的过程>,里面提到了list.sort()和list.strem().sorted()排序的差异. 说到list sort()排序比str ...

  7. FJOI2022 游记

    2022.3.28 省选延期,延到了4.16 2022.4.11 省选又延期,延到了5.2 FJOI 要回来了!! Day -7 开始停课了 QwQ Day -6 打摆 Day -5 打摆 不行,我不 ...

  8. mysql 命令安装

    1.   mysql  下载安装好压缩文件,下面我们进入正题,少废话. 09:39:112023-08-05 先到 mysql 官方网站下载:https://dev.mysql.com/downloa ...

  9. Typescript - 索引签名

    1 索引签名概述 在 TypeScript 中,索引签名是一种定义对象类型的方式,它允许我们使用字符串或数字作为索引来访问对象的属性. 1.1 索引签名的定义和作用 索引签名通过以下语法进行定义: { ...

  10. 《最新出炉》系列初窥篇-Python+Playwright自动化测试-14-playwright操作iframe-番外篇

    1.简介 通过前边三篇的学习,想必大家已经对iframe有了一定的认识和了解,今天这一篇主要是对iframe的一些特殊情况的介绍和讲解,主要从iframe的定位.监听事件和执行js脚本三个方面进行展开 ...