Skip to content

RecycleScroller

RecycleScroller is the core component for virtualizing large lists in Vue. It renders only the visible items, then reuses component instances and DOM nodes as you scroll.

Basic usage

Use the default scoped slot to render each item in the list:

vue
<script>
export default {
  props: {
    list: Array,
  },
}
</script>

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

<style scoped>
.scroller {
  height: 100%;
}

.user {
  height: 32%;
  padding: 0 12px;
  display: flex;
  align-items: center;
}
</style>

TypeScript generics

With Vue 3.3+, RecycleScroller infers the item type from items, so the default slot stays item-aware in TypeScript:

vue
<script setup lang="ts">
import { ref } from 'vue'

interface Message {
  id: string
  text: string
  size: number
}

const messages = ref<Message[]>([])
</script>

<template>
  <RecycleScroller :items="messages" :item-size="32">
    <template #default="{ item }">
      {{ item.text.toUpperCase() }}
    </template>
  </RecycleScroller>
</template>

If you want compile-time validation for string keyField values or sizeField, use the headless useRecycleScroller API with an explicit generic parameter.

Important notes

WARNING

Set the size of the scroller element and the item elements yourself, usually with CSS. Unless you are using variable size mode, every item should have the same height, or width in horizontal mode.

WARNING

If your items are objects, the scroller needs a stable identifier for each one. By default it looks for an id field. Use keyField if your data uses a different property name, or pass a resolver function with the signature (item, index) => string | number for derived keys such as composite IDs.

vue
<RecycleScroller
  :items="messages"
  :item-size="32"
  :key-field="(item, index) => `${item.threadId}:${item.id}:${index}`"
>
  • Avoid functional components inside RecycleScroller. Because views are reused, they are usually slower here rather than faster.
  • Item components must react correctly when the item prop changes without the component being recreated. Computed properties and watchers are usually the right tools for that.
  • You do not need to set key on the list content itself, but nested <img> elements should still use keys to avoid loading glitches.
  • Browsers impose size limits on very large DOM elements, so the practical ceiling is still around a few hundred thousand items depending on the browser.
  • Because DOM elements are reused, hover styles should usually rely on the provided hover class rather than the :hover selector.

How it works

  • RecycleScroller creates pools of reusable views for the visible part of the list.
  • Each view holds one rendered item and can later be reassigned to another item.
  • If you render multiple item types, each type gets its own pool so Vue can reuse compatible component trees.
  • Views that move off-screen are deactivated and reused when new items enter the viewport.

In vertical mode, the internal structure looks like this:

html
<RecycleScroller>
  <!-- Wrapper element with a pre-calculated total height -->
  <wrapper
    :style="{ height: computedTotalHeight + 'px' }"
  >
    <!-- Each view is translated to the computed position -->
    <view
      v-for="view of pool"
      :style="{ transform: 'translateY(' + view.computedTop + 'px)' }"
    >
      <!-- Your elements will be rendered here -->
      <slot
        :item="view.item"
        :index="view.nr.index"
        :active="view.nr.used"
      />
    </view>
  </wrapper>
</RecycleScroller>

As you scroll, most views are simply moved to new positions and receive updated slot props. That keeps component creation and DOM churn low, which is where most of the performance gains come from.

Props

PropDefaultDescription
itemsList of items you want to display in the scroller.
direction'vertical'Scrolling direction, either 'vertical' or 'horizontal'.
itemSizenullDisplay height (or width in horizontal mode) of the items in pixels used to calculate the scroll size and position. Accepts a fixed number, null for sizeField-based variable size mode, or a resolver function (item, index) => number.
gridItemsDisplay that many items on the same line to create a grid. You must set itemSize to a fixed number to use this prop (dynamic sizes are not supported).
itemSecondarySizeSize in pixels (width in vertical mode, height in horizontal mode) of the items in the grid when gridItems is set. Defaults to itemSize if not set.
minItemSizeMinimum size used if the height (or width in horizontal mode) of an item is unknown.
sizeField'size'Field used to get the item's size in variable size mode.
typeField'type'Field used to differentiate different kinds of components in the list. For each distinct type, a pool of recycled items will be created.
keyField'id'Field name or resolver function (item, index) => string | number used to identify items and optimize managing rendered views.
shiftfalseKeep the viewport anchored when items are prepended at the start of the list. Useful for chat-style feeds and reverse timelines.
cacheOptional cache snapshot returned by cacheSnapshot to restore known item sizes after remounting.
prerender0Render a fixed number of items for Server-Side Rendering (SSR).
buffer200Amount of pixels to add to edges of the scrolling visible area to start rendering items further away.
emitUpdatefalseEmit an 'update' event each time the virtual scroller content is updated (can impact performance).
disableTransformfalseUse absolute top/left positioning instead of CSS transforms for pooled item wrappers. This also keeps fixed-grid cross-axis placement in left/top.
flowModefalseKeep active pooled views in native document flow and use spacer elements before/after them instead of absolute positioning. v1 only supports vertical single-axis lists. gridItems, horizontal mode, and hiddenPosition fall back to standard positioning.
updateInterval0The interval in ms at which the view will be checked for updates after scrolling. When set to 0, check happens during the next animation frame.
listClass''Custom classes added to the item list wrapper.
itemClass''Custom classes added to each item.
listTag'div'The element to render as the list's wrapper.
itemTag'div'The element to render as the list item (the direct parent of the default slot content).

Events

EventDescription
resizeEmitted when the size of the scroller changes.
visibleEmitted when the scroller considers itself to be visible in the page.
hiddenEmitted when the scroller is hidden in the page.
update(startIndex, endIndex, visibleStartIndex, visibleEndIndex)Emitted each time the views are updated, only if emitUpdate prop is true.
scroll-startEmitted when the first item is rendered.
scroll-endEmitted when the last item is rendered.

Default scoped slot props

PropDescription
itemItem being rendered in a view.
indexReflects each item's position in the items array.
activeWhether or not the view is active. An active view is considered visible and being positioned by RecycleScroller. An inactive view is not considered visible and is hidden from the user. Any rendering-related computations should be skipped if the view is inactive.

Other slots

The empty slot is rendered only when items is empty.

html
<main>
  <slot name="before"></slot>
  <wrapper>
    <!-- Reused view pools here -->
    <slot name="empty"></slot>
  </wrapper>
  <slot name="after"></slot>
</main>

Example:

vue
<RecycleScroller
  class="scroller"
  :items="list"
  :item-size="32"
>
  <template #before>
    Hey! I'm a message displayed before the items!
  </template>

  <template v-slot="{ item }">
    <div class="user">
      {{ item.name }}
    </div>
  </template>
</RecycleScroller>

Exposed methods

If you keep a template ref to RecycleScroller via useTemplateRef, the component exposes these helpers:

  • scrollToItem(index, options?)
  • scrollToPosition(position, options?)
  • findItemIndex(offset)
  • getItemOffset(index)
  • getItemSize(index)
  • startSpacerSize
  • endSpacerSize
  • cacheSnapshot
  • restoreCache(snapshot)
  • updateVisibleItems(itemsChanged, checkPositionDiff?)

Flow mode

Use flowMode when the surrounding CSS must stay close to native layout, such as block rows or semantic lists. In this mode RecycleScroller keeps active pooled views in DOM order and inserts spacer elements ahead of and after the active window.

flowMode is intentionally limited in v1:

  • vertical direction only
  • no gridItems
  • no horizontal virtualization
  • hiddenPosition is ignored because parked pooled views use display: none

The optional options object for scrolling accepts:

  • align: 'start' | 'center' | 'end' | 'nearest'
  • smooth: use native smooth scrolling when available
  • offset: add or subtract a fixed pixel offset from the computed target

align: 'nearest' only scrolls when the target item is outside the current viewport.

Page mode

If the list should always follow window scrolling, prefer WindowScroller.

Prepend anchoring and cache restore

Use shift when items are inserted at the beginning of the list and you want the current content to stay visually anchored.

Use cacheSnapshot together with the cache prop or restoreCache(snapshot) when the same list is remounted and you want to reuse previously known item sizes instead of measuring them again.

See the dedicated Shift demo for a prepend-history example.

Variable size mode

WARNING

This mode can be performance heavy with a lot of items. Use with caution.

If itemSize is omitted or set to null, the scroller switches to variable size mode. In that case, each item must expose a numeric field with its size.

You can also pass itemSize as a resolver function with the signature (item, index) => number when the size is already available in memory but not stored under a single field name.

WARNING

You still need to set the size of the items with CSS correctly (with classes for example).

Use the sizeField prop, which defaults to 'size', to choose the field that stores that value when itemSize is null.

gridItems still requires a fixed numeric itemSize. Function-based item sizes are not supported in grid mode.

See the dedicated Function itemSize demo for a live example that derives row heights from item content and UI state.

Example:

js
const items = [
  {
    id: 1,
    label: 'Title',
    size: 64,
  },
  {
    id: 2,
    label: 'Foo',
    size: 32,
  },
  {
    id: 3,
    label: 'Bar',
    size: 32,
  },
]

Buffer

Use the buffer prop, in pixels, to render a little beyond the visible viewport. For example, a buffer of 1000 means the scroller starts rendering items 1000 pixels below the current viewport and keeps items 1000 pixels above it mounted as well.

The default value is 200.

html
<RecycleScroller :buffer="200" />

Server-Side Rendering

The prerender prop can be set as the number of items to render on the server inside the virtual scroller:

html
<RecycleScroller
  :items="items"
  :item-size="42"
  :prerender="10"
>

Released under the MIT License.