Bird
Raised Fist0
Vueframework~7 mins

Virtual scrolling for large lists in Vue

Choose your learning style10 modes available
Introduction

Virtual scrolling helps show only a small part of a big list on screen. This makes the app faster and smoother.

When you have a very long list of items to show, like thousands of messages.
When loading all items at once makes the page slow or uses too much memory.
When you want to improve scrolling performance on phones or slow devices.
When you want to save bandwidth by loading only visible items.
When you want a smooth user experience with big data lists.
Syntax
Vue
<template>
  <div ref="scrollContainer" @scroll="onScroll" style="height: 300px; overflow-y: auto;">
    <div :style="{ height: totalHeight + 'px', position: 'relative' }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        :style="{ position: 'absolute', top: itemPositions[item.index] + 'px', height: itemHeight + 'px', width: '100%' }"
      >
        {{ item.text }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'

const props = defineProps({
  items: Array,
  itemHeight: Number
})

const scrollTop = ref(0)
const scrollContainer = ref(null)

const visibleCount = computed(() => Math.ceil(300 / props.itemHeight) + 1)
const totalHeight = computed(() => props.items.length * props.itemHeight)

const startIndex = computed(() => Math.floor(scrollTop.value / props.itemHeight))

const visibleItems = computed(() => {
  return props.items.slice(startIndex.value, startIndex.value + visibleCount.value).map((item, index) => ({
    ...item,
    index: startIndex.value + index
  }))
})

const itemPositions = computed(() => {
  const positions = {}
  for (let i = startIndex.value; i < startIndex.value + visibleCount.value; i++) {
    positions[i] = i * props.itemHeight
  }
  return positions
})

function onScroll() {
  scrollTop.value = scrollContainer.value.scrollTop
}

onMounted(() => {
  scrollTop.value = 0
})
</script>

The container must have a fixed height and overflow-y set to auto or scroll.

Each item is absolutely positioned inside a tall container to simulate full list height.

Examples
This example shows what happens if the list is empty. No items appear but the container still works.
Vue
<template>
  <div ref="scrollContainer" @scroll="onScroll" style="height: 300px; overflow-y: auto;">
    <div :style="{ height: totalHeight + 'px', position: 'relative' }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        :style="{ position: 'absolute', top: itemPositions[item.index] + 'px', height: itemHeight + 'px', width: '100%' }"
      >
        {{ item.text }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const items = ref([]) // empty list
const itemHeight = 30

const scrollTop = ref(0)
const scrollContainer = ref(null)

const visibleCount = computed(() => Math.ceil(300 / itemHeight) + 1)
const totalHeight = computed(() => items.value.length * itemHeight)

const startIndex = computed(() => Math.floor(scrollTop.value / itemHeight))

const visibleItems = computed(() => {
  return items.value.slice(startIndex.value, startIndex.value + visibleCount.value).map((item, index) => ({
    ...item,
    index: startIndex.value + index
  }))
})

const itemPositions = computed(() => {
  const positions = {}
  for (let i = startIndex.value; i < startIndex.value + visibleCount.value; i++) {
    positions[i] = i * itemHeight
  }
  return positions
})

function onScroll() {
  scrollTop.value = scrollContainer.value.scrollTop
}
</script>
This example shows a list with only one item. The item is positioned at the top and visible.
Vue
<template>
  <div ref="scrollContainer" @scroll="onScroll" style="height: 300px; overflow-y: auto;">
    <div :style="{ height: totalHeight + 'px', position: 'relative' }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        :style="{ position: 'absolute', top: itemPositions[item.index] + 'px', height: itemHeight + 'px', width: '100%' }"
      >
        {{ item.text }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const items = ref([{ id: 1, text: 'Only one item' }])
const itemHeight = 30

const scrollTop = ref(0)
const scrollContainer = ref(null)

const visibleCount = computed(() => Math.ceil(300 / itemHeight) + 1)
const totalHeight = computed(() => items.value.length * itemHeight)

const startIndex = computed(() => Math.floor(scrollTop.value / itemHeight))

const visibleItems = computed(() => {
  return items.value.slice(startIndex.value, startIndex.value + visibleCount.value).map((item, index) => ({
    ...item,
    index: startIndex.value + index
  }))
})

const itemPositions = computed(() => {
  const positions = {}
  for (let i = startIndex.value; i < startIndex.value + visibleCount.value; i++) {
    positions[i] = i * itemHeight
  }
  return positions
})

function onScroll() {
  scrollTop.value = scrollContainer.value.scrollTop
}
</script>
This example shows a large list of 1000 items. Only visible items are rendered for smooth scrolling.
Vue
<template>
  <div ref="scrollContainer" @scroll="onScroll" style="height: 300px; overflow-y: auto;">
    <div :style="{ height: totalHeight + 'px', position: 'relative' }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        :style="{ position: 'absolute', top: itemPositions[item.index] + 'px', height: itemHeight + 'px', width: '100%' }"
      >
        {{ item.text }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const items = ref(Array.from({ length: 1000 }, (_, i) => ({ id: i + 1, text: `Item ${i + 1}` })))
const itemHeight = 30

const scrollTop = ref(0)
const scrollContainer = ref(null)

const visibleCount = computed(() => Math.ceil(300 / itemHeight) + 1)
const totalHeight = computed(() => items.value.length * itemHeight)

const startIndex = computed(() => Math.floor(scrollTop.value / itemHeight))

const visibleItems = computed(() => {
  return items.value.slice(startIndex.value, startIndex.value + visibleCount.value).map((item, index) => ({
    ...item,
    index: startIndex.value + index
  }))
})

const itemPositions = computed(() => {
  const positions = {}
  for (let i = startIndex.value; i < startIndex.value + visibleCount.value; i++) {
    positions[i] = i * itemHeight
  }
  return positions
})

function onScroll() {
  scrollTop.value = scrollContainer.value.scrollTop
}
</script>
Sample Program

This Vue component shows a list of 1000 items using virtual scrolling. Only the items visible in the 300px tall container are rendered. Each item is 40px tall and positioned absolutely inside a tall container. Scrolling updates which items are visible.

Keyboard navigation is supported by adding tabindex and role attributes for accessibility.

Vue
<template>
  <div>
    <h2>Virtual Scrolling List</h2>
    <div ref="scrollContainer" @scroll="onScroll" style="height: 300px; overflow-y: auto; border: 1px solid #ccc;">
      <div :style="{ height: totalHeight + 'px', position: 'relative' }">
        <div
          v-for="item in visibleItems"
          :key="item.id"
          :style="{ position: 'absolute', top: itemPositions[item.index] + 'px', height: itemHeight + 'px', width: '100%', padding: '0.5rem', boxSizing: 'border-box', borderBottom: '1px solid #eee', backgroundColor: item.index % 2 === 0 ? '#fafafa' : '#fff' }"
          tabindex="0"
          role="listitem"
          aria-label="List item"
        >
          {{ item.text }}
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'

const itemHeight = 40
const items = ref(Array.from({ length: 1000 }, (_, i) => ({ id: i + 1, text: `Item number ${i + 1}` })))

const scrollTop = ref(0)
const scrollContainer = ref(null)

const visibleCount = computed(() => Math.ceil(300 / itemHeight) + 1)
const totalHeight = computed(() => items.value.length * itemHeight)

const startIndex = computed(() => Math.floor(scrollTop.value / itemHeight))

const visibleItems = computed(() => {
  return items.value.slice(startIndex.value, startIndex.value + visibleCount.value).map((item, index) => ({
    ...item,
    index: startIndex.value + index
  }))
})

const itemPositions = computed(() => {
  const positions = {}
  for (let i = startIndex.value; i < startIndex.value + visibleCount.value; i++) {
    positions[i] = i * itemHeight
  }
  return positions
})

function onScroll() {
  scrollTop.value = scrollContainer.value.scrollTop
}

onMounted(() => {
  scrollTop.value = 0
})
</script>
OutputSuccess
Important Notes

Virtual scrolling reduces rendering work by only showing visible items, improving speed.

Time complexity for rendering is O(visibleCount), not total items.

Common mistake: forgetting to set container height or overflow, which breaks scrolling.

Use virtual scrolling when lists are very large; for small lists normal rendering is simpler.

Summary

Virtual scrolling shows only visible items in a large list to improve performance.

It uses a fixed height container and absolute positioning inside a tall wrapper.

Scroll events update which items are rendered dynamically.

Practice

(1/5)
1. What is the main purpose of virtual scrolling in Vue applications when displaying large lists?
easy
A. To sort the list items automatically
B. To render only visible items to improve performance
C. To disable scrolling for better user control
D. To load all items at once for faster access

Solution

  1. Step 1: Understand virtual scrolling concept

    Virtual scrolling renders only the visible portion of a large list instead of all items at once.
  2. Step 2: Identify the benefit in Vue apps

    This reduces rendering load and improves performance when dealing with many items.
  3. Final Answer:

    To render only visible items to improve performance -> Option B
  4. Quick Check:

    Virtual scrolling = Render visible items only [OK]
Hint: Virtual scrolling shows visible items only to boost speed [OK]
Common Mistakes:
  • Thinking it loads all items at once
  • Believing it disables scrolling
  • Assuming it sorts items automatically
2. Which Vue template snippet correctly sets up a container for virtual scrolling with fixed height and overflow?
easy
A.
B.
C.
D.

Solution

  1. Step 1: Identify container style for virtual scrolling

    The container must have a fixed height and vertical scrolling enabled (overflow-y: auto).
  2. Step 2: Match the correct style snippet

    <div style="height: 300px; overflow-y: auto;"></div> sets height to 300px and overflow-y to auto, enabling vertical scroll inside fixed height.
  3. Final Answer:

    <div style="height: 300px; overflow-y: auto;"></div> -> Option A
  4. Quick Check:

    Fixed height + vertical scroll = <div style="height: 300px; overflow-y: auto;"></div> [OK]
Hint: Fixed height + overflow-y:auto enables virtual scroll container [OK]
Common Mistakes:
  • Using overflow-x instead of overflow-y
  • Setting height to auto instead of fixed
  • Using position fixed incorrectly
3. Given this Vue code snippet for virtual scrolling, what will be rendered when scrollTop is 200, itemHeight is 50, and visibleCount is 10?
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = startIndex + visibleCount;
const visibleItems = items.slice(startIndex, endIndex);
medium
A. Items from index 4 to 13
B. Items from index 0 to 4
C. Items from index 200 to 250
D. Items from index 50 to 100

Solution

  1. Step 1: Calculate startIndex from scrollTop and itemHeight

    startIndex = floor(200 / 50) = 4.
  2. Step 2: Determine endIndex using visibleCount (assume 10)

    endIndex = 4 + 10 = 14, so slice is items[4..13].
  3. Final Answer:

    Items from index 4 to 13 -> Option A
  4. Quick Check:

    startIndex = 4, endIndex = 14 slice = 4 to 13 [OK]
Hint: Divide scrollTop by itemHeight for start index [OK]
Common Mistakes:
  • Using scrollTop directly as index
  • Confusing endIndex with startIndex
  • Ignoring floor function
4. Identify the bug in this Vue virtual scrolling code snippet:
const startIndex = Math.ceil(scrollTop / itemHeight);
const endIndex = startIndex + visibleCount;
const visibleItems = items.slice(startIndex, endIndex);
medium
A. Missing reactive state for scrollTop
B. Using slice instead of splice causes mutation
C. Incorrect calculation of endIndex
D. Using Math.ceil causes skipping the first visible item

Solution

  1. Step 1: Analyze use of Math.ceil for startIndex

    Math.ceil rounds up, so if scrollTop/itemHeight is exactly 4, startIndex becomes 4, but if 3.1 it becomes 4, skipping some items.
  2. Step 2: Understand impact on visible items

    This causes the first partially visible item to be skipped, leading to missing items during scroll.
  3. Final Answer:

    Using Math.ceil causes skipping the first visible item -> Option D
  4. Quick Check:

    Math.ceil skips partially visible items [OK]
Hint: Use Math.floor, not Math.ceil, for start index [OK]
Common Mistakes:
  • Confusing slice with splice
  • Ignoring reactive state importance
  • Miscalculating endIndex
5. You want to implement virtual scrolling for a list of 10,000 items in Vue 3 using the Composition API. Which approach best ensures smooth scrolling and correct item rendering?
hard
A. Render all 10,000 items inside a scrollable div and rely on browser optimization
B. Use v-if to render only the first 100 items regardless of scroll position
C. Use a fixed height container with overflow-y:auto, calculate visible items on scroll, and absolutely position items inside a tall wrapper
D. Use a dynamic height container that grows as user scrolls down

Solution

  1. Step 1: Understand virtual scrolling requirements

    Virtual scrolling needs a fixed height container with scroll enabled and dynamic rendering of visible items.
  2. Step 2: Identify correct rendering technique

    Absolutely positioning items inside a tall wrapper simulates full list height while rendering only visible items improves performance.
  3. Step 3: Evaluate other options

    Rendering all items or ignoring scroll position causes lag; dynamic height container breaks scroll behavior.
  4. Final Answer:

    Use a fixed height container with overflow-y:auto, calculate visible items on scroll, and absolutely position items inside a tall wrapper -> Option C
  5. Quick Check:

    Fixed container + dynamic visible items + absolute positioning = Use a fixed height container with overflow-y:auto, calculate visible items on scroll, and absolutely position items inside a tall wrapper [OK]
Hint: Fixed height + absolute positioning + scroll event = smooth virtual scroll [OK]
Common Mistakes:
  • Rendering all items causing slow UI
  • Ignoring scroll position for rendering
  • Using dynamic height container breaking scroll
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard