v-listの高さを固定してスクロールできるようにする – 仮想スクロールは要りません

こんにちは。てぃろです。

今回はVueのv-listをスクロールしたかっただけなのにハマってしまったので、その顛末をメモしておきたいと思います。

結論から言うと、heightとoverflow-yを設定すればいいだけだった…。ハマるところじゃなかった(泣

実はポイントは、overflow-y: auto; を設定すること

この公式にあるようなリストについて、リストの要素がどれだけ多くても画面内に収めたかったのです。

加えて、要素数は追加/削除によって増減するという仕様です。

この公式の例では要素数が3程度のものが多く、スクロールさせているものがなかったので、スクロールを追加する方法を探すことにしました。

結果的には、v-listはCSSでheightを指定しoverflow-yを設定すればスクロールできるようになります。こんな風にします。

<template>
<v-list
    class="item-list"
    dense
    nav
    >
    <v-list-item
        v-for="(item, index) in items"
        :key="index"
    >
        <v-list-item-content>
            <v-list-item-title>{{ getTitle(index) }}</v-list-item-title>
            <v-list-item-subtitle>{{ getCategory(index) }}</v-list-item-subtitle>
        </v-list-item-content>
    </v-list-item>
</v-list>
</template>

<style scoped>
.item-list {
  height: 40vh;
  overflow-y: auto;
}
</style>

scriptは割愛しています。

実はポイントは、overflow-y: auto; を設定することです。

overflow-yとは、ブロックレベル要素の内容が境界からはみ出た場合の上下の表示方法を指定するCSSプロパティの一種です。ここで設定する値によって、はみ出て表示するのかスクロールバーが出るのかが決まります。

overflow-yプロパティの仕様については以下を参照してください。

ここではoverflow-y: auto;と設定しています。

これは結局、次のような挙動をします。

  • v-listの要素が追加されて画面の高さをはみ出ようとすると、スクロールバーが出てスクロール可能になる
  • v-listの要素が削除されて画面の高さに収まると、スクロールバーが消える

これが私が望んでいた挙動でした。実際の挙動は上記の仕様ページを参照してください。

仮想スクロールを使おうとして泥沼にハマる…

ここまで見るとハマる要素なぞ微塵もありませんが、私は仮想スクロールを使おうと必死にもがいてハマってしまっていました。

その理由はGoogle検索で「v-list スクロール」と検索したところから始まっていました。

一番上に”仮想スクロール”と出ています。「じゃあ仮想スクロール使うかー」となったのがハマった原因なのでした…。

では、何にはまったのか?

それは、仮想スクロールのリストの中身を追加/削除しようとしたことにあります。

仮想スクロールはリアクティブじゃない!

今回使ってみた仮想スクロールのライブラリは以下です。GitHubのスターが5000越えのすばらしいライブラリです。

この中でも標準的に使用するコンポーネントはRecycleScrollerです。今回試したものもこれです。

使い方の例がREADMEにあるので引用すると、以下のようになっています。(一部抜粋)

<template>
  <RecycleScroller
    class="scroller"
    :items="list"
    :item-size="32"
    key-field="id"
    v-slot="{ item }"
  >
    <div class="user">
      {{ item.name }}
    </div>
  </RecycleScroller>
</template>

ここで使われているlistが表示したいリストです。よく見るとこれはpropsです。dataではありません。実際RecycleScrollerのソースを見てもitemsdataには定義されていませんでした。

つまり、itemsはリアクティブではないのでlistを更新しても、再レンダリングはされないのです(泣

今回は仮想スクロールで表示するリストに対して要素の追加/削除をしていました。最も変な挙動は、要素を削除するとリストの要素と要素の間に不自然な余白ができていたことでした。開発者ツールでレンダリングされた変な余白を見ると、RecycleScrollerのラッパー(リストの要素を表示するdiv)が中身空っぽで存在していました。

リアクティブではない点と合わせると、上述のサンプルコードでいうclass="user"が指定されたdivはリアクティブに消えても、RecycleScrollerの要素であるラッパーが消えなかったため、変な空白が残るという挙動になったんだと考えられます。

このようにして仮想スクロールはそもそも変更検知できないのに、どうやればいいのかと四苦八苦していたのでした…。

仮想スクロールの使いどころは、大量のリストをスクロールする場合

本当の仮想スクロールの使いどころは、1万件といった大量のリストをスクロールしたい場合です。

普通のリストの場合表示はされてなくてもすべてレンダリングされていて、それだけブラウザに負荷がかかっています。そのため1万件を入れようとするとそれだけレンダリングしているのでクラッシュすることがあります。

そのため、仮想スクロールでは見えている範囲だけレンダリングするよう最適化します。

仮想スクロール自体は一般的な最適化処理の一つなので、Vueに限らず活用の幅は広いものなのです。

Vueで言っても以下のようなVuetifyでも実装はありますし、それ以外にもいくつか仮想スクロールのライブラリは存在します。

それぞれには当然実装のクセがありますので、気に入ったものを使えばよいと思います。

まとめ:普通のスクロールと仮想スクロールは使い分け

今回はv-listの要素が多くなった時にスクロールを追加する方法を、自分の苦い経験とともに紹介しました。

今回の私の要件では仮想スクロールは必要なかったですが、1万件といった大量のリストをスクロール表示させたいときに仮想スクロールは真価を発揮します。使い分け方は以下のような形でしょうか。

  • リストの要素数に増減がある + 要素数が多くない:普通のスクロール
  • リストの要素数に増減がある + 要素数が非常に多い :そうならないように設計を変える
  • リストの要素数に増減がない + 要素数が多くない:普通のスクロール
  • リストの要素数に増減がない + 要素数が非常に多い:仮想スクロール

しっかり実装したい仕様から最適な技術を選んで気持ちよいUXを実現できるようにしていきましょう!