バーチャルスクロール
リストやテーブルなどのコンポーネントに大量のデータを表示すると結構時間が掛かりますよね?
バーチャルスクロールは、画面に表示される部分だけをレンダリングし、見えない部分は破棄または非表示にすることでパフォーマンスを最適化する技術です。
ユーザーがスクロールするたびにスクロール位置を計算し、新たに表示すべきアイテムだけをDOMに追加、既に見えなくなったアイテムを削除します。
これにより、ユーザーには全データが表示されているように見えるという仕組みです。とは言え、自分で実装するのは大変なのでライブラリを使用します。
今回は「vue-virtual-scroller」ライブラリを使用してみました。 下記コマンドでインストールします。
npm install --save vue-virtual-scroller@next
インストールはこれで完了ですが、実装して「モジュール 'vue-virtual-scroller' の宣言ファイルが見つかりませんでした。」 というエラーになる場合はTypeScriptがvue-virtual-scrollerの型定義ファイルを見つけられないことが原因です。
この場合はtypesフォルダ内に新しいファイルを作成します。
例: types/vue-virtual-scroller.d.ts
declare module 'vue-virtual-scroller';
pages/virtualScroll.vue
■ RecycleScroller
このコンポーネントは、表示可能なアイテムのみをDOMにレンダリングし、見えない部分は破棄することでパフォーマンスを向上させます。
■ item-size
各行の高さを指定することで、正確なスクロール位置を計算します。
■ items
大量のデータを渡すために、10000件のダミーデータを作成しています。
■ filteredItems
filter()メソッドを利用してデータを絞り込めるようにしています。
<template>
<div>
<!-- 絞り込み用の入力フィールド -->
<input
v-model="searchQuery"
type="text"
placeholder="検索条件を入力"
class="search-input"
/>
<!-- 横スクロールを許可したテーブルコンテナ -->
<div class="table-wrapper" ref="tableWrapper" @scroll="onHorizontalScroll">
<div class="virtual-table">
<RecycleScroller
:items="filteredItems"
:item-size="50"
class="table-container"
direction="vertical"
ref="virtualScroller"
@scroll="onVerticalScroll"
>
<template #default="{ item, index }">
<div class="table-row" :key="index">
<div class="table-cell" v-for="n in 20" :key="n">
{{ item.name }} - {{ n }}
</div>
</div>
</template>
</RecycleScroller>
</div>
<!-- 縦スクロールバー -->
<div ref="customScrollBar" class="vertical-scroll-bar" @mousedown="onMouseDown">
<div ref="scrollThumb" class="scroll-thumb" :style="thumbStyle"></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
import { RecycleScroller } from 'vue-virtual-scroller';
import { ref, computed, onMounted } from 'vue';
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i + 1,
name: `Item ${i + 1}`,
value: Math.random().toFixed(2),
}));
const searchQuery = ref('');
const filteredItems = computed(() => {
if (!searchQuery.value) {
return items;
}
return items.filter((item) =>
item.name.toLowerCase().includes(searchQuery.value.toLowerCase())
);
});
const tableWrapper = ref<HTMLDivElement | null>(null);
const customScrollBar = ref<HTMLDivElement | null>(null);
const scrollThumb = ref<HTMLDivElement | null>(null);
const virtualScroller = ref<any>(null);
const thumbStyle = ref({
height: '50px',
top: '0px',
});
let isDragging = false;
let startY = 0;
let startTop = 0;
const updateThumbPosition = () => {
if (!virtualScroller.value || !customScrollBar.value) return;
const scrollHeight = virtualScroller.value.$el.scrollHeight;
const clientHeight = virtualScroller.value.$el.clientHeight;
const thumbHeight = Math.max((clientHeight / scrollHeight) * clientHeight, 20);
const scrollTop = virtualScroller.value.$el.scrollTop;
thumbStyle.value = {
height: `${thumbHeight}px`,
top: `${(scrollTop / scrollHeight) * clientHeight}px`,
};
};
const onVerticalScroll = () => {
updateThumbPosition();
};
const onHorizontalScroll = () => {
if (tableWrapper.value && customScrollBar.value) {
const scrollLeft = tableWrapper.value.scrollLeft;
customScrollBar.value.style.right = `-${scrollLeft}px`;
}
};
const onMouseDown = (event: MouseEvent) => {
isDragging = true;
startY = event.clientY;
startTop = parseFloat(thumbStyle.value.top);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
const onMouseMove = (event: MouseEvent) => {
if (!isDragging || !virtualScroller.value) return;
const deltaY = event.clientY - startY;
const scrollHeight = virtualScroller.value.$el.scrollHeight;
const clientHeight = virtualScroller.value.$el.clientHeight;
const newTop = Math.min(
clientHeight - parseFloat(thumbStyle.value.height),
Math.max(0, startTop + deltaY)
);
thumbStyle.value.top = `${newTop}px`;
const scrollRatio = newTop / clientHeight;
virtualScroller.value.$el.scrollTop = scrollRatio * scrollHeight;
};
const onMouseUp = () => {
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
onMounted(() => {
updateThumbPosition();
});
</script>
<style scoped>
.search-input {
margin-bottom: 10px;
padding: 8px;
width: 100%;
box-sizing: border-box;
}
.table-wrapper {
width: 100%;
overflow: auto;
border: 1px solid #ccc;
position: relative;
}
.virtual-table {
min-width: 2000px;
}
.table-container {
height: 400px;
overflow: hidden; /* 標準スクロールバーを隠す */
}
.table-row {
display: flex;
border-bottom: 1px solid #e0e0e0;
}
.table-cell {
flex: 1 0 150px;
padding: 0 8px;
border-right: 1px solid #ddd;
text-align: left;
}
.table-cell:last-child {
border-right: none;
}
.vertical-scroll-bar {
position: absolute;
top: 0;
right: 0;
width: 10px;
height: 100%;
background: rgba(0, 0, 0, 0.1);
cursor: pointer;
}
.scroll-thumb {
position: absolute;
width: 100%;
background: rgba(0, 0, 0, 0.4);
border-radius: 5px;
}
</style>
以下が実行結果です。とても高速に表示されました!
「vue-virtual-scroller」ライブラリを利用すると非常に簡単に実装できました!
大量データが原因でページが表示されるのが遅くて悩んでいる方は是非お試しください。
今回はここで終了ですが、今回のサンプルプログラムは理解できましたか?
覚えていて損は無いと思いますので、いろいろ自分で試してみて少しずつ理解を深めてみてくださいね!