<template>
<div class="scroll-container" @scroll="onScroll">
<div
class="row"
v-for="(item, index) in visibleItems"
:key="startIndex + index"
:style="rowStyle(index)"
>
{{ item }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { throttle } from 'lodash-es';
const items = ref(Array.from({ length: 10000000 }, (_, i) => `Item ${i + 1}`));
const rowHeight = ref(50);
const visibleRowsCount = ref(0);
const startIndex = ref(0);
const visibleItems = computed(() =>
items.value.slice(startIndex.value, startIndex.value + visibleRowsCount.value)
);
const calculateVisibleRows = () => {
const containerHeight = document.querySelector('.scroll-container')?.clientHeight || 0;
visibleRowsCount.value = Math.ceil(containerHeight / rowHeight.value) + 2;
};
let animationFrame: number | null = null;
const onScroll = throttle(() => {
if (animationFrame !== null) {
cancelAnimationFrame(animationFrame);
}
animationFrame = requestAnimationFrame(() => {
const container = document.querySelector('.scroll-container') as HTMLElement;
const scrollTop = container.scrollTop;
startIndex.value = Math.floor(scrollTop / rowHeight.value);
});
}, 16);
const rowStyle = (index: number) => ({
height: `${rowHeight.value}px`,
transform: `translateY(${(startIndex.value + index) * rowHeight.value}px)`,
width: '100%',
position: 'absolute' as 'absolute',
backgroundColor: (startIndex.value + index) % 2 === 0 ? 'black' : 'red',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
});
onMounted(() => {
calculateVisibleRows();
onScroll();
});
</script>
<style>
.scroll-container {
height: 800px;
overflow-y: auto;
position: relative;
will-change: transform;
}
.row {
display: flex;
align-items: center;
background-color: aquamarine;
}
</style>