《code秘密花园》送书活动第 24 期,文末查看抽奖方式(送 5 本)
大家好,我是 ssh。
太长不看版:
在构建大型 React 应用程序时,性能问题常常困扰开发者,主要原因是重复渲染。
React Scan
是一个自动检测并高亮显示导致性能问题的渲染工具,帮助开发者精准定位需要修复的组件。文章介绍了 React 重复渲染的几种情况,包括引用类型导致的重新渲染、组件不必要的更新和组件内部状态的频繁变动,并提供了 React Scan 的安装和使用方法。还介绍了一些常见的优化性能的方法,如使用
React.memo
、
useCallback
和
useMemo
、以及合理使用
shouldComponentUpdate
和
PureComponent
。
学习要点
-
重复渲染问题
:了解 React 重复渲染的几种情况及其对性能的影响。
-
React Scan 工具
:掌握 React Scan 的安装和使用方法,包括通过 script 标签和 npm 安装。
-
API 使用
:熟悉 React Scan 的主要 API,如
scan
、
withScan
、
getReport
和
setOptions
。
-
性能优化技巧
:学习使用
React.memo
、
useCallback
、
useMemo
以及
shouldComponentUpdate
和
PureComponent
来优化 React 应用的性能。
-
实际应用
:通过示例代码了解如何在实际项目中应用 React Scan 进行性能检测和优化。
以后的文章我都会加入这个部分,基于 AI 总结,方便没空了解细节的同学快速阅读。
正文如下:
在使用 React 构建大型应用程序时,性能问题通常是困扰开发者的一个重要方面。重复渲染是性能瓶颈的主要原因之一,特别是在
React Compiler
还没出现之前,
React Compiler
在一定程度上就是希望解决这种问题,但是还没有得到广泛应用。所以目前对于大部分
React
项目,在没有得到良好的性能优化重构的前提下,都会有各种性能问题,因此
React Scan
应运而生了。
React Scan
是一个能够自动检测并高亮显示导致性能问题的渲染的工具。这意味着开发者可以精准地知道哪些组件需要修复。
React 重复渲染
在开始介绍之前,我们先简单聊聊 React 重复渲染引发的性能问题。
React
通过对组件的状态(
state
)和属性(
props
)进行监控,决定组件何时需要重新渲染。当组件的状态或属性发生变化时,React 会重新渲染该组件以及与其相关的子组件。尽管这种机制确保了页面内容的实时更新,但也可能带来不必要的重复渲染,进而造成性能问题。
1. 引用类型导致的重新渲染
在 React 中,
props
和
state
的变化会触发重新渲染,但对于引用类型(如对象和数组)的比较,React 使用的是浅比较方式。这意味着即便对象的内容没有变化,只要对象的引用发生了改变,React 仍会触发重新渲染。
示例:
// 示例导致 `ExpensiveComponent` 频繁重新渲染
<ExpensiveComponent onClick={() => alert('hi')} style={{ color: 'purple' }} />
上述代码中,每次渲染时
onClick
和
style
对象都会重新创建,导致
ExpensiveComponent
被迫重新渲染,尽管其内容可能没有变化。
2. 组件不必要的更新
有时候,父组件的状态或属性变化会导致子组件的重新渲染。如果子组件不依赖于这些变化,但依旧被重新渲染,这些更新就是不必要的。
示例:
function ParentComponent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Increase Countbutton>
<ChildComponent />
div>
);
}
function ChildComponent() {
// 如果 `ChildComponent` 不依赖 `count`,它每次仍然会被重新渲染
return <div>Child Componentdiv>;
}
上述例子中,每次
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 标签(确保在引入任何其他脚本之前引用):
html>
<html lang="en">
<head>
<script src="https://unpkg.com/react-scan/dist/auto.global.js">script>
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({
enabled: true,
log: true, // 将渲染信息记录到控制台(默认: false)
});
API 参考
scan(options)
自动扫描应用中的渲染。
scan({
enabled: true, // 启用/禁用扫描
includeChildren: true, // 包括应用了 withScan 的组件的子组件
playSound: true, // 启用/禁用戈伊格计数器音效
log: false, // 将渲染记录到控制台
showToolbar: true, // 显示工具栏
renderCountThreshold: 0, // 渲染次数阈值,仅显示渲染多于此值的组件
report: false, // 向 getReport() 报告数据
onCommitStart: () => {}, // 提交开始时的回调
onRender: (fiber, render) => {}, // 渲染时的回调
onCommitFinish: () => {}, // 提交完成时的回调
onPaintStart: (outline) => {}, // 绘制开始时的回调
onPaintFinish: (outline) => {}, // 绘制完成时的回调
});
withScan(Component, options)
扫描特定组件的渲染。
function Component(props) {
// ...
}
withScan(Component, {
enabled: true, // 启用/禁用扫描
includeChildren: true, // 包括应用了 withScan 的组件的子组件
playSound: true, // 启用/禁用戈伊格计数器音效
log: false, // 将渲染记录到控制台
showToolbar: true, // 显示工具栏
renderCountThreshold: 0, // 渲染次数阈值,仅显示渲染多于此值的组件
report: false, // 向 getReport() 报告数据
onCommitStart: () => {}, // 提交开始时的回调
onRender: (fiber, render) => {}, // 渲染时的回调
onCommitFinish: () => {}, // 提交完成时的回调
onPaintStart: (outline) => {}, // 绘制开始时的回调
onPaintFinish: (outline) => {}, // 绘制完成时的回调
});
getReport()
获取所有组件和渲染的汇总报告。
scan({ report: true });
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({
enabled: true, // 启用/禁用扫描
includeChildren: true, // 包括应用了 withScan 的组件的子组件
playSound: true, // 启用/禁用戈伊格计数器音效
log: false, // 将渲染记录到控制台
showToolbar: true, // 显示工具栏
renderCountThreshold: 0, // 渲染次数阈值,仅显示渲染多于此值的组件
report: false, // 向 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({
enabled: true,
log: true,
playSound: true,
showToolbar: true,
report: true,
});
setInterval(() => {
const report = getReport();
for (const component in report) {
const { count, time } = report[component];
console.log(`${component} rendered ${count} times, took ${time}ms`);
}
}, 1000);