一个搜索框,卡到同事过来问我"你电脑是不是该换了"------我默默打开 Performance 面板,发现组件渲染了 37 次。

😅 第一次被性能问题暴击
事情要从一个搜索页面说起。
需求很简单:一个输入框,一个列表,一个分页。
我花了半天写完,自信满满地点开测试------输入"react",页面卡了 2 秒才出结果。
我当时没在意,以为是接口慢。
直到我用 React DevTools 的 Profiler 看了一眼,才发现一个惊悚的事实:
每次输入,整个列表 + 分页组件 + 甚至页脚的版权信息,全部重新渲染了一遍。
我的搜索框,每敲一个字母,全页 20 多个组件就集体"上班"一次。
🤯 罪魁祸首:父组件更新 = 子组件全量重渲染
先看一段简化后的代码,你是不是也这么写过:
jsx
function SearchPage() {
const [keyword, setKeyword] = useState("")
const [list, setList] = useState([])
return (
<div>
<SearchInput value={keyword} onChange={setKeyword} />
<UserList data={list} />
<Pagination total={list.length} />
</div>
)
}
看起来没毛病对吧?
但真相是:只要 keyword 变一次,UserList 和 Pagination 也会跟着重新渲染。
为什么?
因为 setKeyword 触发 SearchPage 重新执行,SearchPage 返回的新 JSX 里,<UserList> 和 <Pagination> 是全新的虚拟 DOM,React 的 diff 算法发现"旧的引用 ≠ 新的引用"------于是全部重渲。
这就是 React 新手最容易踩的坑:状态变了,整个函数组件重新跑,子组件全量跟着跑。
🧩 优化一:React.memo --- 最简单的无脑缓存
对于纯展示组件,用 React.memo 包一层就够了:
jsx
const UserList = React.memo(function UserList({ data }) {
console.log("UserList 渲染了 🙄")
return (
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
})
原理很简单:React.memo 会对 props 做浅比较 ,如果 props 没变,直接复用上一次的渲染结果。
加上这一层后,我的搜索框再打字,UserList 终于不会跟着刷了。
踩坑提醒 :
React.memo只做浅比较。如果你传了对象或数组作为 props,每次父组件重新执行时都会创建新引用,memo 就失效了。这时候需要配合useMemo。
🧩 优化二:useMemo --- 缓存计算结果
再看一个真实场景:列表数据需要做复杂过滤和排序。
jsx
function UserList({ data, filterText }) {
// 没优化:每次渲染都重新过滤 + 排序
const filtered = data
.filter(item => item.name.includes(filterText))
.sort((a, b) => a.age - b.age)
return (
<ul>
{filtered.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
)
}
如果 data 有 10000 条,每次父组件更新都重新过滤一遍------卡死。
用 useMemo 缓存:
jsx
const filtered = useMemo(
() => data
.filter(item => item.name.includes(filterText))
.sort((a, b) => a.age - b.age),
[data, filterText] // 只有这两个变了才重新计算
)
适用场景:大数据量过滤、排序、复杂数学计算。简单的加法就没必要了------useMemo 本身也有开销。
🧩 优化三:useCallback --- 缓存函数引用
这个坑我踩得最冤:
jsx
function SearchPage() {
const handleSearch = (value) => {
console.log("搜索:", value)
}
return <SearchInput onSearch={handleSearch} />
}
const SearchInput = React.memo(function SearchInput({ onSearch }) {
// onSearch 每次都是新函数 → React.memo 白写了
})
handleSearch 是每次渲染都在函数组件里重新定义的,所以它的引用每次都不同。
React.memo 比较 props 时发现 onSearch 变了------等于没优化。
修复方案:useCallback
jsx
const handleSearch = useCallback((value) => {
console.log("搜索:", value)
}, []) // 依赖数组为空,引用永远不变
记住 :
useCallback单独用没意义,它必须配合React.memo才有价值。

🧩 优化四:Key 选不对,列表整个重渲
很多人写列表渲染时图省事:
jsx
// ❌ 错误示范
{items.map((item, index) => <li key={index}>{item.name}</li>)}
用 index 作为 key 的后果:
- 列表顺序变了 → React 认错元素 → 复用错误的 DOM
- 列表增删了 → 所有元素 key 都变了 → 整个列表销毁重建
- 如果有输入框或选中状态 → 状态错乱
正确的做法:
jsx
// ✅ 用唯一 id
{items.map(item => <li key={item.id}>{item.name}</li>)}
如果接口没给 id,用
uuid或者自己维护一个自增 ID。总之------永远不要用 index。
🔥 终极方案:虚拟列表
如果你的列表动辄上万条,前面的优化都治标不治本。
正确的思路是:只渲染用户能看到的那几行。
用 react-window 实现:
bash
npm install react-window
jsx
import { FixedSizeList } from "react-window"
function VirtualUserList({ data }) {
return (
<FixedSizeList
height={600}
itemCount={data.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>{data[index].name}</div>
)}
</FixedSizeList>
)
}

效果:10000 条数据,只渲染屏幕上的 12 行,滚动时动态替换。
真实案例:我在一个后台管理系统中,把用户列表从渲染 5000 个 DOM 节点降到了 30 个,页面加载时间从 3.2s 降到了 0.4s。
📌 总结一下这 4 个优化技巧
| 技巧 | 解决什么问题 | 使用时机 |
|---|---|---|
React.memo |
子组件不必要的重复渲染 | 纯展示组件,props 基本不变 |
useMemo |
复杂计算重复执行 | 大数据量过滤、排序 |
useCallback |
函数引用变化破坏 memo | 配合 React.memo 使用 |
| 正确 key | 列表渲染错乱 | 所有列表渲染 |
| 虚拟列表 | 海量数据 DOM 过多 | 上千条的长列表 |
我给团队定了一条规矩:所有列表页面,默认上 react-window + React.memo。
不是因为一定要优化到极致,而是养成习惯后,你永远不会被性能问题半夜叫醒。
🗣 最后
你有没有遇到过"明明代码没写错,但页面就是卡"的经历?
欢迎在评论区分享你的踩坑故事------你用什么手段查出来的?用了什么方案解决?
📌 完整源码
本文所有示例代码都在 github.com/example/rea...,欢迎 star 和交流。