当前位置: 首页 > news >正文

TinyVue表格重构性能优化详解

本文由体验技术团队岑灌铭原创。

前言

表格作为组件库高频使用的组件,它作为承载、展示和交互数据的核心载体,每一次卡顿都可能意味着时间的浪费与耐心的消磨。
然而有小伙伴反馈说,表格组件树表数据滚动场景卡顿,偶尔会出现白屏现象,甚至会出现表头和表体滚动不同步的情况。
后来据了解是小伙伴的机器性能较为普通,加上表格树表大数据虚拟滚动确实存在较大的性能瓶颈,存在大量的计算与dom操作。
问题在性能较好的机器上被“屏蔽”了。
请添加图片描述

  1. 打开 performance 面板
  2. 点击面板内右上角设置按钮
  3. 在 CPU 设置选项中点击弹出下拉面板
  4. 选择需要降低的 CPU 倍率

在降低 CPU 性能 4x 减速后,再使用官网中的树表大数据demo体验滚动,用户反馈的问题也成功地“轻松”复现了。

请添加图片描述

白屏与滚动不同步的表格滚动体验实在是太差了,因此我们决定针对表格滚动场景做一个专项优化,解决这一大痛点。

使用performance工具分析滚动场景下任务耗时,发现其中hasRowChangerenderColumn比较耗时。

其中hasRowChange是动态计算表格中单元格有无发生数据变化。

renderColumn 就是渲染表格行和列了,展开后发现是其中vue更新组件耗时,其中涉及dom元素的创建和销毁,事件的处理等。

请添加图片描述

针对已有分析的问题,主要对表格做了如下重构:

主要重构点

单元格事件委托

重构前:

在之前的实现中,每个单元格(cell)都单独绑定了自己的事件监听器,并在单元格内容更新或页面滚动导致单元格变化时,频繁地进行事件监听器的绑定和销毁操作。这种做法在处理大数据量表格时,尤其是在滚动过程中动态更新单元格内容的情况下,占用了大量的资源。

重构后:

为了优化这一情况,采用了事件委托的技术方案;将所有单元格的事件处理器统一绑定到外层表格(table)上,利用事件冒泡机制,仅保留一个全局的事件处理器。无论表格中有多少个单元格,或是单元格如何动态变化,都不需要再对每个单元格单独管理事件监听器。相反,通过在外层表格上捕获并分发事件,我们可以显著减少因频繁绑定和销毁事件监听器带来的资源消耗,从而提升整体应用性能

关键代码修改:

将单元格事件处理逻辑使用composition-api抽离到packages\vue\src\grid\src\composable\useCellEvent.ts

详细的处理逻辑可以查看具体文件

const bindMouseEvents = (target) => {on(target, 'mouseenter', handleMouseEnter, true)on(target, 'mouseleave', handleMouseLeave, true)on(target, 'mousedown', handleMouseDown, true)on(target, 'click', handleClick, true)on(target, 'dblclick', handleDoubleClick, true)}
// ...省略其他代码
hooks.watch(table, (table, old) => {if (isBound && old) {unbindMouseEvents(old)isBound = false}if (!isBound && table) {bindMouseEvents(table)isBound = true}})// ...省略其他代码

空数据展示优化

重构前:

之前空数据居中展示,需要通过复杂的JS逻辑计算,保存表格的表体高度(表格整体高度减去表头高度),再通过js给空数据赋值对应高度,使其能够适配表体高度。期间会读取到clientHeight触发不必要的回流。

重构后:

重构后的空数据居中展示,使用纯css方案,使用sticky粘性定位加高度自适应实现,避免JS逻辑计算。

关键代码修改:

// packages\vue\src\grid\src\table\src\methods.ts
// 删除updateTableBodyHeight方法updateTableBodyHeight() {if (!this.tasks.updateTableBodyHeight) {this.tasks.updateTableBodyHeight = () => {fastdom.measure(() => {const tableBodyElem = this.elemStore['main-body-wrapper']this.tableBodyHeight = tableBodyElem ? tableBodyElem.clientHeight : 0})}}this.tasks.updateTableBodyHeight()},
// packages\theme\src\grid\table.less
// 改用sticky定位& &__empty-block {height: 100%;min-height: 60px;padding: 60px 0;display: flex;align-items: center;justify-content: center;text-align: center;position: sticky;left: 0;flex: auto;flex-direction: column;}

表头、表体、表尾三合一。

重构前:

在过去的版本中,为了实现表头固定显示在顶部,将表头、表体、表尾拆分为了三个表格,使滚动条仅出现在表体中从未达到表头固定展示的效果。当滚动横向滚动条时,通过js逻辑同步设置表头。表尾的滚动位置,达到三者同步效果。在性能不佳的机器上,大数据滚动场景会出现表头表尾不同步的问题。

重构后:

最新版本的表格目前将表头、表体、表尾都合并到了一个table中,同样使用sticky定位去实现表头固定显示在顶部,删除滚动同步逻辑。由于三者位于同一滚动容器中,因此不会出现滚动不同步问题。

关键代码修改:

// packages\vue\src\grid\src\header\src\header.ts
function renderHeaderTable(args) {// ...省略其他代码return h('table',{class: 'tiny-grid__header',style: { tableLayout },attrs: { cellspacing: 0, cellpadding: 0, border: 0 },ref: 'table'},[// 列宽renderTableColgroup(tableColumn),// 头部renderTableThead(args1)])
}// packages\vue\src\grid\src\body\src\body.tsx
function renderTable({ $table, _vm, tableColumn, tableData, tableLayout }) {return h('table',{class: 'tiny-grid__body',style: { tableLayout },attrs: { cellspacing: 0, cellpadding: 0, border: 0 },ref: 'table'},[// 渲染colgroup标签,设置表格列宽度,保证表头的表格和表体的表格每列宽相同h('colgroup',{ ref: 'colgroup' },tableColumn.map((column, columnIndex) => h('col', { attrs: { name: column.id }, key: columnIndex }))),h('tbody', { ref: 'tbody' }, renderRows({ h, _vm, $table, $seq: '', rowLevel: 0, tableData, tableColumn }))])
}// packages\vue\src\grid\src\footer\src\footer.tsrender() // ...省略其他代码 return h('div',{class: ['tiny-grid__footer-wrapper', 'body__wrapper'],on: { scroll: this.scrollEvent }},[h('div', { class: 'tiny-grid-body__x-space', ref: 'xSpace' }),typeof renderFooter === 'function'? renderFooter(renderParams, h): h('table',{class: 'tiny-grid__footer',style: { tableLayout },attrs: tableAttrs,ref: 'table'},[//  列宽colgroupVNode,// 底部tfootVNode])])},
// packages\vue\src\grid\src\body\src\body.tsx
// heder、body、footer三者再同一个table中
const tableVnode = (<tableref="table"class="tiny-grid__body"style={{ tableLayout, width: bodyTableWidth ? `${bodyTableWidth}px` : undefined }}cellspacing={0}cellpadding={0}border={0}data-tableid={$table.id}>{[// 列分组(用于指定列宽)<colgroup ref="colgroup">{columnPool.map(({ id, item: column, used }) => {return (<colkey={id}name={column.id}width={String(column.renderWidth)}style={{ display: used ? undefined : 'none' }}/>)})}</colgroup>,// 表头$table.showHeader ? <thead ref="thead">{renderHeaderRows(_vm)}</thead> : null,// 表体内容<tbody ref="tbody">{renderRows(_vm)}</tbody>,// 表尾$table.showFooter && !isNoData && typeof $table.renderFooter !== 'function' ? (<tfoot ref="tfoot">{renderFooterRows(_vm)}</tfoot>) : null]}</table>)

增加数据缓存,以空间换时间

重构前:

在之前的实现中,每次滚动时都会对每个单元格的状态进行重新计算,例如判断单元格数据是否发生变化(dirty check),这些操作涉及大量的计算资源,进一步加重了主线程的负担。频繁的重新计算和大对象查询导致了滚动等其他逻辑执行缓慢,引起卡顿问题。

重构后:

引入了缓存机制。在第一次渲染时,将每个单元格的 dirty 状态存储在一个小型缓存表中。这样,在后续的滚动过程中,无需再进行重复的状态计算或从原始大对象中查询数据,而是直接在缓存表中快速查找所需信息。通过这种方式,以空间换时间,减少不必要的计算开销。

关键代码修改:

将单元格状态相关逻辑使用composition-api抽离到packages\vue\src\grid\src\composable\useCellStatus.ts

将数据相关缓存抽离到packages\vue\src\grid\src\composable\useData.ts

// packages\vue\src\grid\src\composable\useCellStatus.ts
export const getCellStatus = ($table, row, column) => {const cellKey = getCellKey($table, row, column)const map = $table.cellStatusif (map.has(cellKey)) {return map.get(cellKey)} else {return { isDirty: false }}
}
// packages\vue\src\grid\src\composable\useData.ts
const structure = ({ array, stack, tiled, map, customMappings, getID, childrenKey, sizeKey }) => {if (!Array.isArray(array)) {return}const level = stack.lengthconst nodes = []for (let i = 0; i < array.length; i++) {const item = array[i]const node = {id: getID(item) || ++nid,payload: item,path: [...stack, item],level,parentNode: level > 0 ? map.get(stack[stack.length - 1]) : undefined,childNodes: undefined,space: { originDistance: 0, size: item[sizeKey] || 36 },mappings: customMappings ? Object.assign({}, customMappings({ payload: item, viewIndex: tiled.length })) : {}}tiled.push(node)map.set(item, node)nodes.push(node)if (childrenKey) {stack.push(item)node.childNodes = structure({array: item[childrenKey],stack,tiled,map,customMappings,getID,childrenKey,sizeKey})stack.pop()}}return nodes
}

其他更新

  • 特性增强:表格支持跨冻结列合并
  • 优化渲染机制,减少表格内组件重新 render 次数
  • 优化列配置收集,列配置收集完成后再渲染真实表格,解决表格初始化渲染高度过大问题。

验证性能提升

测试环境信息:

  • 浏览器:chrome 版本 138.0.7204.158 无痕模式
  • 操作系统: win10 专业版
  • 处理器: Intel® Xeon® Gold 6278C CPU
  • 内存: 32GB

本次性能测试的 demo 是官网中已有的树表虚拟滚动:https://opentiny.design/tiny-vue/zh-CN/os-theme/components/grid-large-data#large-data-grid-large-tree-data

先来看看直观感受

重构前:

请添加图片描述

可以明显的看到,当纵向和横向快速滚动时,都会出现白屏现象。且滚动条也较为卡顿。
重构后:

请添加图片描述

重构后,无论是横向亦或是纵向快速滚动,都不会出现白屏现象,滚动效果也相对丝滑。

利用 performance 记录一下大数据树表虚拟滚动初始化的情况。

请添加图片描述

重构前的内存占用约为 37.2M (832KB 应为 chrome 初始化必需内存),代码执行时间为 981ms。

请添加图片描述

重构后的内存占用约为 27M,代码执行时间为 552ms。

小结:
大数据树表虚拟滚动初始化场景,内存节省27%, js 执行时间减少43%,另外渲染和绘制时间也有小幅提升。

再来记录一下滚动场景:
对 Demo 进行以下改造,点击按钮后, 在 3 秒内对表格进行横向滚动6000px

// template add code
<tiny-button @click="handleScroll">开始滚动</tiny-button>// ...省略其他代码// script add code
const grid = ref();
const handleScroll = () => {const now = Date.now();let frameCount = 0;const doScroll = () => {const time = Date.now() - now;requestAnimationFrame(() => {frameCount++;if (time <= 3000) {grid.value.scrollTo(time * 2);doScroll();} else {console.log(`滚动结束:平均帧率为${frameCount / 3}FPS`);}});};doScroll();
};

先使用 chrome performance 工具记录,然后再点击按钮

重构前后数据对比如下:

请添加图片描述

请添加图片描述

重构前:FPS 约为20FPS,js 执行时间为2288ms

重构后:FPS 约为44FPS,js 执行时间为1302ms

请添加图片描述

重构后单个任务中,已经消除了状态计算的逻辑,renderColumn任务也从40ms降低为15ms

小结:
大数据树表虚拟滚动横向场景下,FPS 提升110%, js 执行时间减少43%

总结

主要优化点:

  • 事件委托:将单元格事件统一绑定到表格外层,减少事件监听器频繁绑定/销毁
  • CSS 优化:空数据展示改用纯 CSS 方案,避免 JS 计算和回流
  • 结构简化:表头、表体、表尾合并为单一表格,使用 sticky 定位,消除滚动同步问题
  • 缓存机制:增加数据缓存,以空间换时间,减少重复计算
  • 滚动优化:分离滚动处理与数据剪切逻辑,提高响应速度

中等配置电脑下性能提升如下:

初始化场景:内存节省 27%,JS 执行时间减少 43%
滚动场景:FPS 提升 110%(20FPS → 44FPS),JS 执行时间减少 43%

关于 OpenTiny

欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~

OpenTiny 官网:https://opentiny.design
OpenTiny 代码仓库:https://github.com/opentiny
TinyVue 源码:https://github.com/opentiny/tiny-vue
TinyEngine 源码: https://github.com/opentiny/tiny-engine
欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI、TinyEditor~
如果你也想要共建,可以进入代码仓库,找到 good first issue 标签,一起参与开源贡献~

http://www.lryc.cn/news/618150.html

相关文章:

  • 从基础编辑器到智能中枢:OpenStation 为 VSCode 注入大模型动力
  • 人工智能+虚拟仿真,助推医学检查技术理论与实践结合
  • MySQL 索引:索引为什么使用 B+树?(详解B树、B+树)
  • 零知开源——基于STM32F407VET6和INA219的功率监测器设计与实现
  • ZKmall开源商城的容灾之道:多地域部署与故障切换如何守护电商系统
  • 【新启航】从人工偏差到机械精度:旋转治具让三维扫描重构数据重复精度提升至 ±0.01mm
  • 解决 HTTP 请求 RequestBody 只能被读取一次的问题
  • 医美产业科技成果展陈中心:连接微观肌肤世界与前沿科技的桥梁
  • 【机器学习】什么是DNN / MLP(全连接深度神经网络, Deep Neural Network / Multilayer Perceptron)?
  • 01. maven的下载与配置
  • http网页部署
  • 微算法科技(NASDAQ:MLGO)开发经典增强量子优化算法(CBQOA):开创组合优化新时代
  • 聆思duomotai_ap sdk适配dooiRobot
  • 基于SpringBoot的课程作业管理系统
  • 【论文阅读】从表面肌电信号中提取神经信息用于上肢假肢控制:新兴途径与挑战
  • iOS 签名证书全生命周期实战,从开发到上架的多阶段应用
  • 数据可视化交互深入理解
  • 论文阅读:Agricultural machinery automatic navigation technology
  • 【论文阅读】RestorerID: Towards Tuning-Free Face Restoration with ID Preservation
  • LeetCode 分割回文串
  • 增加vscode 邮件菜单
  • 论文阅读(九)Locality-Aware Zero-Shot Human-Object Interaction Detection
  • Openlayers基础教程|从前端框架到GIS开发系列课程(24)openlayers结合canva绘制矩形绘制线
  • iOS 签名证书实践日记,我的一次从申请到上架的亲历
  • Docker-10.Docker基础-自定义镜像
  • 医疗矫正流(MedRF)框架在数智化系统中的深度应用
  • 无人机在环保监测中的应用:低空经济发展的智能监测与高效治理
  • 云平台监控-云原生环境Prometheus企业级监控实战
  • .NET MAUI框架编译Android应用流程
  • 计算机视觉(7)-纯视觉方案实现端到端轨迹规划(思路梳理)