列表首屏毫秒级加载与自动滚动定位方案

news/2024/7/10 1:59:48 标签: vue, javascript, 性能优化

引用自 摸鱼wiki

场景

<template>
	<div ref="commentsRef">
		<div
			v-for="comment in displayComments"
			:key="comment.id"
			:data-cell-id="comment.id"
			class="card"
		>
			{{ comment.data }}
		</div>
	</div>
</template>

<script lang="ts" setup>javascript">
// 假设comment有2000条数据,首次只加载200条
const comments = ref<{ id: string; data: string }[]>();
const displayComments = computed(() => comments.slice(0, 200));
</script>

说明:需要一个场景,用户首屏需要渲染200条数据,且后续如果comments列表发生变动时,页面上能够保持当前显示的条目不进行滚动。

分批加载

如果直接全量数据加载,200个节点肯定不能在1帧内渲染完毕,这样会出现较长时间的白屏。这时候可以使用时间分片的思路,在每个小时间段内渲染少量节点,提高首屏的渲染效率。

<template>
	<div>
		<div
			v-for="comment in displayComments.slice(0, loadCount)"
			:key="comment.id"
		>
			{{ comment.data }}
		</div>
	</div>
</template>

<script lang="ts" setup>javascript">
// 假设comment有2000条数据,首次只加载200条
const commentsRef = ref();
const comments = ref<{ id: string; data: string }[]>();
const displayComments = computed(() => comments.slice(0, 200));

// 首屏动态加载的数量
const loadCount = ref<number>(0);
// 如果少于20条,直接全量加载;否则每隔4ms渲染40条数据
watch(displayComments, () => {
  clearInterval(timer);
  loadCount.value = 20;
  if (loadCount.value < nv.length) {
    timer = setInterval(() => {
      loadCount.value = Math.min(loadCount.value + 40, nv.length);
      if (loadCount.value >= nv.length) {
        clearInterval(timer);
      }
    }, 4);
  }
})
</script>

滚动定位

实现在列表数据更新时自动定位到用户当前可视区域,尽可能保证可视区域的内容不因数据更新而发生剧烈抖动。

获取当前可视内容

通过浏览器提供的 IntersectionObserver API 监听列表中的元素,获取元素滚动时在可视区域内的元素,记录他们的id。

const visibleCardIds: Set<string> = new Set();
const intersection: IntersectionObserver = new IntersectionObserver(
  (entries) => {
    entries.forEach((e) => {
      const id = e.target.getAttribute('data-cell-id');
      if (!id) return;
      if (e.isIntersecting && e.intersectionRatio > 0.91) {
        visibleCardIds.add(id);
      } else {
        visibleCardIds.delete(id);
      }
    });
  },
  {
    threshold: [0, 0.9, 1],
  },
);

const addIntersectionObserve = () => {
  visibleCardIds.clear();
  intersection?.disconnect();
  nextTick(() => {
    const nodes = document.body.querySelectorAll('.card') || [];
    for (const node of nodes) {
      intersection?.observe(node);
    }
  });
};

滚动处理

从上一次拿到的可视区域元素中拿到在更新后的数据中仍存在的、最靠近顶部的节点,记录它的位置,并把容器节点滚动到该节点的 scrollTop 高度。

const getNearestVisibleId = () => {
    return displayComment.value.find(c => visibleCardIds.has(c.id));
}

const silenceScroll = () => {
  const id = getNearestVisibleId();
  nextTick(() => {
    commentScrollTop(id);
  });
};

const commentScrollTop = (id: string) => {
  const n = commentsRef.value?.querySelector(
    `[data-cell-id="${id}"]`,
  ) as HTMLElement;
  if (n) {
    commentsRef.value?.scrollTo({
      top: n.offsetTop,
      behavior: 'instant',
    });
  }
};

结合起来

在数据更新后的下一帧触发滚动即可实现定位效果

<template>
	<div>
		<div
			v-for="comment in displayComments.slice(0, loadCount)"
			:key="comment.id"
		>
			{{ comment.data }}
		</div>
	</div>
</template>

<script lang="ts" setup>javascript">
// 假设comment有2000条数据,首次只加载200条
const commentsRef = ref();
const comments = ref<{ id: string; data: string }[]>();
const displayComments = computed(() => comments.slice(0, 200));

// 首屏动态加载的数量
const loadCount = ref<number>(0);
// 如果少于20条,直接全量加载;否则每隔4ms渲染40条数据
watch(displayComments, () => {
  clearInterval(timer);
  loadCount.value = 20;
  if (loadCount.value < nv.length) {
    timer = setInterval(() => {
      loadCount.value = Math.min(loadCount.value + 40, nv.length);
      if (loadCount.value >= nv.length) {
        clearInterval(timer);
        silenceScroll();
        addIntersectionObserve();
      }
    }, 4);
  } else {
    silenceScroll();
    addIntersectionObserve();
  }
})
</script>

定位优化

首屏加载速度上去了,也实现了自动滚动到指定的条目位置。但这时候遇到个新问题,如果列表的数据发生了变动,比如从前面插入了一条新数据,那么这时候可视范围内的数据肯定会发生改变。

如果使用批量重绘,即使本次修改只有一条数据更新,会出现页面先重新渲染,等10+ms后再滚动到指定位置,中间会出现画面的闪动。

这时候可以利用vue VDom算法的一些小Trick。先判断前后数据的差异程度,如果差异数量小于一个阈值(比如小于10),那么可以借用vue的diff算法,尽可能保留原有节点,渲染少量新节点。这时候可以大大提升页面的渲染效率,即使全量渲染也不会有性能问题。

<template>
	<div>
		<div
			v-for="comment in displayComments.slice(0, loadCount)"
			:key="comment.id"
		>
			{{ comment.data }}
		</div>
	</div>
</template>

<script lang="ts" setup>javascript">
// 假设comment有2000条数据,首次只加载200条
const commentsRef = ref();
const comments = ref<{ id: string; data: string }[]>();
const displayComments = computed(() => comments.slice(0, 200));

// 首屏动态加载的数量
const loadCount = ref<number>(0);
// 如果少于20条,直接全量加载;否则每隔4ms渲染40条数据
watch(displayComments, () => {
  clearInterval(timer);
  const nSet = new Set(nv.map((v) => v.id));
  const oSet = new Set(ov.map((v) => v.id));
  const n = new Set([...nSet].filter((x) => !oSet.has(x)));
  if (n.size <= 10) {
    loadCount.value = nv.length;
    silenceScroll();
    addIntersectionObserve();
  } else {
    loadCount.value = 20;
    if (loadCount.value < nv.length) {
      timer = setInterval(() => {
        loadCount.value = Math.min(loadCount.value + 40, nv.length);
        if (loadCount.value >= nv.length) {
          clearInterval(timer);
          silenceScroll();
          addIntersectionObserve();
        }
      }, 4);
    } else {
      silenceScroll();
      addIntersectionObserve();
    }
  }
})
</script>

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

相关文章

ffmpeg rtp发送video和audio并播放

发送h264 video ffmpeg -re -stream_loop -1 -i h264.mp4 -vcodec h264 -f rtp rtp://127.0.0.1:5006SDP: v0 o- 0 0 IN IP4 127.0.0.1 sNo Name cIN IP4 127.0.0.1 t0 0 atool:libavformat LIBAVFORMAT_VERSION mvideo 5006 RTP/AVP 96 artpmap:96 H264/90000 afmtp:96 packe…

html-dom核心内容--四要素

1、结构 HTML DOM (文档对象模型) 当网页被加载时&#xff0c;浏览器会创建页面的文档对象模型&#xff08;Document Object Model&#xff09;。 2、核心关注的内容&#xff1a;“元素”&#xff0c;“属性”&#xff0c;“修改样式”&#xff0c;“事件反应”。>四要素…

Linux线程篇(中)

有了之前对线程的初步了解我们学习了什么是线程&#xff0c;线程的原理及其控制。这篇文章将继续讲解关于线程的内容以及重要的知识点。 线程的优缺点&#xff1a; 线程的缺点 在这里我们来谈一谈线程健壮性&#xff1a; 首先我们先思考一个问题&#xff0c;如果一个线程出现…

python3高级编程

文章目录 1. Python网络编程1.1 服务器端代码(Server)1.2 客户端代码(Client) 2. 多线程2.1 线程模块2.2 使用 threading 模块创建线程2.3 线程同步2.4 线程优先级队列&#xff08; Queue&#xff09; 3. 日期和时间4. SMTP发送邮件4.1 使用Python发送HTML格式的邮件4.2 Python…

从非计算机科班到计算机领域:我的转码奇幻之旅

嘿&#xff0c;各位小伙伴们&#xff01;近年来&#xff0c;我发现有越来越多的小伙伴们都在考虑从其他行业跳槽进入计算机领域。你们知道吗&#xff0c;我觉得这真是个酷炫的决定&#xff01;毕竟&#xff0c;在计算机这个领域里&#xff0c;机会和创新无处不在。不过&#xf…

【深度学习】半监督学习 Efficient Teacher: Semi-Supervised Object Detection for YOLOv5

https://arxiv.org/abs/2302.07577 https://github.com/AlibabaResearch/efficientteacher 文章目录 AbstractIntroductionRelated WorkEfficient TeacherDense Detector Abstract 半监督目标检测&#xff08;SSOD&#xff09;在改善R-CNN系列和无锚点检测器的性能方面取得了成…

基于PIC单片机温度-脉搏-DS18B20温度-液晶12864显示(proteus仿真+源程序)

一、系统方案 1、上电初始化液晶第一行显示脉搏&#xff0c;第二行显示温度&#xff0c;第三行显示模式&#xff0c;第四行显示强度&#xff1b;按下K1按键可以选择模式&#xff0c;催眼模式或治疗模式。 2、治疗模块下&#xff0c;可以通过K2、K3修改强度。 二、硬件设计 原理…

4.RabbitMQ高级特性 幂等 可靠消息 等等

一、如何保证生产者生产消息100%的投递成功 保障消息的成功发出保障MQ节点的成功接收发送端收到MQ节点&#xff08;Broker&#xff09;确认应答完善的消息进行补偿机制 1. 理解Confirm确认消息机制 消息的确认&#xff0c;是指生产者投递消息后&#xff0c;如果Broker收到消…