长列表性能优化方案

  |  

普通场景

一下创建 100000 个li ,然后插入到页面上

<ul id="container"></ul>
<script>
let now = Date.now();
const total = 100000;
let ul = document.getElementById('container');
for (let i = 0; i < total; i++) {
let li = document.createElement('li');
li.innerText = ~~(Math.random() * total);
ul.appendChild(li);
}
console.log('JS运行时间:', Date.now() - now);
setTimeout(() => {
console.log('总运行时间:', Date.now() - now);
}, 0)
</script>

这个时候快速下拉,页面直接卡顿了

至于为什么会出现卡顿现象, 可以看回上一篇文章

优化方案一 - 时间分片

可以采用setTimeout去追加

<script>
let ul = document.getElementById('container');
let total = 100000;
let once = 20;
let page = total / once
let index = 0;
function loop(curTotal, curIndex) {
if (curTotal <= 0) {
return false;
}
let pageCount = Math.min(curTotal, once);
setTimeout(() => {
for (let i = 0; i < pageCount; i++) {
let li = document.createElement('li');
li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
ul.appendChild(li)
}
loop(curTotal - pageCount, curIndex + pageCount)
}, 0)
}
loop(total, index);
</script>

但是,setTimeout一堆的异步任务,快速拖动时还是有明显的卡顿现象
因此采用, requestAnimationFrame

<body>
<ul id="container"></ul>
<script>
let ul = document.getElementById('container');
let total = 100000;
let once = 20;
let page = total / once
let index = 0;

function loop(curTotal, curIndex) {
if (curTotal <= 0) {
return false;
}
let pageCount = Math.min(curTotal, once);
window.requestAnimationFrame(() => {
for (let i = 0; i < pageCount; i++) {
let li = document.createElement('li');
li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
ul.appendChild(li)
}
loop(curTotal - pageCount, curIndex + pageCount);
})
}
loop(total, index);
</script>
</body>

其实,对于dom的操作,我们还可以换成操作DocumentFragment

<script>
let ul = document.getElementById('container');
let total = 100000;
let once = 1000;
let page = total / once
let index = 0;
function loop(curTotal, curIndex) {
if (curTotal <= 0) {
return false;
}
let pageCount = Math.min(curTotal, once);
window.requestAnimationFrame(function () {
let fragment = document.createDocumentFragment();
for (let i = 0; i < pageCount; i++) {
let li = document.createElement('li');
li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
fragment.appendChild(li)
}
ul.appendChild(fragment)
loop(curTotal - pageCount, curIndex + pageCount)
})
}
loop(total, index);
</script>

方案二 - 虚拟列表

虚拟列表: 其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。

基于vue 实现

<template>
<!--
由于只是对可视区域内的列表项进行渲染,所以为了保持列表容器的高度并可正常的触发滚动,将Html结构设计成如下结构:
infinite-list-container 为可视区域的容器
infinite-list-phantom 为容器内的占位,高度为总列表高度,用于形成滚动条
infinite-list 为列表项的渲染区域
-->
<div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)">
<div
class="infinite-list-phantom"
:style="{ height: listHeight + 'px' }"
></div>
<div class="infinite-list" :style="{ transform: getTransform }">
<div
ref="items"
class="infinite-list-item"
v-for="item in visibleData"
:key="item.id"
:style="{ height: itemSize + 'px', lineHeight: itemSize + 'px' }"
>
{{ item.value }}
</div>
</div>
</div>
</template>

<script lang="ts" setup>
import { ref, computed, onMounted, onUpdated } from 'vue';
const props = defineProps({
listData: {
type: Array,
defaut: () => [],
},
itemSize: {
type: Number,
default: 200,
},
});
const list = ref<HTMLDivElement>(null);
const items = ref<HTMLDivElement>(null);
// 可视区域的高度
const screenHeight = ref<number>(0);
// 偏移量
const startOffset = ref<number>(0);
// 开始索引
const start = ref<number>(0);
// 结束索引
const end = ref<number | null>(null);

// 列表高度
const listHeight = computed(() => props.listData?.length * props.itemSize);
// 可显示的列表项数
const visibleCount = computed(() =>
Math.ceil(screenHeight.value / props.itemSize)
);
// 偏移量对应的style
const getTransform = computed(() => `translate3d(0,${startOffset.value}px,0`);
// 真实展示显示列表的数据
const visibleData = computed(() =>
props.listData?.slice(start.value, Math.min(end.value, props.listData.length))
);
onMounted(() => {
screenHeight.value = list.value.clientHeight;
start.value = 0;
end.value = start.value + visibleCount.value;
});
function scrollEvent() {
// 当前滚动位置
let scrollTop = list.value.scrollTop;
start.value = Math.floor(scrollTop / props.itemSize);
end.value = start.value + visibleCount.value;
startOffset.value = scrollTop - (scrollTop % props.itemSize);
}
</script>

<style scoped>
.infinite-list-container {
height: 100%;
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
}

.infinite-list-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}

.infinite-list {
left: 0;
right: 0;
top: 0;
position: absolute;
text-align: center;
}
.infinite-list-item {
padding: 10px;
color: #555;
box-sizing: border-box;
border-bottom: 1px solid #999;
}
</style>

×

纯属好玩

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 普通场景
  2. 2. 优化方案一 - 时间分片
  3. 3. 方案二 - 虚拟列表
,