04-性能优化与最佳实践------04. 列表渲染优化 - key 与虚拟化-CSDN博客
上面这篇文章讲得详细一些
比如渲染1万条数据
直接渲染1万条数据,浏览器需要同时创建并维护1万个DOM节点,这会带来三大问题:内存占用飙升、首次渲染变慢、滚动时频繁重排重绘导致掉帧卡顿。有实测数据显示,传统方式渲染1万条数据初始加载约需1280ms 、内存占用约48MB ,而使用react-window后加载仅需32ms 、内存约3.2MB,差距非常明显。
为什么不加虚拟滚动会卡
- DOM节点爆炸:1万条数据=1万个DOM元素,浏览器对DOM数量很敏感,几千个就开始卡,上万直接可能白屏。
- 重排重绘频繁 :滚动时大量节点参与布局计算,帧率会掉到18fps左右,肉眼可见的不流畅。
- 内存压力大:所有节点长期驻留内存,垃圾回收压力剧增,移动端更容易崩溃。
react-window 是怎么解决的
react-window 核心思路是只渲染视口内可见的十几二十个节点 ,滚动时动态替换内容,DOM数量始终保持在极低水平(实测仅24个节点)。 它体积只有约3KB(gzip),无依赖,接入成本很低。
适用场景很明确:列表项结构简单、高度固定或可预估时,直接用 FixedSizeList 或 VariableSizeList 就够用;如果还要做无限滚动,可以配合 react-window-infinite-loader。
什么情况下可以不急着上
- 列表项本身很轻(纯文本、无复杂交互),且数据量在几百条以内,直接渲染通常没太大问题。
- 数据本来就是分页加载的,每页只展示20-50条,那就不需要虚拟滚动。
- 但如果数据一次性全量拿到、需要连续滚动查看,强烈建议用虚拟滚动,收益非常明显。
一句话总结:10000条全量渲染基本一定会卡,用 react-window 是性价比很高的优化手段;但优化前最好先实测确认瓶颈,别凭直觉优化。
1万条数据,如果没有使用分页或者react-window来处理的话,会造成性能的严重浪费,几百条数据就不需要了。

javascript
import { useCallback, useEffect, useRef, useState } from 'react'
import { List } from 'react-window'
import { fetchAllUsers } from '../services/api'
import './UserList.css'
const STATUS_OPTIONS = [
{ value: '', label: '全部状态' },
{ value: 'active', label: '🟢 活跃' },
{ value: 'inactive', label: '🔴 停用' },
{ value: 'pending', label: '🟡 待审核' },
]
const STATUS_CLASS = {
active: 'status-active',
inactive: 'status-inactive',
pending: 'status-pending',
}
// 列配置:宽度百分比 + 渲染函数
const COLUMNS = [
{ key: 'id', label: 'ID', width: 6, render: (u) => u.id },
{ key: 'name', label: '姓名', width: 12, render: (u) => u.name },
{ key: 'email', label: '邮箱', width: 18, render: (u) => u.email, className: 'mono' },
{ key: 'phone', label: '手机号', width: 12, render: (u) => u.phone, className: 'mono' },
{ key: 'gender', label: '性别', width: 6, render: (u) => u.gender },
{ key: 'age', label: '年龄', width: 6, render: (u) => u.age },
{ key: 'department', label: '部门', width: 10, render: (u) => u.department },
{ key: 'role', label: '角色', width: 8, render: (u) => u.role },
{
key: 'status', label: '状态', width: 8, render: (u) => (
<span className={`status-badge ${STATUS_CLASS[u.status]}`}>
{STATUS_OPTIONS.find((o) => o.value === u.status)?.label || u.status}
</span>
),
},
{ key: 'score', label: '积分', width: 6, render: (u) => u.score },
{ key: 'createTime', label: '注册时间', width: 8, render: (u) => u.createTime, className: 'mono' },
]
const ROW_HEIGHT = 44
const TABLE_HEIGHT = 560
// react-window v2 API:
// - rowComponent 接收 { index, style, ariaAttributes, ...rowProps展开 }
// - rowProps 的字段会直接合并到 props 上(不是嵌套)
function Row({ index, style, data }) {
const user = data[index]
if (!user) return null
return (
<div style={style} className="virtual-row">
{COLUMNS.map((col) => (
<div key={col.key} className={`virtual-cell ${col.className || ''}`} style={{ width: `${col.width}%` }}>
{col.render(user)}
</div>
))}
</div>
)
}
export default function UserList() {
const [list, setList] = useState([])
const [status, setStatus] = useState('')
const [keyword, setKeyword] = useState('')
const [searchInput, setSearchInput] = useState('')
const [loading, setLoading] = useState(false)
const listRef = useRef(null)
const loadData = useCallback(async () => {
setLoading(true)
try {
const data = await fetchAllUsers({ status, keyword })
setList(data)
listRef.current?.scrollToRow({ index: 0 })
} catch (err) {
console.error('加载用户列表失败:', err)
} finally {
setLoading(false)
}
}, [status, keyword])
useEffect(() => {
loadData()
}, [loadData])
const handleSearch = () => setKeyword(searchInput)
const handleReset = () => {
setSearchInput('')
setKeyword('')
setStatus('')
}
return (
<div className="user-list">
<div className="toolbar">
<h2>
用户管理
<span className="total-badge">共 {list.length.toLocaleString()} 条</span>
</h2>
<div className="filters">
<input
className="search-input"
placeholder="搜索姓名/邮箱/手机号..."
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
<select
className="filter-select"
value={status}
onChange={(e) => setStatus(e.target.value)}
>
{STATUS_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
<button className="btn btn-primary" onClick={handleSearch}>搜索</button>
<button className="btn" onClick={handleReset}>重置</button>
</div>
</div>
<div className="table-wrap">
{/* 固定表头 */}
<div className="virtual-header">
{COLUMNS.map((col) => (
<div key={col.key} className="virtual-header-cell" style={{ width: `${col.width}%` }}>
{col.label}
</div>
))}
</div>
{loading ? (
<div className="loading" style={{ height: TABLE_HEIGHT }}>加载中...</div>
) : list.length === 0 ? (
<div className="virtual-row virtual-empty" style={{ height: TABLE_HEIGHT }}>
暂无数据
</div>
) : (
/* react-window v2: List 组件 */
<List
listRef={listRef}
rowCount={list.length}
rowHeight={ROW_HEIGHT}
rowComponent={Row}
rowProps={{ data: list }}
overscanCount={5}
style={{ height: TABLE_HEIGHT, width: '100%' }}
/>
)}
</div>
<div className="footer-hint">
🚀 使用 react-window 虚拟滚动:只渲染可视区域内的 DOM,10000 条数据也能丝滑滚动
</div>
</div>
)
}