最新开源:超强的 React 性能测试工具!

科技   2024-12-10 10:10   江苏  

《code秘密花园》送书活动第 24 期,文末查看抽奖方式(送 5 本)

大家好,我是 ssh。


太长不看版:

在构建大型 React 应用程序时,性能问题常常困扰开发者,主要原因是重复渲染。React Scan 是一个自动检测并高亮显示导致性能问题的渲染工具,帮助开发者精准定位需要修复的组件。文章介绍了 React 重复渲染的几种情况,包括引用类型导致的重新渲染、组件不必要的更新和组件内部状态的频繁变动,并提供了 React Scan 的安装和使用方法。还介绍了一些常见的优化性能的方法,如使用 React.memouseCallbackuseMemo、以及合理使用 shouldComponentUpdatePureComponent

学习要点

  1. 重复渲染问题:了解 React 重复渲染的几种情况及其对性能的影响。
  2. React Scan 工具:掌握 React Scan 的安装和使用方法,包括通过 script 标签和 npm 安装。
  3. API 使用:熟悉 React Scan 的主要 API,如 scanwithScangetReportsetOptions
  4. 性能优化技巧:学习使用 React.memouseCallbackuseMemo 以及 shouldComponentUpdatePureComponent 来优化 React 应用的性能。
  5. 实际应用:通过示例代码了解如何在实际项目中应用 React Scan 进行性能检测和优化。

以后的文章我都会加入这个部分,基于 AI 总结,方便没空了解细节的同学快速阅读。


正文如下:

在使用 React 构建大型应用程序时,性能问题通常是困扰开发者的一个重要方面。重复渲染是性能瓶颈的主要原因之一,特别是在 React Compiler 还没出现之前, React Compiler 在一定程度上就是希望解决这种问题,但是还没有得到广泛应用。所以目前对于大部分 React 项目,在没有得到良好的性能优化重构的前提下,都会有各种性能问题,因此 React Scan 应运而生了。

React Scan 是一个能够自动检测并高亮显示导致性能问题的渲染的工具。这意味着开发者可以精准地知道哪些组件需要修复。

React 重复渲染

在开始介绍之前,我们先简单聊聊 React 重复渲染引发的性能问题。

React 通过对组件的状态(state)和属性(props)进行监控,决定组件何时需要重新渲染。当组件的状态或属性发生变化时,React 会重新渲染该组件以及与其相关的子组件。尽管这种机制确保了页面内容的实时更新,但也可能带来不必要的重复渲染,进而造成性能问题。

1. 引用类型导致的重新渲染

在 React 中,propsstate 的变化会触发重新渲染,但对于引用类型(如对象和数组)的比较,React 使用的是浅比较方式。这意味着即便对象的内容没有变化,只要对象的引用发生了改变,React 仍会触发重新渲染。

示例:

// 示例导致 `ExpensiveComponent` 频繁重新渲染
<ExpensiveComponent onClick={() => alert('hi')} style={{ color: 'purple' }} />

上述代码中,每次渲染时 onClickstyle 对象都会重新创建,导致 ExpensiveComponent 被迫重新渲染,尽管其内容可能没有变化。

2. 组件不必要的更新

有时候,父组件的状态或属性变化会导致子组件的重新渲染。如果子组件不依赖于这些变化,但依旧被重新渲染,这些更新就是不必要的。

示例:

function ParentComponent({
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>
      <ChildComponent />
    </div>

  );
}

function ChildComponent({
  // 如果 `ChildComponent` 不依赖 `count`,它每次仍然会被重新渲染
  return <div>Child Component</div>;
}

上述例子中,每次 count 发生变化时,ChildComponent 也会不必要地重新渲染。

3. 组件内部状态的频繁变动

当组件有内部管理的状态且状态频繁变动时,它会导致组件自身的频繁重新渲染。如果状态变化得过于频繁,可能会显著影响性能。

示例:

function Counter({
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return <div>Count: {count}</div>;
}

上述代码中,每秒钟 count 的变化都会触发组件的重新渲染。

安装

你可以通过两种方式快速安装 React Scan

通过 Script 标签

在你的应用中引入以下 script 标签(确保在引入任何其他脚本之前引用):

<!doctype html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>

    <!-- rest of your scripts go under -->
  </head>
  <body>
    <!-- ... -->
  </body>
</html>

Next.js(页面)引入示例:将脚本标签添加到 pages/_document.tsx 中:

import { Html, Head, Main, NextScript } from 'next/document';

export default function Document({
  return (
    <Html lang="en">
      <Head>
        <script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>
        {/* 其余的脚本都在这里下面添加 */}
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>

  );
}

通过 npm 安装

如果你习惯通过 npm 管理依赖,可以执行以下命令安装 React Scan

npm install react-scan

然后在应用中,在导入 React 之前,引入以下代码:

import { scan } from 'react-scan'// 在 react 之前引入
import React from 'react';

scan({
  enabledtrue,
  logtrue// 将渲染信息记录到控制台(默认: false)
});

API 参考

scan(options)

自动扫描应用中的渲染。

scan({
  enabledtrue// 启用/禁用扫描
  includeChildrentrue// 包括应用了 withScan 的组件的子组件
  playSoundtrue// 启用/禁用戈伊格计数器音效
  logfalse// 将渲染记录到控制台
  showToolbartrue// 显示工具栏
  renderCountThreshold0// 渲染次数阈值,仅显示渲染多于此值的组件
  reportfalse// 向 getReport() 报告数据
  onCommitStart() => {}, // 提交开始时的回调
  onRender(fiber, render) => {}, // 渲染时的回调
  onCommitFinish() => {}, // 提交完成时的回调
  onPaintStart(outline) => {}, // 绘制开始时的回调
  onPaintFinish(outline) => {}, // 绘制完成时的回调
});

withScan(Component, options)

扫描特定组件的渲染。

function Component(props{
  // ...
}

withScan(Component, {
  enabledtrue// 启用/禁用扫描
  includeChildrentrue// 包括应用了 withScan 的组件的子组件
  playSoundtrue// 启用/禁用戈伊格计数器音效
  logfalse// 将渲染记录到控制台
  showToolbartrue// 显示工具栏
  renderCountThreshold0// 渲染次数阈值,仅显示渲染多于此值的组件
  reportfalse// 向 getReport() 报告数据
  onCommitStart() => {}, // 提交开始时的回调
  onRender(fiber, render) => {}, // 渲染时的回调
  onCommitFinish() => {}, // 提交完成时的回调
  onPaintStart(outline) => {}, // 绘制开始时的回调
  onPaintFinish(outline) => {}, // 绘制完成时的回调
});

getReport()

获取所有组件和渲染的汇总报告。

scan({ reporttrue });

const report = getReport();

for (const component in report) {
  const { count, time } = report[component];
  console.log(`${component} rendered ${count} times, took ${time}ms`);
}

setOptions(options)

设置扫描选项。

function Component(props{
  // ...
}

setOptions({
  enabledtrue// 启用/禁用扫描
  includeChildrentrue// 包括应用了 withScan 的组件的子组件
  playSoundtrue// 启用/禁用戈伊格计数器音效
  logfalse// 将渲染记录到控制台
  showToolbartrue// 显示工具栏
  renderCountThreshold0// 渲染次数阈值,仅显示渲染多于此值的组件
  reportfalse// 向 getReport() 报告数据
  onCommitStart() => {}, // 提交开始时的回调
  onRender(fiber, render) => {}, // 渲染时的回调
  onCommitFinish() => {}, // 提交完成时的回调
  onPaintStart(outline) => {}, // 绘制开始时的回调
  onPaintFinish(outline) => {}, // 绘制完成时的回调
});

getOptions()

获取当前扫描选项。

const options = getOptions();
console.log(options);

测试一下

下面我们来实现一个 Demo 试一下:

import React, { useState } from 'react';
import type { FC } from 'react';
import { scan, getReport } from 'react-scan';
import ExpensiveComponent from './components/ExpensiveComponent';
import Counter from './components/Counter';
import ParentComponent from './components/ParentComponent';

// 初始化 React Scan
scan({
    enabledtrue,
    logtrue,
    playSoundtrue,
    showToolbartrue,
    reporttrue,
});


setInterval(() => {
    const report = getReport();

    for (const component in report) {
        const { count, time } = report[component];
        console.log(`${component} rendered ${count} times, took ${time}ms`);
    }
}, 1000);


const App: FC = () => {
    const [theme, setTheme] = useState<'light' | 'dark'>('light');



    return (
        <div className="App" style={{ padding: '20px' }}>
            <h1>React Scan Demo</h1>

            <button
                onClick={() =>
 setTheme(theme === 'light' ? 'dark' : 'light')}
                style={{ marginBottom: '20px' }}
            >
                切换主题
            </button>

            <div style={{
                display: 'grid',
                gap: '20px',
                backgroundColor: theme === 'light' ? '#fff' : '#333',
                color: theme === 'light' ? '#000' : '#fff',
                padding: '20px'
            }}>

                <ExpensiveComponent
                    onClick={() =>
 alert('clicked')}
                    style={{ color: 'purple' }}
                />
                <Counter />
                <ParentComponent />
            </div>
        </div>

    );
};

export default App; 

当我们点击切换主题时,可以精准查看子组件重新渲染的情况和耗时,另外我们也可以打印完整的性能报告:

常见问题解答

  • 为什么选择这个而不是 React Devtools:虽然 React Devtools 旨在作为通用工具,但它没有区别必要和不必要渲染的明显区分,并且缺乏编程 API。如果你每天都在处理 React 性能问题,那么 React Scan 可能更适合你。除此之外,React Devtools 存在一些实际使用中的问题,例如:渲染速度过快的组件滞后、滚动或调整框大小时位置不更新、没有渲染次数统计等。

  • React Native 支持吗?很快就会支持!

  • Chrome 插件支持吗?也即将推出!

解决 React 性能问题

下面是一些常见的解决 React 性能问题的思路:

1. 使用 React.memo

React.memo 是一个高阶组件,用于缓存函数组件的渲染结果,从而避免不必要的更新。如果传递给组件的 props 没有变化,React.memo 会使用上一次渲染的结果而不是重新渲染组件。

示例:

const ChildComponent = React.memo(function ChildComponent({
  return <div>Child Component</div>;
});

2. 使用 useCallback 和 useMemo

useCallbackuseMemo 可以帮助缓存创建的函数和计算结果,从而避免因为函数引用变化导致的重复渲染。

示例:

function ParentComponent({
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    alert('hi');
  }, []);

  const style = useMemo(() => ({ color'purple' }), []);

  return <ExpensiveComponent onClick={handleClick} style={style} />;
}

上述代码使用 useCallback 缓存 handleClick 函数,使用 useMemo 缓存 style 对象,避免它们每次重新创建导致的重复渲染。

3. 合理使用 shouldComponentUpdate 和 PureComponent

对于类组件,可以使用 shouldComponentUpdate 来控制组件的更新逻辑。此外,React.PureComponent 会自动实现浅比较,避免不必要的重新渲染。

class ChildComponent extends React.PureComponent {
  render() {
    return <div>Child Component</div>;
  }
}

前端从进阶到入院
我是 ssh,只想用最简单的方式把原理讲明白。wx:sshsunlight,分享前端的前沿趋势和一些有趣的事情。
 最新文章