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;
這些優化手段在實際項目中,可以根據具體情況選擇和組合使用,以達到最佳的性能效果。