Vue.js + ブラウザのネイティブ APIを用いたテーブル表示の高速化
こんにちは、booostのOTAです。
今回は、Vue.js + ブラウザのネイティブ API Intersection Observer を用いたテーブル表示の高速化についてご紹介します。
Intersection Observer API - Web APIs | MDN
Vue.js による大量テーブル描画がボトルネックに
あるページの初期表示に 15 秒以上かかるという課題が発生していました。調査の結果、バックエンドのレスポンスは 1 秒未満と非常に速いにも関わらず、フロントエンド側で 1000 行以上のデータを Vue.js により一括で描画していたことが主な原因であると判明しました。
Vue.js のリアクティブシステムは、すべての DOM 要素に対して状態の追跡と更新を行うため、初期描画に極端な時間がかかっていたのです。
Intersection Observer + slice による改善策
パフォーマンス改善のアプローチとして、フロントエンド側で以下のような「疑似レイジーローディング」を実装しました。(一般公開用に一部抽象化)
<template>
<tbody>
<tr
v-for="(element, index) in computedList"
:key="index"
:ref="isLazyLoadingActive && index === computedList.length - 1 ? 'lazyRow' : null"
>
<slot name="data" :data="element" :index="index" />
</tr>
</tbody>
</template>
<script>
export default {
name: 'LazyTBody',
inject: ['useTBodyLazyLoading'],
props: {
list: { type: Array, required: true, default: () => [] },
useLazyLoading: { type: Boolean, default: false },
initialCount: { type: Number, default: 50 },
loadCount: { type: Number, default: 30 }
},
data () {
return {
displayCount: this.initialCount,
observer: null
}
},
computed: {
isLazyLoadingActive () {
return this.useTBodyLazyLoading || this.useLazyLoading
},
computedList () {
return this.isLazyLoadingActive
? this.list.slice(0, this.displayCount)
: this.list
}
},
mounted () {
this.setupObserver()
},
updated () {
this.$nextTick(() => {
if (this.$refs.lazyRow && !this.observer) {
this.setupObserver()
}
})
},
beforeUnmount () {
if (this.observer) this.observer.disconnect()
},
methods: {
loadMore () {
if (this.displayCount < this.list.length) {
this.displayCount += this.loadCount
if (this.observer) {
this.observer.disconnect()
this.$nextTick(this.setupObserver)
}
}
},
setupObserver () {
if (!this.$refs.lazyRow) return
this.observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) this.loadMore()
})
})
this.observer.observe(this.$refs.lazyRow)
}
}
}
</script>
- 初期表示では JavaScript の
slice
関数を用いて、先頭から一定行数(例:50 行)のみを描画 - スクロールを検知するために、ブラウザのネイティブ API である Intersection Observer API を活用
- テーブル末尾の要素がビューポートに入ったことを検出したタイミングで、描画する行数を
props
で受け取った単位(例:50 行)ずつ増加 - Vue.js のリアクティビティを活かし、自然な形でテーブルが拡張されていくようにレンダリング
この仕組みにより、描画対象の行数を柔軟に制御できるようになり、異なるテーブル構成や利用シーンにも適用可能となりました。
パフォーマンス改善の効果
この改善を実施した結果、初期描画にかかっていた時間は 15 秒以上から 2 秒未満へと大幅に短縮されました。すべてのデータを一括で描画せず、スクロールに応じて順次表示することで、DOM 操作のコストを抑えつつ、ユーザーにとってもスムーズな閲覧体験を実現しました。
結びに
フロントエンドのレンダリング処理がボトルネックになるケースは意外と多く、特に Vue.js のようなリアクティブなフレームワークでは、データ量が増えるほどパフォーマンスへの影響が顕著になります。
今回のように Intersection Observer API と slice
を組み合わせることで、シンプルかつ柔軟に描画処理を最適化することが可能です。もし同様の課題に直面している場合は、このアプローチが解決のヒントになるかもしれません。
最後までお読みいただき、ありがとうございました。
Discussion