1. 使用 React.memo 进行组件优化
问题:当父组件重新渲染时,子组件也会重新渲染,即使它的 props 没有变化。
解决方案:使用 React.memo 包裹子组件,让其只在 props 变化时才重新渲染。
示例场景:展示一个显示计数的组件,只有计数值变化时才会重新渲染。
import React, { useState } from 'react';const ChildComponent = React.memo(({ count }) => {console.log('ChildComponent rendered');return <div>Count: {count}</div>;
});function ParentComponent() {const [count, setCount] = useState(0);const [text, setText] = useState('');return (<div><ChildComponent count={count} /><button onClick={() => setCount(count + 1)}>Increase Count</button><inputtype="text"value={text}onChange={e => setText(e.target.value)}placeholder="Type something"/></div>);
}export default ParentComponent;
在这个例子中,即使输入框内容改变,ChildComponent 也不会重新渲染,因为它被 React.memo 包裹,只会在 count 变化时重新渲染。
2. 使用 useCallback 和 useMemo 优化函数和计算
问题:在函数式组件中,每次渲染都会创建新的函数和计算,导致不必要的渲染和性能浪费。
解决方案:使用 useCallback 缓存函数,useMemo 缓存计算结果。
示例场景:避免函数在不必要的情况下被重新创建。
import React, { useState, useCallback, useMemo } from 'react';function ExpensiveComponent({ onClick }) {console.log('ExpensiveComponent rendered');return <button onClick={onClick}>Click me</button>;
}function ParentComponent() {const [count, setCount] = useState(0);const handleClick = useCallback(() => {console.log('button clicked');}, []);const computedValue = useMemo(() => {console.log('computed value');return count * 2;}, [count]);return (<div><ExpensiveComponent onClick={handleClick} /><div>Computed Value: {computedValue}</div><button onClick={() => setCount(count + 1)}>Increase Count</button></div>);
}export default ParentComponent;
在这个例子中,handleClick 和 computedValue 只会在依赖项变化时重新创建,从而避免不必要的渲染和计算。
3. 避免不必要的 Re-render
问题:由于父组件的状态或 props 改变,导致子组件不必要地重新渲染。
解决方案:拆分组件,使用 React.memo 或 shouldComponentUpdate,并确保 key 使用合理。
示例场景:优化子组件渲染逻辑。
import React, { useState } from 'react';const ListItem = React.memo(({ item }) => {console.log('ListItem rendered:', item);return <li>{item}</li>;
});function ParentComponent() {const [items, setItems] = useState(['Item 1', 'Item 2', 'Item 3']);const [newItem, setNewItem] = useState('');const addItem = () => {setItems([...items, newItem]);setNewItem('');};return (<div><ul>{items.map((item, index) => (<ListItem key={index} item={item} />))}</ul><inputvalue={newItem}onChange={(e) => setNewItem(e.target.value)}placeholder="Add new item"/><button onClick={addItem}>Add Item</button></div>);
}export default ParentComponent;
4. 虚拟列表优化长列表渲染
问题:渲染大量列表项会导致页面卡顿。
解决方案:使用虚拟滚动技术(如 react-window、@tanstack/react-virtual)只渲染当前视口中的元素。
示例场景:渲染大量数据时使用虚拟列表优化性能。
import React from 'react';
import { FixedSizeList as List } from 'react-window';const Row = ({ index, style }) => (<div style={style}>Row {index}</div>
);function VirtualizedList() {return (<Listheight={150}itemCount={1000}itemSize={35}width={300}>{Row}</List>);
}export default VirtualizedList;
虚拟滚动的原理如下:
4.1. IntersectionObserver 接口简介
IntersectionObserver 接口是 Intersection Observer API 的一部分,用于异步观察目标元素与其祖先元素或顶级文档视口之间的交叉状态,这里的祖先元素或视口被称为根。
创建 IntersectionObserver 对象时,可以设置监听的可见区域比例。该配置一旦确定便不可更改,因此一个观察者对象只能用于监听指定的变化值。不过,同一个观察者可以监听多个目标元素。
// https://caniuse.com/?search=Intersection%20Observer// 创建一个 IntersectionObserver 实例
var observer = new IntersectionObserver(callback, options);
4.2. IntersectionObserver 支持的参数
4.2.1. 回调函数
当目标元素的可见性发生变化时,观察器会调用回调函数 callback 。通常,该函数会触发两次:一次是元素进入视口,开始可见;另一次是离开视口,开始不可见。
示例代码如下:
// 初始化一个 IntersectionObserver 实例
var observer = new IntersectionObserver(changes => {for (const change of changes) {console.log(change.time); // 状态变化的时间戳console.log(change.rootBounds); // 根元素的矩形区域信息console.log(change.boundingClientRect); // 目标元素的矩形区域信息console.log(change.intersectionRect); // 交叉区域的信息console.log(change.intersectionRatio); // 目标元素的可见比例console.log(change.target); // 被观察的目标元素}
}, {});// 开始监听目标元素
observer.observe(target);// 停止监听目标元素
observer.unobserve(target);// 停止所有监听
observer.disconnect();
在上述代码中,callback 是一个箭头函数,接收一个包含 IntersectionObserverEntry 对象的数组作为参数。每个对象代表一个可见性变化的条目。
IntersectionObserverEntry 对象包含以下属性:
time:可见性变化发生的时间戳。
target:被观察的目标元素。
rootBounds:根元素的矩形区域信息。
boundingClientRect:目标元素的矩形区域信息。
intersectionRect:目标元素与视口或根元素的交叉区域信息。
intersectionRatio:目标元素的可见比例,完全可见时为1,完全不可见时为0或更小。
4.2.2. 配置对象
IntersectionObserver 构造函数的第二个参数是配置对象,其中可设置以下属性:
1. threshold 属性
threshold 决定了在何种交叉比例下触发回调函数。默认值为 [0],表示当交叉比例达到 0 时触发回调。用户可自定义该数组,如 [0, 0.25, 0.5, 0.75, 1] 表示当目标元素可见比例为 0%、25%、50%、75%、100% 时触发回调。
new IntersectionObserver(entries => { /* ... */ }, { threshold: [0, 0.25, 0.5, 0.75, 1] });
2. root 属性和 rootMargin 属性
root 属性指定目标元素所在的滚动容器。rootMargin 用于扩展或缩小根元素的边界,从而影响交叉区域的大小。该属性使用 CSS 语法定义,如 10px 20px 30px 40px,表示四个方向的 margin 值。
var opts = {root: document.querySelector('.container'),rootMargin: "500px 0px"
};var observer = new IntersectionObserver(callback, opts);
4.3. 应用实例
4.3.1. 懒加载
使用 IntersectionObserver API 可以实现惰性加载,在目标元素进入视口时才加载,从而节省带宽。
function query(selector) {return Array.from(document.querySelectorAll(selector));
}var observer = new IntersectionObserver(function(changes) {changes.forEach(function(change) {var container = change.target;var content = container.querySelector('template').content;container.appendChild(content);observer.unobserve(container);});
});query('.lazy-loaded').forEach(function (item) {observer.observe(item);
});
4.3.2. 无限滚动
实现无限滚动的代码示例如下:
var intersectionObserver = new IntersectionObserver(function (entries) {if (entries[0].intersectionRatio <= 0) return;loadItems(10);console.log('Loaded new items');
});intersectionObserver.observe(document.querySelector('.scrollerFooter'));
4.4. 核心 Hooks 实现
以下是 IntersectionObserver 的核心 Hooks 实现代码:
import { RefObject, useEffect, useState } from 'react';interface Args extends IntersectionObserverInit {freezeOnceVisible?: boolean;
}export const useIntersectionObserver = (elementRef: RefObject<Element>,{ threshold = 0, root = null, rootMargin = '0%', freezeOnceVisible = false }: Args,
): IntersectionObserverEntry | undefined => {const [entry, setEntry] = useState<IntersectionObserverEntry>();const frozen = entry?.isIntersecting && freezeOnceVisible;const updateEntry = ([entry]: IntersectionObserverEntry[]): void => {setEntry(entry);};useEffect(() => {const node = elementRef?.current;const hasIOSSupport = !!window.IntersectionObserver;if (!hasIOSSupport || frozen || !node) return;const observer = new IntersectionObserver(updateEntry, { threshold, root, rootMargin });observer.observe(node);return () => observer.disconnect();}, [elementRef?.current, JSON.stringify(threshold), root, rootMargin, frozen]);return entry;
};
5. 懒加载组件
问题:页面首次加载时间过长。
解决方案:使用 React.lazy 和 Suspense,按需加载组件,减少首屏渲染时间。
示例场景:按需加载不常用的组件。
import React, { Suspense } from 'react';const LazyComponent = React.lazy(() => import('./LazyComponent'));function App() {return (<Suspense fallback={<div>Loading...</div>}><LazyComponent /></Suspense>);
}export default App;
6. 优化资源加载
场景:减少初始资源加载,提升页面性能。
图片懒加载:
<!-- 图片懒加载 -->
<img src="example.jpg" loading="lazy" alt="Lazy loaded image" />
代码分割示例:
// 代码分割示例
import { lazy } from 'react';const SomeComponent = lazy(() => import('./SomeComponent'));
7. 避免不必要的状态更新
问题:频繁更新状态会导致组件重复渲染。
解决方案:优化状态管理逻辑,使用 useReducer 或 Context。
示例场景:使用 useReducer 管理复杂状态。
import React, { useReducer } from 'react';function reducer(state, action) {switch (action.type) {case 'increment':return { count: state.count + 1 };default:return state;}
}function Counter() {const [state, dispatch] = useReducer(reducer, { count: 0 });return (<div><button onClick={() => dispatch({ type: 'increment' })}>Count: {state.count}</button></div>);
}export default Counter;
8. 使用 Profiler 分析性能瓶颈
场景:使用 React Profiler 工具来分析和优化组件渲染。
import React, { Profiler } from 'react';function onRenderCallback(id,phase,actualDuration,baseDuration,startTime,commitTime,interactions
) {console.log({ id, phase, actualDuration });
}function App() {return (<Profiler id="App" onRender={onRenderCallback}><MyComponent /></Profiler>);
}export default App;
这些优化手段在实际项目中,可以根据具体情况选择和组合使用,以达到最佳的性能效果。