React 文档
导读与全书目录表:见同目录 01-React 入门:简介·特性与设计范式.md 开头。
react 渲染优化
函数组件优化
三大缓存:缓存组件、缓存函数本身、缓存函数结果,以此减少不必要的渲染和计算开销
- 缓存组件 - React.memo
React.memo 是 React 提供的一个高阶组件(HOC),它会缓存组件的渲染结果,只有当 props 真正改变 时,组件才重新渲染,类似于类组件中的 PureComponent
js
import { memo } from 'react';
const Child = memo(({ name }) => {
return <div>{name}</div>;
}, (prevProps, nextProps) => {
// 第二个参数为 arePropsEqual:返回 true 表示 props 相当,跳过本次渲染
return prevProps.name === nextProps.name;
});- 缓存函数本身 - useCallback
避免每次渲染都创建新函数,用于传递给子组件的方法
js
import { useCallback } from 'react';
const memoizedFn = useCallback(() => {
doSomething(a, b);
}, [a, b]);
<Child data={data} memoizedFn={memoizedFn}></Child>- 缓存函数结果 - useMemo:相当于 vue 的计算属性
js
import { useMemo } from 'react';
const [num, setNum] = useState(0);
const [other, setOther] = useState(100);
const memoizedValue = useMemo(() => {
return other + 999 + num;
}, [num, other]);类组件优化
- 手动比较:手动比较 state 和 props
- 自动比较:自动 浅 比较 state 和 props
- 手动比较 - shouldComponentUpdate:适用于复杂组件的精细控制
js
shouldComponentUpdate(nextProps, nextState) {
// 仅在数据变化时更新
if (this.props.id !== nextProps.id) return true;
if (this.state.count !== nextState.count) return true;
return false;
}- 自动比较 - PureComponent:自动浅比较,适合大部分场景,比手动实现更简洁 注意:传递对象 / 数组时需创建新引用,否则 PureComponent 可能不更新
js
class User extends React.PureComponent {
render() {
return <div>{this.props.name}</div>;
}
}react 如何提高渲染效率?避免不必要 render?
回答
shell
React 渲染效率优化的本质是:"该渲染的组件精准渲染,不该渲染的组件绝不渲染"
避免不必要的 render:通过 props/state 比较、缓存等手段,跳过不需要更新的组件。
优化 render 过程:减少单次渲染的计算量(如虚拟列表、状态拆分)。
# 具体方案
1. 用 React.memo 包裹函数组件(最常用)
原理:React.memo 是高阶组件(HOC),会对组件的 props 进行浅比较(shallow equal)。如果 props 未变化,直接跳过渲染,复用上次的渲染结果。
场景:纯展示型组件(如列表项、卡片),或子组件渲染仅依赖 props 时。
话术:" 对于纯展示型子组件,我会用 React.memo 包裹它。这样只要父组件传递的 props 引用不变,子组件就不会重新渲染。比如一个商品列表项组件,只有当 item 数据真正变化时才会更新。"
2. 用 useCallback 缓存函数引用(配合 React.memo)
原理:父组件重新渲染时,内部定义的函数会生成新的引用。如果直接把新函数传给 React.memo 包裹的子组件,浅比较会失效,导致子组件误渲染。useCallback 可以缓存函数引用,只有依赖项变化时才返回新函数。
场景:父组件向子组件传递回调函数(如 onClick、onChange)时
话术:" 如果父组件要给子组件传函数,我会用 useCallback 把函数包起来。比如父组件有个 handleDelete 函数,用 useCallback 缓存后,只要它的依赖项(比如 list)不变,函数引用就不变。配合子组件的 React.memo,就能避免子组件因为父组件其他状态变化而重新渲染。"
3. 用 useMemo 缓存计算结果
原理:对于复杂计算(如大数据过滤、排序、格式化),如果每次渲染都重新执行,会浪费性能。useMemo 可以缓存计算结果,只有依赖项变化时才重新计算。
场景:处理大数组、复杂对象转换、耗时计算等。
话术:" 如果组件里有对大数组的过滤或排序,我会用 useMemo 把计算逻辑包起来。比如一个搜索过滤列表,只有当 searchText 或原始 data 变化时,才会重新计算过滤后的列表,避免每次渲染都遍历整个数组。"
4. 合理拆分组件与状态,缩小渲染范围
原理:如果把所有状态都放在一个大组件里,任何小状态变化都会导致整个组件重新渲染。通过拆分组件,把不相关的状态放到不同子组件中,可以让状态变化只影响局部。
场景:页面包含多个独立模块(如表单 + 列表、侧边栏 + 主内容)时。
话术示例:" 我会避免把所有状态堆在一个组件里。比如一个页面有表单和列表,我会把表单状态封装在 Form 组件,列表状态封装在 List 组件。这样表单输入时,只有 Form 组件重新渲染,不会影响 List。"
5. 列表渲染用唯一 key,避免用索引
原理:key 是 React 识别列表项的唯一标识。用不稳定的 index 做 key,当列表顺序变化时,React 会错误地复用 DOM 节点,导致额外的渲染甚至 bug。用唯一 ID 能让 React 精准复用节点。
场景:任何列表渲染(map)。
话术示例:" 列表渲染时我会用数据的唯一 ID 做 key,而不是数组索引。比如用户列表用 user.id,这样即使列表排序或插入新项,React 也能正确识别每个节点,减少不必要的 DOM 销毁和重建。"
6. 长列表用虚拟列表(Virtual List)
原理:如果渲染上千条数据,即使列表项不重新渲染,大量 DOM 节点也会占用内存。虚拟列表只渲染可视区域内的节点,滚动时动态替换内容,大幅减少 DOM 数量。
工具:react-window(轻量)、react-virtualized(功能全)。
场景:长列表、无限滚动、大数据表格。
话术示例:" 对于超过几百条的长列表,我会用虚拟列表。比如用 react-window,只渲染屏幕可见的几十条数据,滚动时再动态加载新内容。这样不管有多少条数据,DOM 节点数都保持在可控范围,渲染和滚动都会很流畅。"
# 总结
" 总的来说,React 渲染优化的核心是 ' 缓存 ' + ' 拆分':用 React.memo、useMemo、useCallback 做缓存,避免不必要的渲染;用组件拆分和虚拟列表做结构优化,减少单次渲染的压力。同时,我会用 React DevTools 的 Profiler 面板定位性能瓶颈,针对性优化,而不是盲目加优化手段。"代码
- React.memo —— 组件级缓存:子组件仅依赖 props,父组件其他状态变化时不希望子组件重新渲染。
js
// 子组件:用 React.memo 包裹
const ProductItem = React.memo(({ item, onBuy }) => {
console.log('ProductItem 渲染了'); // 只有 item 或 onBuy 变化时才会打印
return <div onClick={() => onBuy(item.id)}>{item.name}</div>;
});
// 父组件
const App = () => {
const [count, setCount] = useState(0);
const [product] = useState({ id: 1, name: 'iPhone' });
// 注意:这里的 onBuy 还没优化,后面用 useCallback 解决
const handleBuy = (id) => console.log('购买', id);
return (
<div>
<button onClick={() => setCount(count + 1)}>点击次数: {count}</button>
{/* 即使 count 变化,ProductItem 也不会重新渲染(因为 product 引用不变) */}
<ProductItem item={product} onBuy={handleBuy} />
</div>
);
};- useCallback —— 函数引用缓存(配合 React.memo) —— 父组件向子组件传递回调函数,避免父组件渲染导致子组件误渲染。
js
const ProductItem = React.memo(({ item, onBuy }) => {
console.log('ProductItem 渲染了');
return <div onClick={() => onBuy(item.id)}>{item.name}</div>;
});
const App = () => {
const [count, setCount] = useState(0);
const [product] = useState({ id: 1, name: 'iPhone' });
// 用 useCallback 缓存函数引用,只有依赖项(这里是空数组)变化时才返回新函数
const handleBuy = useCallback((id) => {
console.log('购买', id);
}, []); // 依赖项:如果函数里用到了 product,需要把 product 放进去
return (
<div>
<button onClick={() => setCount(count + 1)}>点击次数: {count}</button>
{/* 现在 count 变化时,handleBuy 引用不变,ProductItem 不会重新渲染 */}
<ProductItem item={product} onBuy={handleBuy} />
</div>
);
};- useMemo —— 计算结果缓存 —— 避免每次渲染都执行耗时的计算(如大数组过滤、排序)
js
const App = () => {
const [searchText, setSearchText] = useState('');
const [count, setCount] = useState(0);
// 模拟大数组
const bigList = Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
// 用 useMemo 缓存过滤结果,只有 searchText 或 bigList 变化时才重新计算
const filteredList = useMemo(() => {
console.log('执行了过滤计算'); // count 变化时不会打印
return bigList.filter(item => item.name.includes(searchText));
}, [searchText, bigList]); // 依赖项
return (
<div>
<button onClick={() => setCount(count + 1)}>点击次数: {count}</button>
<input value={searchText} onChange={(e) => setSearchText(e.target.value)} />
<div>结果数量: {filteredList.length}</div>
</div>
);
};- 组件拆分 —— 把大组件拆成小组件,让状态变化只影响局部。
js
// ❌ 不好的做法:所有状态都在一个组件,输入时整个页面重新渲染
const BadApp = () => {
const [text, setText] = useState('');
const [count, setCount] = useState(0);
console.log('BadApp 渲染了'); // 输入 text 或点击 count 都会打印
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
);
};
// ✅ 好的做法:拆分成独立小组件,状态变化只影响自身
const InputComponent = () => {
const [text, setText] = useState('');
console.log('InputComponent 渲染了'); // 只有输入时打印
return <input value={text} onChange={(e) => setText(e.target.value)} />;
};
const ButtonComponent = () => {
const [count, setCount] = useState(0);
console.log('ButtonComponent 渲染了'); // 只有点击时打印
return <button onClick={() => setCount(count + 1)}>{count}</button>;
};
const GoodApp = () => {
return (
<div>
<InputComponent />
<ButtonComponent />
</div>
);
};- 虚拟列表 —— react-window 示例 —— 长列表只渲染可视区域,减少 DOM 节点。
js
import { FixedSizeList as List } from 'react-window';
const App = () => {
// 模拟 10000 条数据
const bigList = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);
// 列表项渲染函数
const Row = ({ index, style }) => (
<div style={style}>
{bigList[index]}
</div>
);
return (
// 只渲染可视区域的 10 条数据(高度 500px,每项 50px)
<List
height={500} // 列表总高度
itemCount={bigList.length} // 数据总条数
itemSize={50} // 每项高度
width={300} // 列表宽度
>
{Row}
</List>
);
};react 优化相关梳理
回答
shell
我从 “组件层面” 和 “全局层面” 总结:
① 组件内:用 useMemo(缓存计算结果)、useCallback(缓存函数),避免子组件不必要的重渲染;React.memo 包裹纯组件(浅对比 props,减少重渲染);
② 列表优化:加唯一 key(避免 key 用 index),虚拟列表(长列表只渲染可视区域);
③ 全局层面:路由懒加载(React.lazy + Suspense)、减少不必要的重新渲染(比如状态只存在需要的组件里)、避免重复请求(请求缓存)。useMemo、useCallback、React.memo 该怎么用?什么时候会负优化?
shell
# 适用场景
子组件频繁重渲染时,用 useCallback 缓存传递给子组件的函数,useMemo 缓存复杂计算结果,React.memo 浅比较 props 阻止子组件无效渲染;
# 负优化场景
简单值(如数字 / 字符串)、轻量计算(如简单加法)不用缓存,因为缓存本身有开销,反而增加性能损耗;频繁变化的依赖(如每次渲染都变的对象)也不用,缓存会失效,白做功;③ 核心原则:只缓存 “重计算 / 重渲染” 的场景,不滥用。React 长列表(万条数据)怎么优化?
shell
1. 核心用虚拟列表(react-window/react-virtualized),只渲染可视区的列表项,上下留少量缓冲区,避免一次性渲染万条 DOM;
2. 配合防抖节流:滚动事件做节流,避免同一时段内反复触发重计算;
3. 数据处理:大数据解析 / 筛选丢到 Web Worker,不阻塞主线程;
4. 组件优化:列表项用 React.memo 包裹,props 只传必要值,减少重渲染。性能优化类
shell
组件层面:使用 React.memo(函数组件)/PureComponent(类组件)做浅比较,避免无意义的重渲染;拆分组件,让组件粒度更细,只渲染变化的部分;
渲染层面:列表渲染加唯一 key,避免 diff 算法误判;使用 useMemo 缓存计算结果、useCallback 缓存函数引用,防止因为函数 / 值重新创建导致子组件重渲染;
状态层面:减少不必要的状态,把状态放在最小作用域内,避免全局状态滥用;
其他手段:使用懒加载(React.lazy + Suspense)拆分代码包,减小首屏加载体积;避免在 render 中创建函数 / 对象,因为每次 render 都会生成新引用,触发子组件重渲染。react 组件懒加载 + 错误边界
使用 React.lazy 和 Suspense 实现,特点:构建工具打包时实现代码分割
组件懒加载
js
// 使用 React.lazy 和 Suspense
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}路由懒加载
js
import { Component, Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 懒加载组件
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
// 错误边界组件
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <div>加载失败,请刷新重试</div>;
}
return this.props.children;
}
}
export default function App() {
return (
<BrowserRouter>
<ErrorBoundary>
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</ErrorBoundary>
</BrowserRouter>
);
}react 生产环境打包构建优化
- 代码分割:使用 React.lazy + Suspense 加载异步组件,或者使用路由懒加载
- Tree Shaking:保使用 ES Module,配合 Webpack、Rollup 移除未使用代码。(import & export)
- 代码压缩:Vite 2+ 默认使用 esbuild 进行代码压缩(比 Terser 快 10-100 倍),只需在 vite.config.js 中启用
- 缓存:Vite 默认 每次构建生成新的 hash 文件名
react 虚拟列表
js
// 使用 react-window 处理长列表
import { FixedSizeList } from 'react-window';
const LongList = ({ items }) => (
<FixedSizeList
height={500}
width="100%"
itemCount={items.length}
itemSize={50}
>
{({ index, style }) => (
<div style={style}>{items[index]}</div>
)}
</FixedSizeList>
);react 图片优化
WebP 格式 代替 jpg/png
- 为何 WebP 性能好 WebP 性能好的本质是 “技术代差”(2010 年由谷歌推出)(JPEG 1992 年、PNG 1996 年)
- WebP 特点(相比jpg/png):文件体积、画质、原生透明、动画、渐进式加载、支持近无损、裁剪优化,按需调节
- WebP 优势 WebP 成为现代网页、APP、小程序等场景的 “性能首选图片格式”—— 目前主流浏览器(Chrome、Firefox、Edge、Safari 14.1+)和移动系统(Android 4.0+、iOS 14+)均已全面支持,兼容性已不再是障碍。
图片懒加载
- 方式一:自行封装(监听 scroll)
js
import React, { useRef, useEffect, useState } from 'react';
export default function LazyImageScroll({ src, placeholder = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', alt = '' }) {
const imgRef = useRef(null);
const [imageSrc, setImageSrc] = useState(placeholder);
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const checkVisibility = () => {
if (isLoaded) return;
const rect = imgRef.current.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
// 图片顶部进入视口或底部进入视口
if (rect.top <= windowHeight && rect.bottom >= 0) {
setImageSrc(src);
setIsLoaded(true);
}
};
// 初始检查
checkVisibility();
// 添加滚动监听
window.addEventListener('scroll', checkVisibility);
return () => {
window.removeEventListener('scroll', checkVisibility);
};
}, [src, isLoaded]);
return <img ref={imgRef} src={imageSrc} alt={alt} />;
}- 方式二:自行封装(新 IntersectionObserver API)
js
import React, { useRef, useEffect, useState } from 'react';
export default function LazyImage({ src, placeholder = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', alt = '' }) {
const imgRef = useRef(null);
const [imageSrc, setImageSrc] = useState(placeholder);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// 图片进入视口,加载图片
setImageSrc(src);
observer.unobserve(imgRef.current);
}
});
},
{ threshold: 0.1 }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => {
if (imgRef.current) {
observer.unobserve(imgRef.current);
}
};
}, [src]);
return <img ref={imgRef} src={imageSrc} alt={alt} />;
}- 方式三:使用现成库(最简单)
js
import LazyLoad from 'react-lazyload';
function MyComponent() {
return (
<LazyLoad height={200} offset={100}>
<img src="https://example.com/your-image.jpg" alt="示例图片" />
</LazyLoad>
);
}http 优化
开启 gzip/brotli 压缩
- 方式一:打包构建预压缩
js
// npm i -D compression-webpack-plugin
const CompressionPlugin = require('compression-webpack-plugin');
const zlib = require('node:zlib');
module.exports = {
webpack: {
plugins: {
add: [
// Gzip 压缩
new CompressionPlugin({
filename: '[path][base].gz',
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192, // 大于8KB才压缩
minRatio: 0.8
}),
// Brotli 压缩
new CompressionPlugin({
filename: '[path][base].br',
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8,
compressionOptions: { level: 11 }
})
]
}
}
};- 方式二:服务端开启
js
// express
const compression = require('compression');
const shrinkRay = require('shrink-ray-current');
const express = require('express');
const app = express();
// 先尝试 Brotli,再降级到 Gzip
app.use(shrinkRay({
brotli: { quality: 5 },
gzip: { level: 6 }
}));
// 或者只用 Gzip
// app.use(compression({ level: 6 }));
app.use(express.static('build'));
app.listen(3000);- 另外:最后,nginx 服务器配置
shell
# 静态预压缩文件
gzip_static on;
brotli_static on;
# 动态压缩
gzip on;
gzip_types text/css application/javascript image/svg+xml;
gzip_vary on;
brotli on;
brotli_types text/css application/javascript image/svg+xml;
brotli_vary on;- 注意 小文件(<8KB)压缩收益有限,可设置 threshold Brotli 压缩率更高但 CPU 消耗大,适合预压缩
缓存策略(强缓存 / 协商缓存)
HTML 不缓存或短时间缓存,其他静态资源长缓存
shell
location / {
expires 1h;
try_files $uri $uri/ /index.html;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
}CDN 分发资源
又名内容分发网络,极速:智能调度、边缘缓存、就近访问
- 作用:解决了传统网络中地域距离远、带宽受限等导致的访问延迟问题
- cdn 选择:如果你是中小企业可以使用 nginx 模拟 cdn 如果你需要 全球加速,其实更推荐用 Cloudflare、阿里云 OSS+CDN、腾讯云 CDN 等现成服务
