Vue 3とTypeScriptを使用したvue-virtual-scrollerライブラリの使い方と実装方法を解説する。仮想スクロールの基本から、DynamicScrollerとRecycleScrollerの違い、さらには動的なアイテムサイズへの対応方法まで、実用的なテクニックも紹介している。

vue-virtual-scrollerとは

Vue.jsにおいて長いリストや大量のデータを効率的にスクロール表示するためのライブラリ。このライブラリを使用すると、DOM要素の数を削減してパフォーマンスを向上させることができる。

公式サイト: https://github.com/Akryum/vue-virtual-scroller

基本的な仕組み

一般的なスクロール可能なリストでは、すべてのアイテムがDOM上に存在するため、リストが長くなるとパフォーマンスが低下する可能性がある。vue-virtual-scrollerは「ウィンドウ」と呼ばれる表示領域だけにアイテムをレンダリングし、ユーザーがスクロールするとそれに合わせて表示されるアイテムを動的に更新する。

主な特徴

  1. 動的な高さ: 各アイテムの高さが一定でなくてもうまく動作する。
  2. 再利用: 既にレンダリングされたDOM要素を再利用することで、不必要なレンダリングを削減する。
  3. スクロール位置の保存: ユーザーがリスト内での位置を維持できるように、スクロール位置を保存する。

RecycleScrollerとDynamicScrollerの使い分け

vue-virtual-scrollerでは主にRecycleScrollerDynamicScrollerの2種類のコンポーネントが提供されている。両者は「大量のリストデータを効率的に扱う」という点では共通した機能を有しているが、用途や動作にはいくつか違いがある。

RecycleScroller

  1. 固定または既知のアイテムサイズ: このコンポーネントは、すべてのアイテムが同じサイズ、またはアイテムのサイズが事前に計算できる場合に最適。
  2. 高パフォーマンス: アイテムのサイズが一定であるため、計算が簡単でスクロールのパフォーマンスも高い。
  3. シンプル: APIとプロパティが比較的シンプルで、基本的な用途には十分な機能を持っている。

DynamicScroller

  1. 動的なアイテムサイズ: このコンポーネントは、異なる高さや幅を持つアイテムに適している。
  2. サイズのキャッシュ: 一度レンダリングされたアイテムのサイズは内部でキャッシュされ、次回以降のレンダリングが高速化される。

まとめ

  • RecycleScroller: シンプルで高速。固定サイズのアイテムに適している。
  • DynamicScroller: より柔軟で高機能。動的なサイズのアイテムに対応している。

Vue3 TypeScript環境での実装方法

パッケージをインストールする。

npm install --save vue-virtual-scroller@next

RecycleScroller

以下が実装のコード。コードの解説はコメントを参照。

<template>
  <v-container class="list">
    <!-- item-sizeにアイテムの高さを指定する -->
    <recycle-scroller
      class="scroller"
      :items="list"
      :item-size="300"
      key-field="id"
      v-slot="{ item }"
    >
    <v-card width="400">
      <v-img
        :height="200"
        :src="item.imageSrc"
        cover
        class="text-white"
      >
        <v-toolbar
          color="rgba(0, 0, 0, 0)"
          theme="dark"
        >
          <v-toolbar-title class="text-h6">
            {{item.text}}
          </v-toolbar-title>
          <template v-slot:append>
            <v-btn icon="mdi-dots-vertical"></v-btn>
          </template>
        </v-toolbar>
      </v-img>
      <v-card-text>
        テキスト
      </v-card-text>
    </v-card>
    </recycle-scroller>
  </v-container>
</template>

<script lang="ts">
import { defineComponent, onMounted, ref, type Ref } from 'vue';
// RecycleScrollerをインポート
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

export default defineComponent({
  name: 'VueVirtualScrollerPane',
  components: {
    RecycleScroller
  },
  setup () {
    /**
     * リストアイテムの配列
     */
    const list = ref([]) as Ref<Array<{ id: number, imageSrc: string, text: string }>>
    /**
     * #onMounted
     * リストアイテムを100件に増やしている
     */
    onMounted(() => {
      for (let index = 0; index < 100; index++) {
        list.value.push({
          id: index,
          imageSrc: 'https://cdn.vuetifyjs.com/docs/images/cards/purple-flowers.jpg',
          text: `Messages${index}`
        })
      }
    })
    return {
      list
    }
  }
})
</script>

<style scoped>
/** recycle-scrollerコンポーネントで固定の高さを確保し、 overflow-y: scroll;を指定する必要がある。*/
.scroller {
  height: 70vh;
  overflow-y: scroll;
}
</style>

表示は以下のようになる。データ100件に対して生成されるDOMの数は5件程度であり、効率的にDOMを再利用していることがわかる。各要素にtransformプロパティが存在しており、スクロールに応じて表示位置を自動でコントロールしてくれている。

RecycleScrollerの注意点

  • virtual-scroller要素とアイテム要素のサイズを設定する必要がある(CSS使用が奨励)。
    可変サイズモードを使用していない限り、すべてのアイテムは同じ高さを持つべきである。
  • アイテムがオブジェクトである場合、スクローラーがそれらを識別するために一意のkeyを指定する必要がある。デフォルトでは、アイテムにidフィールドがあるかどうかを確認する。別のフィールド名をkeyとして使用している場合、keyFieldプロパティで設定する。
  • リストアイテムのコンポーネントは、再作成されずにitemプロパティが更新されるようにリアクティブでなければならない。
  • ブラウザにはDOM要素のサイズ制限があり、現状では最大約50万アイテムしか表示できない。

カスタムプロパティとイベントについては以下を参照する。

https://github.com/Akryum/vue-virtual-scroller/tree/master/packages/vue-virtual-scroller#props

DynamicScroller

RecycleScrollerはアイテムの高さが固定の場合に便利だが、SNSの投稿一覧などの機能を実装する場合、アイテムの高さが全て固定であるケースは少ない。各アイテムはそれぞれ異なる高さを持つため、DynamicScrollerを使って動的な高さのアイテムに対応できるようにする。

<template>
  <v-container class="container">
    <DynamicScroller
      class="scroller"
      :items="list"
      :min-item-size="200"
      key-field="id"
      item-class="original-scroller-item"
    >
    <template v-slot="{ item, index, active }">
      <DynamicScrollerItem
        :item="item"
        :active="active"
        :data-index="index"
        :size-dependencies="[item.imageSrc, item.text]"
      >
        <v-card>
          <img
            :src="item.imageSrc"
            class="text-white"
          >
          <div class="pb-6">
            テキスト
          </div>
        </v-card>
      </DynamicScrollerItem>
    </template>
    </DynamicScroller>
  </v-container>
</template>

<script lang="ts">
import { defineComponent, onMounted, ref, type Ref } from 'vue';
// RecycleScrollerをインポート
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

const images = [
  'https://cdn.pixabay.com/photo/2023/08/28/23/17/superb-fairywren-8220199_1280.jpg',
  'https://cdn.pixabay.com/photo/2023/09/04/13/17/mushrooms-8232731_1280.jpg',
  'https://cdn.vuetifyjs.com/images/cards/plane.jpg',
  'https://cdn.vuetifyjs.com/images/cards/docks.jpg',
]

export default defineComponent({
  name: 'VueVirtualScrollerPane',
  components: {
    DynamicScroller,
    DynamicScrollerItem
  },
  setup () {
    /**
     * リストアイテムの配列
     */
    const list = ref([]) as Ref<Array<{ id: number, imageSrc: string, text: string }>>

    /**
     * ランダムに画像を抽出する
     */
    const getRandomImage = () => {
      const randomIndex = Math.floor(Math.random() * images.length);
      return images[randomIndex];
    }
    /**
     * #onMounted
     * リストアイテムを100件に増やしている
     */
    onMounted(() => {
      for (let index = 0; index < 100; index++) {
        list.value.push({
          id: index,
          imageSrc: getRandomImage(),
          text: `Messages${index}`
        })
      }
    })
    return {
      list
    }
  }
})
</script>

<style scoped>
.container {
  width: 400px;
}
/** recycle-scrollerコンポーネントで固定の高さを確保し、 overflow-y: scroll;を指定する必要がある。*/
.scroller {
  height: 80vh;
  overflow-y: scroll;
}
img {
  max-width: 100%;
}
</style>

表示は以下のようになる。データ100件に対して生成されるDOMの数は15件程度となっている。画像の縦横比が異なる即ち、各アイテムの高さが異なる場合でもDynamicScrollerを使うことで動的にスクローラーの高さを再計算している。

DynamicScrollerの注意点

  • minItemSizeは、アイテムの初期レンダリングに必須である。
  • DynamicScrollerは自動的にサイズ変更を検出しないが、DynamicScrollerItemsize-dependenciesを付けることで、アイテムサイズに影響を与える値を設定することができる。例えば上記の実装例の場合、item.imageSrcを指定することで画像の高さに変更が生じた場合に、再計算を行うようにライブラリに通知することができる。

カスタムプロパティとイベントについては以下を参照する。

https://github.com/Akryum/vue-virtual-scroller/tree/master/packages/vue-virtual-scroller#dynamicscrolleritem

カテゴリー: Vue.js