vue v-for 渲染大量数据卡顿的优化方案

news/2024/7/19 13:44:23 标签: vue.js, 前端, javascript, js, 前端框架

vue 中使用 v-for 渲染大量数据的优化方案

前端中我们难免会遇到需要展示大量数据的情况,如果基础数据量过大,那么在初始化组件时,可能会造成严重卡顿,影响用户体验。在我参与的开源项目 swanlab 中,某些情况下需要大量渲染程序日志,最近对其进行了一下优化,将使用方案记录于此。

swanlab 是我们团队开源的一款关于人工智能训练相关的工具,包含且不限于日志记录、实验数据图表、多实验对比图表、硬件信息记录等众多功能。
实验日志相关页面位于 这里。
你可以 fork 项目查看相关代码,也欢迎提 issue 和 pr

接下来就以渲染大量日志为例,说一下我的解决方案。

这是相关页面的前端效果:
在这里插入图片描述

一、解决思路

浏览器的解析、渲染操作都是在主线程中完成的,而当一次性需要解析的 dom 树过多则会导致一系列问题,原理可以看 这篇博客 - JS执行过程与浏览器渲染原理 ,总之,结果就是卡顿。

所以自然而然会想到,如果我减少渲染量,即减少渲染主线程一次任务的工作量,是不是就能缓解卡顿?那怎么减少渲染量呢?

可以想到几个粗略的方案:

  1. 加载方案
    刚进入页面时,发起初始化请求拿到日志数据,这个日志数据可多可少,但是在初始化中,将所有日志数据都保存在 origin_data 中,仅取出合适大小的一部分放到 render_data 中,使用 v-for="item in render_data" 渲染限定部分,当用户滚动到最底部或者最顶部(取决于你的组件是怎么个渲染交互逻辑),触发加载,将 origin_data 中的下一部分加载到 render_data 中,如果缓存的数据加载完了就继续请求就行。
    但是有个缺点,滚动起来不够流畅,得做好加载过渡(不过再怎么过渡,用户体验的瓶颈在那)。

  2. 分页方案
    分成多页,简单粗暴,操作简单,唯一麻烦的地方就是要用分页器组件,如果定制化要求比较高,自己写起来还得费点功夫。

上面两种方法理解起来比较简单,但是这篇博客想说的是参考 wandb 日志加载方案后的另一种方法:动态渲染视窗部分及附近的区域

二、动态渲染

如果日志总共有数万条,怎么保证在每个渲染主线程中的任务开销在可控范围?我们可以只渲染视窗及其上下部分,在滚动时计算应该在新的窗口位置渲染的部分。

画一个简单的模型图,也可以结合 代码 中的标签层级理解:

在这里插入图片描述

日志窗口

日志窗口是日志展示部分的最外层容器,在我的项目中给其固定了宽高,如果你的项目中不需要固定宽高,而是直接让浏览器窗口出现滚动条,那么只需要更改后续滚动事件的绑定位置即可,思路是一样的。总之,他的作用就是当“日志区域”高度大于该容器高度时,自动显示纵向的滚动条。

日志区域

对于日志区域,不限制高度,即有多少日志就撑开多高。但是我们仅渲染部分在视窗及其附近区域的日志,怎么能让日志区域的高度为所有日志都渲染时的高度?这个就需要动态计算,在获取到日志行高后,同时获取日志条数,计算得到总高度。

日志渲染区域

日志渲染区域是最重要的一部分,我们在这个元素中使用 v-for
在我的例子中,页面初始化的时候就已经获取到了全部日志数据。为了渲染,需要有一个计算函数,这个函数需要做这么一些工作:

  • 获取行高 (如果行高固定可以写死,但是考虑到缩放等等因素,可以从dom动态获取行高)
  • 计算视窗部分可以展示多少条数据:视窗部分高度 / 行高 => Math.ceil(e.clientHeight / line_height)
  • 视窗顶部,第一条日志的索引:滚动条到容器顶部距离 / 行高 => Math.floor(e.scrollTop / line_height)
  • 视窗底部,最后一条日志的索引:将上面两个加起来即可

我们肯定不能仅仅只渲染看得到的部分,还需要渲染一部分看不到的但临近视窗的部分,这样用户在滚动时,即使计算稍慢 (在我的代码中,给滚动事件添加了一个 100ms 的防抖),也能保持流畅的渲染,不至于一滚动就出现空白部分。至于留多少条的范围,可以自己设定合理的条数,这里我们额外使用一个变量 addition 保存在视窗上下分别额外渲染的条数。

那么在计算的时候:

  • 真正需要渲染的第一条日志索引为:视窗中第一条日志的索引 - 额外向上渲染的条数 => Math.floor(e.scrollTop / line_height) - addition
  • 真正需要渲染的第一条日志索引为:视窗中第一条日志的索引 + 视窗中能渲染的条数 + 额外向下渲染的条数 => Math.floor(e.scrollTop / line_height) + Math.ceil(e.clientHeight / line_height) - addition

其实实现起来也不困难:

js">// 这个 debounce 是自己实现的防抖函数
const handleScroll = debounce((event) => {
  const e = event.target
  computeRange(e)
}, 100)

// 计算 log 的渲染范围
const computeRange = (e) => {
  const line_height = lineHeight.value
  // 计算应该渲染多少条数据
  const pageSize = Math.ceil(e.clientHeight / line_height)
  // 计算第一条 log 的索引
  const startIndex = Math.floor(e.scrollTop / line_height)
  // 计算最后一条 log 的索引
  const endIndex = startIndex + pageSize

  // 如果距离顶部很近,把顶部到第一条中的log也渲染了
  if (e.scrollTop <= addition * line_height) range.value[0] = 0
  // 如果距离顶部比较远,仅渲染第一条上的 addition 条 log
  else range.value[0] = startIndex - addition

  // 如果距离底部很近,把最后一条到底部的log也渲染了
  if (e.scrollTop + e.clientHeight >= e.scrollHeight - addition * line_height) range.value[1] = lines.value.length - 1
  // 如果距离底部较远,仅渲染最后一条下的 addition 条 log
  else range.value[1] = endIndex + addition
}

至于这个函数绑定到哪:哪儿个元素负责滚动条,就给他绑定滚动事件 @scroll="handleScroll",所以很明显,上面的计算函数接收的第一个参数是滚动事件对象对应的 dom 实例。

渲染区定位

看到这,我们大概知道层级结构,但是有个关键问题没有解决:渲染日志区域只有那么大,怎么让他一直定位到日志窗口中心?

在这里,我们可以将日志区域设置为相对定位,再给日志渲染区域设置为绝对定位,并且使用计算属性实时计算日志渲染区域应该距离日志区域顶部多远:整个渲染区域第一条日志的索引 * 行高

三、结尾

具体实现代码可见 swanlab - LogPage,欢迎体验或参与该项目。


http://www.niftyadmin.cn/n/5409953.html

相关文章

QJsonValue的学习

类型判断&#xff1a; QJsonValue v("1");QJsonValue v1(1);qDebug()<<v.isString();//trueqDebug()<<v.isBool();//falseqDebug()<<v.isDouble();//falseqDebug()<<v1.isString();//falseqDebug()<<v1.isBool();//falseqDebug()<…

安卓类加载机制

目录 一、ClassLoader介绍二、双亲委托机制三、类的加载过程 一、ClassLoader介绍 任何一个 Java 程序都是由一个或多个 class 文件组成&#xff0c;在程序运行时&#xff0c;需要将 class 文件加载到 JVM 中才可以使用&#xff0c;负责加载这些 class 文件的就是 Java 的类加…

Golang-channel合集——源码阅读、工作流程、实现原理、已关闭channel收发操作、优雅的关闭等面试常见问题。

前言 面试被问到好几次“channel是如何实现的”&#xff0c;我只会说“啊&#xff0c;就一块内存空间传递数据呗”…所以这篇文章来深入学习一下Channel相关。从源码开始学习其组成、工作流程及一些常见考点。 NO&#xff01;共享内存 Golang的并发哲学是“要通过共享内存的…

电子电器架构刷写策略 —— 队列刷写

电子电器架构刷写策略 —— 队列刷写 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明自己…

线性代数笔记10--矩阵的四个基本子空间

0. 引入 矩阵 A m n A_{m \times n} Amn​ 1. 列空间 C ( A ) C(A) C(A)在 R m R^m Rm中 d i m ( C ( A ) ) p i v o t _ c o l u m n _ c n t r a n k ( A ) r dim(C(A))pivot\_column\_cnt rank(A)r dim(C(A))pivot_column_cntrank(A)r 2. 零空间 N ( A ) N(A) N(A)…

C#,数值计算,求解微分方程的预测校正法(修正欧拉法)算法与源代码

Leonhard Euler 1 微分方程 微分方程&#xff0c;是指含有未知函数及其导数的关系式。解微分方程就是找出未知函数。 微分方程是伴随着微积分学一起发展起来的。微积分学的奠基人Newton和Leibniz的著作中都处理过与微分方程有关的问题。微分方程的应用十分广泛&#xff0c;可…

怎么判断晶振是否起振?晶振不起振该怎么办?

如果怀疑晶振不起振造成电路板上电不良&#xff0c;该如何进一步判定是晶振本身的不良呢?这一步的判定非常关键&#xff0c;因为若为晶振不振&#xff0c;就可以排除晶振与电路板不匹配造成电路板上电不良发生的假定。晶发电子以下介绍针对晶振单体判定的方法&#xff1a; 1.…

算法46:动态规划专练(力扣198: 打家劫舍 力扣740:删除并获取点数)

打家劫舍问题&#xff1a; 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被小偷闯入&#xff0c;系统会自动报警。 给定…