跳到主要内容

性能优化

2025年03月07日
柏拉文
越努力,越幸运

一、认识


React 性能优化策略如下:

优化策略一、useMemo 缓存耗时计算任务, 用于缓存复杂或耗时的计算结果,只有依赖发生变化时才重新计算,避免每次渲染都重复计算。

优化策略二、useCallback 缓存事件处理函数, 用来缓存函数引用,避免因函数每次重新创建而导致子组件重新渲染(尤其是传递给 memo 包裹的组件时)。

优化策略三、基于 memo 浅比较 props, 使用 React.memo 包裹函数组件,对传入的 props 进行浅比较,只有当 props 发生变化时才重新渲染组件。

优化策略四、谨慎使用 Context, 每当 Providervalue 发生变化时, 所有依赖该 Context 的组件都会重新渲染,容易引起性能瓶颈,即使它们只使用了部分数据或者没有引用 Context 数据, 这可能会导致不必要的重渲染和性能下降。我们应当谨慎使用 Context, 将其主要用于传递诸如主题、语言、认证状态等相对静态或低频变化且特别通用的数据, 对于频繁更新的数据或者局部更新的数据则建议采用局部状态或专门的状态管理库

优化策略五、基于 useTransition 来实现并发与过渡, 利用 startTransition 将不紧急的任务标记为低优先级更新,允许这些任务延迟或者中断,从而确保用户交互的流畅性。

优化策略六、列表渲染优化, 列表中的每个子组件都应有唯一且稳定的 key,避免使用 index 作为 key,因为这可能导致重排和不必要的 DOM 更新。

优化策略七、路由和大型组件的懒加载, 使用 React.lazyimport() 进行代码分割, 将页面拆分为多个独立的块,确保首屏关键组件打包进主 bundle, 配合 Suspense 提供加载时的提示。从而加快首屏渲染。

优化策略八、明确 stateref 的使用: 需要触发视图更新的数据应存放在 state 中; 仅用于保存、记录或 DOM 引用的数据则应使用 ref,从而避免不必要的渲染。

优化策略九、副作用与清理, 在组件卸载时应清理全局事件监听器、定时器、订阅等,防止内存泄漏并降低性能损耗。

优化策略十、数据不可变性与深拷贝优化, 对于深嵌套对象或大数组,使用不可变数据结构(例如通过 immer)来管理状态,可以减少深拷贝的性能开销,并使状态更新更易于追踪。

优化策略十一、针对长列表、大数据, 基于 requestAnimationFrame 分片渲染, 由于渲染任务分散到多个动画帧内执行,用户可以逐步看到内容加载,而不会出现长时间白屏或卡顿。保证页面响应速度的同时,逐步渲染大量数据,从而提升整体用户体验

优化策略十二、利用 IntersectionObserver 实现基于视口渲染的组件, 在大量组件或内容需要渲染的场景下, 如果一次性全部渲染, 可能会导致页面加载缓慢或性能瓶颈。基于视口渲染(Viewport-based Rendering)的优化策略是: 1. 延迟加载, 只有当组件进入视口(或即将进入视口)时,才真正渲染其内容; 2. 资源节省, 避免在页面初始加载时渲染那些当前不可见的组件, 从而减少 DOM 操作和内存占用; 3. 通过延迟渲染不可见组件,减少初始渲染量,加快页面响应。

优化策略十三、基于 Web Worker 异步并行计算超级耗时任务, 在某些场景下, 即便使用 useMemo 缓存重计算,单线程的 JavaScript 依然可能在执行非常复杂的计算任务时造成页面卡顿。这时可以借助 Web Worker 将耗时任务放到单独的线程中执行, 从而避免阻塞主线程。

二、实现


2.1 针对长列表、大数据, 基于 requestAnimationFrame 分片渲染

思想: 基于 React 的示例代码,展示如何使用 requestAnimationFrame 对长列表或大数据进行分片渲染,从而避免一次性渲染全部数据导致页面阻塞的问题。任务被分布到多个动画帧中执行,用户可以逐步看到内容加载,保证页面响应速度的同时提升整体用户体验。

说明:

  1. 分片渲染, 组件接收一个 items 数组,并在 useEffect 中通过 requestAnimationFrame 分块处理数据。每个分片渲染指定数量(chunkSize)的数据后,通过 state 更新使得这部分数据立即出现在页面上。

  2. 逐步加载, 由于数据被分片渲染,页面不会一次性渲染所有内容,用户能更快看到部分渲染结果,避免长时间白屏或卡顿现象。

  3. 清理工作, 在组件卸载时通过 cancelAnimationFrame 清除未完成的动画帧调用,避免内存泄漏。

总结: 这种策略将大量渲染任务均摊到多个动画帧中执行,有助于平滑用户体验,同时保证页面响应速度和整体性能。

import React, { useState, useEffect } from 'react';

/**
* ChunkedList 组件:接收一个数据数组,通过 requestAnimationFrame 分片渲染
*
* @param {Array} items - 待渲染的数据数组
* @param {number} chunkSize - 每个动画帧渲染的数据条数,默认为 50
* @param {function} renderItem - 渲染单个数据项的函数,传入 item 和 index
*/
const ChunkedList = ({ items, chunkSize = 50, renderItem }) => {
const [renderedItems, setRenderedItems] = useState([]);

useEffect(() => {
let currentIndex = 0;
let animationFrameId;

const processChunk = () => {
const nextIndex = Math.min(currentIndex + chunkSize, items.length);
// 将当前分片的数据追加到已渲染列表中
setRenderedItems(prevItems => [
...prevItems,
...items.slice(currentIndex, nextIndex)
]);
currentIndex = nextIndex;

// 如果还有数据未渲染,则继续下一帧
if (currentIndex < items.length) {
animationFrameId = requestAnimationFrame(processChunk);
}
};

// 开始分片渲染任务
animationFrameId = requestAnimationFrame(processChunk);

// 组件卸载时清理动画帧
return () => cancelAnimationFrame(animationFrameId);
}, [items, chunkSize]);

return (
<div>
{renderedItems.map((item, index) => (
// 若 item 本身具有唯一 id,则优先使用 item.id
<div key={index}>
{renderItem(item, index)}
</div>
))}
</div>
);
};

export default ChunkedList;

2.2 利用 IntersectionObserver 实现基于视口渲染的组件

思想: 在大量组件或内容需要渲染的场景下, 如果一次性全部渲染,可能会导致页面加载缓慢或性能瓶颈。基于视口渲染(Viewport-based Rendering)的优化策略是:

  1. 延迟加载:只有当组件进入视口(或即将进入视口)时,才真正渲染其内容。

  2. 资源节省:避免在页面初始加载时渲染那些当前不可见的组件,从而减少 DOM 操作和内存占用。

IntersectionObserver 提供了高效、异步的方式来检测 DOM 元素是否进入视口,适合用于实现这一优化策略。

s实现: 可以封装一个高阶组件或包装组件,对目标组件进行 懒渲染。主要步骤包括:

  1. 引用目标元素: 使用 useRef 获取需要观察的 DOM 元素。

  2. 创建观察者: 利用 IntersectionObserver 监听元素与视口的交叉状态。

  3. 条件渲染: 当元素进入视口时更新状态,进而渲染目标组件内容;否则保持空内容或加载占位符。

  4. 清理资源: 组件卸载时断开观察,防止内存泄漏。

import React, { useState, useRef, useEffect } from 'react';

/**
* ViewportRender 组件
* 仅当其内容进入视口时才进行渲染
*
* @param {object} props
* @param {React.ReactNode} props.children - 要渲染的内容
* @param {number} [props.threshold=0.1] - 触发渲染的可见比例
* @param {string} [props.rootMargin='0px'] - 视口边距,可提前加载内容
*/
const ViewportRender = ({ children, threshold = 0.1, rootMargin = '0px' }) => {
const [isVisible, setIsVisible] = useState(false);
const containerRef = useRef(null);

useEffect(() => {
// 创建 IntersectionObserver 实例
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
// 一旦进入视口后取消观察,避免多余回调
observer.unobserve(entry.target);
}
},
{ threshold, rootMargin }
);

if (containerRef.current) {
observer.observe(containerRef.current);
}

// 清理 observer
return () => observer.disconnect();
}, [threshold, rootMargin]);

return <div ref={containerRef}>{isVisible ? children : null}</div>;
};

export default ViewportRender;
import React from 'react';
import ViewportRender from './ViewportRender';
import HeavyComponent from './HeavyComponent';

function App() {
return (
<div>
<h1>我的页面</h1>
{/* 只有当 HeavyComponent 进入视口时才渲染 */}
<ViewportRender threshold={0.2} rootMargin="100px">
<HeavyComponent />
</ViewportRender>
</div>
);
}

2.3 基于 Web Worker 异步并行计算超级耗时任务

思想: 在某些场景下,即便使用 useMemo 缓存重计算,单线程的 JavaScript 依然可能在执行非常复杂的计算任务时造成页面卡顿。这时可以借助 Web Worker 将耗时任务放到单独的线程中执行,从而避免阻塞主线程。

实现: Worker 在独立线程中运行,不会干扰主线程的渲染。我们通过 Worker 的消息通信机制(postMessage/onmessage)将数据传入 Worker 进行处理,并将计算结果返回给主线程。为了方便示例,这里采用动态创建 Worker(通过 Blob 构造 URL),当然也可以将 Worker 代码单独放到文件中。在 React 组件中,我们使用 useEffect 创建 Worker, 并在组件卸载时终止 Worker, 防止内存泄漏。计算结果通过 useState 保存并展示。

这种基于 Worker 的并行计算方式,可以将大量计算任务从主线程中剥离出去,有效提升页面响应速度和整体用户体验。结合 ReactuseEffectuseState,就可以方便地在组件中实现复杂计算的异步并行处理。

import React, { useState, useEffect } from 'react';

const HeavyComputationWithWorker = () => {
const [result, setResult] = useState(null);
const [error, setError] = useState(null);

useEffect(() => {
// Worker 代码:封装重计算任务,例如计算数组所有元素的累加和
const workerCode = () => {
self.onmessage = (e) => {
try {
const data = e.data;
// 模拟一个耗时的计算任务:计算数组累加和
let sum = 0;
for (let i = 0; i < data.length; i++) {
sum += data[i];
}
// 发送计算结果回主线程
self.postMessage({ result: sum });
} catch (err) {
self.postMessage({ error: err.message });
}
};
};

// 将 workerCode 转换为字符串,并创建 Blob 对象
const codeString = workerCode.toString();
const blob = new Blob([`(${codeString})()`], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));

// 监听 Worker 消息
worker.onmessage = (e) => {
const { result, error } = e.data;
if (error) {
setError(error);
} else {
setResult(result);
}
};

// 生成测试数据:一个很大的数组,模拟耗时计算
const data = new Array(10000000).fill(1);
// 发送数据给 Worker
worker.postMessage(data);

// 清理:组件卸载时终止 Worker
return () => {
worker.terminate();
};
}, []);

return (
<div>
<h1>基于 Worker 的并行计算</h1>
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
{result !== null ? <p>计算结果:{result}</p> : <p>计算中...</p>}
</div>
);
};

export default HeavyComputationWithWorker;