react里面的长列表渲染优化

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),无依赖,接入成本很低。‌‌

适用场景很明确:列表项结构简单、高度固定或可预估时,直接用 FixedSizeListVariableSizeList 就够用;如果还要做无限滚动,可以配合 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>
  )
}
相关推荐
小羊没烦恼!1 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰1 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi3 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen3 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马4 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒4 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端
乘风gg5 小时前
DeepSeek V4.1 Flash 来了,明天中午 Flash 降价 60%
前端·ai编程·claude
默_笙5 小时前
❗ 给金鱼装记忆(上):从白板到笔记本,AI 的短期记忆生存指南
前端·javascript
酷酷的逗逗乐5 小时前
前端转 Agent 开发 · 第五节:Memory 会话记忆
前端·程序员