首页 > 其他分享 >关于 React 性能优化和数栈产品中的实践

关于 React 性能优化和数栈产品中的实践

时间:2023-10-24 15:22:16浏览次数:52  
标签:WorkBench const WorkBenchChild render React 组件 return 数栈 优化

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

本文作者:的卢

引入

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

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

先看下面一张图:

file

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

file

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) }} /> */}
    </>
  );
};

file

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

  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;

file

file

我们可以使用 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;

file

file

上面 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

    file

    // 画一个小十字
    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

标签:WorkBench,const,WorkBenchChild,render,React,组件,return,数栈,优化
From: https://www.cnblogs.com/dtux/p/17784890.html

相关文章

  • 数据集与模型的优化策略
    随着人工智能技术的快速发展,神经网络作为其核心组成部分,已经在各个领域取得了显著的成果。而神经网络的性能优劣,往往取决于其训练数据集和训练模型的选择与设计。本文将围绕这一主题,对神经网络训练数据集和神经网络训练模型进行详细阐述。神经网络训练数据集神经网络训练数据集是神......
  • WPF ItemsControl 卡顿 数据量大 虚拟化 优化
    <ItemsControlItemsSource="{BindingMemberInfos}"VirtualizingStackPanel.IsVirtualizing="True"VirtualizingStackPanel.VirtualizationMode="Recycling"VirtualizingPanel.CacheLength="50">......
  • React技术栈支援Vue项目,你需要提前了解的
    写在前面react整体是函数式的思想,把组件设计成纯组件,状态和逻辑通过参数传入,而vue的思想是响应式的,也就是基于是数据可变的,通过对每一个属性建立Watcher来监听,当属性变化的时候,响应式的更新对应的虚拟domreact的思路通过js来生成html,所以设计了jsx,还有通过js来操作css。vue是......
  • MySQL中大量数据优化方案
    目录1大量数据优化1.1引言1.2评估表数据体量1.2.1表容量1.2.2磁盘空间1.2.3实例容量1.3出现问题的原因1.4解决问题1.4.1数据表分区1.4.1.1简介1.4.1.2优缺点1.4.1.2操作1.4.2数据库分表1.4.2.1简介1.4.2.2分库分表方案1.4.2.2.1取模方案1.4.2.2.2range范围方案1......
  • reactor模式
    reactor模式模型:1.三种角色说明reactor:派发器负责监听及分配事件,将事件分配给对应的handleracceptor:请求连接器,处理用户新过来的连接handler:请求处理器,负责事件的处理,将自身于事件绑定2.模型分类单reactor单线程模型单reactor多线程模型主从reactor单线程模型主从reac......
  • 安防视频监控平台EasyCVR查询告警后,无法自动清除记录该如何优化?
    视频监控TSINGSEE青犀视频平台EasyCVR能在复杂的网络环境中,将分散的各类视频资源进行统一汇聚、整合、集中管理,在视频监控播放上,视频安防监控汇聚平台可支持1、4、9、16个画面窗口播放,可同时播放多路视频流,也能支持视频定时轮播。视频监控平台EasyCVR支持多种播放协议,包括:HLS、HTT......
  • 你是如何做好Unity项目性能优化的
    在面试中,我们经常会被问各种”莫名奇妙”的问题, 比如这道:”你是如何做好Unity项目性能优化的?”。“这个问题也太泛了吧,没有具体的优化点,这怎么回答?”瞬间跃入脑海。做面试复盘的时候,你可能会想这个面试官是不是什么都不懂,是个”青铜”啊。没错,能问这道问题的面试官要么是个......
  • 关于移动跳跃进一步优化
    ......
  • 昇腾CANN 7.0 黑科技:大模型训练性能优化之道
    本文分享自华为云社区《昇腾CANN7.0黑科技:大模型训练性能优化之道》,作者:昇腾CANN。目前,大模型凭借超强的学习能力,已经在搜索、推荐、智能交互、AIGC、生产流程变革、产业提效等场景表现出巨大的潜力。大模型经过海量数据的预训练,通常具有良好的通用性和泛化性。用户基于“大......
  • 构建交互式待办事项应用:使用React和Redux实现技术深度实战
    本文将介绍如何使用React和Redux框架构建一个交互式的待办事项应用。我们将使用React组件化的思想和Redux的状态管理,实现待办事项的添加、完成和过滤功能。通过本文的实战演示,读者将深入了解React和Redux的使用方式和相关概念,为构建复杂的前端应用打下坚实的基础。技术栈:React:一个......