Skip to content
← All questions
Advanced

How would you virtualize a list of thousands of items?

Performance

List virtualization renders only the items visible in the viewport instead of creating DOM nodes for every item. A list of 10,000 items with virtualization still uses around 20 DOM nodes, the same as a list of 100.

The problem

vue
<template>
  <!-- 10,000 UserCard components mounted at once -->
  <div class="list">
    <UserCard v-for="user in users" :key="user.id" :user="user" />
  </div>
</template>

Each DOM node consumes memory, and mounting 10,000 components blocks the main thread. The browser struggles or crashes.

Solution: vue-virtual-scroller

The most popular option. RecycleScroller recycles DOM nodes as the user scrolls.

vue
<template>
  <RecycleScroller
    class="list"
    :items="users"
    :item-size="80"
    key-field="id"
    v-slot="{ item }"
  >
    <UserCard :user="item" />
  </RecycleScroller>
</template>

<script setup>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
</script>

<style scoped>
.list {
  height: 600px; /* container must have a fixed height */
}
</style>

For variable-height items, use DynamicScroller:

vue
<template>
  <DynamicScroller :items="messages" :min-item-size="54" key-field="id">
    <template #default="{ item, index, active }">
      <DynamicScrollerItem :item="item" :active="active" :data-index="index">
        <ChatMessage :message="item" />
      </DynamicScrollerItem>
    </template>
  </DynamicScroller>
</template>

Alternative: @tanstack/vue-virtual

A headless virtualizer that gives you full control over rendering. No built-in styles or container component.

vue
<template>
  <div ref="parentRef" class="list-container">
    <div :style="{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }">
      <div
        v-for="row in virtualizer.getVirtualItems()"
        :key="row.key"
        :style="{
          position: 'absolute',
          top: 0,
          left: 0,
          width: '100%',
          height: `${row.size}px`,
          transform: `translateY(${row.start}px)`
        }"
      >
        <UserCard :user="users[row.index]" />
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useVirtualizer } from '@tanstack/vue-virtual'

const users = ref([/* thousands of items */])
const parentRef = ref(null)

const virtualizer = useVirtualizer({
  count: users.value.length,
  getScrollElement: () => parentRef.value,
  estimateSize: () => 80,
  overscan: 5
})
</script>

<style scoped>
.list-container {
  height: 600px;
  overflow: auto;
}
</style>

Library comparison

LibraryApproachBest for
vue-virtual-scrollerComponent-based, batteries includedQuick setup, most use cases
@tanstack/vue-virtualHeadless composableCustom layouts, full control
vue-virtual-scroll-grid2D virtualizationGrid/gallery layouts

When NOT to virtualize

  • Lists under 50-100 items with simple content (overhead not worth it)
  • Print layouts where all content must render
  • SEO-critical content that needs to be in the initial HTML
  • Accessibility scenarios where all items must be reachable by screen readers at once

Released under the MIT License.