React无react-table时大表格如何用useMemo优化排序性能
React自定义表格组件大数据量排序卡顿优化方案
问题描述
- 未依赖react-table的自研React表格组件,绑定数据集规模超过2000行
- 自行实现的过滤、排序逻辑每次操作耗时1-2秒,Chrome下存在明显卡顿,控制台输出
[Violation] 'setTimeout' handler took 1458ms性能警告,相同代码在Firefox下运行流畅 - 尝试使用
const test = useMemo(() => handleHeaderClick, [props.data])做优化时,直接导致功能失效
原始问题代码
export default function Table(props) { const [order, setOrder] = useState(true); function handleHeaderClick(clickedHeader) { setOrder(!order); const newdata = props.data.sort((a, b) => { const x = a[clickedHeader]; const y = b[clickedHeader]; if (order && x > y) return -1; return 1; }); props.setdata(newdata); } function createHeader(header, index) { if (header.includes("BET") || header.includes("C") || header.includes("CHECK")) { return ( <th onClick={(e) => handleHeaderClick(header)} key={index}> {header} </th> ); } } function createRow(row) { // ♠♥♦♣ try { return row.replace(/s/g, "♠").replace(/d/g, "♦").replace(/c/g, "♣"); } catch (e) { return row; } } function tableRow(row, indexRow) { return ( <tr key={indexRow}> {Object.keys(row).map((hd) => { const value = createRow(props.data[indexRow][hd]); if (hd.includes("BET") || hd.includes("C") || hd.includes("CHECK")) { return ( <td className={value[1]} key={hd}> {value} </td> ); } })} </tr> ); } return ( <table> <thead> <tr>{Object.keys(props.data[0]).map((header, indexHeader) => createHeader(header, indexHeader))}</tr> </thead> <tbody>{props.data.map((row, indexRow) => tableRow(row, indexRow))}</tbody> </table> ); }
核心问题根因
Array.sort是原地修改方法,直接改动原数组引用,会触发React全量不必要重渲染;原逻辑把排序操作放在点击事件里直接修改父组件传入的data状态,每次排序都触发整棵组件树重跑- 之前写的useMemo用法完全错误:既没有执行逻辑也没有缓存计算结果,只是缓存了一个未调用的函数引用,自然会导致功能失效
- 行渲染没有任何缓存策略,每次状态变更2000+行全量重绘,Chrome对主线程长任务的阻塞判定阈值比Firefox更严格,所以会出现明显卡顿和性能警告
- 排序逻辑本身有bug:
setOrder是异步批量更新的,点击事件里拿到的order是上一次渲染的旧值,排序方向会和预期不一致 - 渲染阶段重复执行大量相同计算:每个单元格渲染时都重复做字段匹配判断、字符串replace替换,重复开销极大
正确优化实现
1. 用useMemo缓存派生数据,不直接修改原状态
不要在点击事件里直接改原数组,把排序字段、排序方向存在本地状态,排序后的数据、预处理的展示数据全部用useMemo缓存,仅在依赖项变化时重新计算:
import { useState, useMemo, useCallback, memo } from 'react'; export default function Table(props) { // 同时存储排序字段和方向,用函数式更新避免拿到旧状态 const [sortConfig, setSortConfig] = useState({ key: null, ascending: true }); // 缓存需要展示的列,仅在数据结构变化时重新计算 const visibleHeaders = useMemo(() => { if (!props.data?.length) return []; return Object.keys(props.data[0]).filter(header => header.includes("BET") || header.includes("C") || header.includes("CHECK") ); }, [props.data]); // 缓存排序结果,不修改原数组 const sortedData = useMemo(() => { const dataToSort = [...props.data]; if (!sortConfig.key) return dataToSort; return dataToSort.sort((a, b) => { const valA = a[sortConfig.key]; const valB = b[sortConfig.key]; // 区分数字、字符串比较逻辑,避免隐式转换bug if (typeof valA === 'number' && typeof valB === 'number') { return sortConfig.ascending ? valA - valB : valB - valA; } return sortConfig.ascending ? String(valA).localeCompare(String(valB)) : String(valB).localeCompare(String(valA)); }); }, [props.data, sortConfig]); // 提前预处理单元格格式,不要在渲染阶段重复做字符串替换 const formattedData = useMemo(() => { return sortedData.map(row => { const processedRow = {...row}; visibleHeaders.forEach(key => { const val = processedRow[key]; if (typeof val === 'string') { processedRow[key] = val .replace(/s/g, "♠") .replace(/h/g, "♥") .replace(/d/g, "♦") .replace(/c/g, "♣"); } }); return processedRow; }) }, [sortedData, visibleHeaders]);
2. 用useCallback缓存事件函数,避免不必要的重渲染
const handleHeaderClick = useCallback((clickedHeader) => { setSortConfig(prev => ({ key: clickedHeader, // 点击同列切换排序方向,点击新列默认升序 ascending: prev.key === clickedHeader ? !prev.ascending : true })); }, []);
3. 抽离表头、行组件,用memo缓存避免全量重绘
把内联的渲染逻辑抽成独立组件,用memo包裹,仅当组件入参变化时才重渲染,避免2000行每次状态更新全量重绘:
// 抽离表头组件 const TableHeader = memo(function TableHeader({ headers, onSort, activeKey, isAscending }) { return ( <tr> {headers.map(header => ( <th key={header} onClick={() => onSort(header)} style={{cursor: 'pointer'}} > {header} {activeKey === header ? (isAscending ? ' ↑' : ' ↓') : ''} </th> ))} </tr> ) }); // 抽离单行组件 const TableRow = memo(function TableRow({ row, headers }) { return ( <tr> {headers.map(hd => { const cellValue = row[hd]; return ( <td className={cellValue?.[1] || ''} key={hd}> {cellValue} </td> ) })} </tr> ) });
4. 最终返回逻辑
if (!props.data?.length) return <div>暂无数据</div>; return ( <table> <thead> <TableHeader headers={visibleHeaders} onSort={handleHeaderClick} activeKey={sortConfig.key} isAscending={sortConfig.ascending} /> </thead> <tbody> {formattedData.map((row, idx) => ( <TableRow // 优先用数据唯一ID当key,不要用数组索引,减少diff开销 key={row.id || idx} row={row} headers={visibleHeaders} /> ))} </tbody> </table> ) }
进阶可选优化
如果后续数据量涨到1万行以上仍有卡顿,可以追加两个优化:
- 加虚拟滚动,仅渲染视口内可见的行,常用轻量库为
react-window,不需要引入全量表格组件库 - 把排序、过滤等重计算逻辑移到Web Worker中执行,完全不阻塞主线程
内容的提问来源于stack exchange,提问作者Collaxd
相关产品推荐
相关产品推荐

