Handling large lists efficiently in Vue 3
Jakub Andrzejewski

Jakub Andrzejewski @jacobandrewsky

About: Senior Fullstack Developer & Advocate • @GoogleDevExpert in Web Performance • @nuxt_js Team • Ambassador @Storyblok, @algolia, @cloudinary, @supabase • Teaching Vue | Nuxt | Perf

Location:
Wrocław, Poland
Joined:
Jun 20, 2021

Handling large lists efficiently in Vue 3

Publish Date: Mar 31
32 5

Handling large lists in Vue 3 efficiently is crucial for maintaining smooth performance and a great user experience. When dealing with thousands (or even millions) of items, a naive rendering approach can slow down or crash the application.

In this article, we’ll explore the best techniques to optimize large lists in Vue 3.

Enjoy!

🤔 Common Performance Issues with Large Lists

Before diving into solutions, let's identify key performance problems:

  • Rendering Too Many Elements: The browser struggles to handle excessive DOM nodes.
  • Slow Re-renders: Frequent updates to large lists can cause slow performance.
  • Inefficient Event Handling: Using events like click or mouseover on every list item can cause excessive computation.
  • Memory Consumption: Storing too much data in memory can cause high RAM usage.

Now, let’s look at techniques to solve these issues.

🟢 Handling large lists efficiently in Vue

1. Use Virtual Scrolling for Large Lists

Instead of rendering the entire list, only visible items are rendered. As the user scrolls, items are dynamically loaded and unloaded. Let's take a look at the following example with VueUse’s useVirtualList:

<script setup>
import { ref } from 'vue';
import { useVirtualList } from '@vueuse/core';

const items = ref(Array.from({ length: 100000 }, (_, i) => `Item ${i + 1}`));
const { list, containerProps, wrapperProps } = useVirtualList(items, { itemHeight: 50 });
</script>

<template>
  <div v-bind="containerProps" style="height: 300px; overflow-y: auto;">
    <div v-bind="wrapperProps">
      <div v-for="item in list" :key="item.index" class="list-item">
        {{ item.data }}
      </div>
    </div>
  </div>
</template>

<style>
.list-item {
  height: 50px;
  display: flex;
  align-items: center;
  border-bottom: 1px solid #ccc;
}
</style>
Enter fullscreen mode Exit fullscreen mode

This efficiently displays only the items visible in the viewport.

2. Optimize v-for with key

When using v-for, always provide a unique key to help Vue optimize re-renders. Using an index as the key can cause unintended re-renders when modifying the list.

<li v-for="item in items" :key="item.id">{{ item.name }}</li>

<!-- ❌ Not Recommended -->
<li v-for="(item, index) in items" :key="index">{{ item.name }}</li>
Enter fullscreen mode Exit fullscreen mode

3. Use Lazy Loading for Data Fetching

Instead of loading all data at once, fetch items in small batches.

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

const items = ref([]);
const page = ref(1);
const loading = ref(false);

const fetchMoreItems = async () => {
  if (loading.value) return;
  loading.value = true;

  // Simulating API call
  setTimeout(() => {
    for (let i = 1; i <= 20; i++) {
      items.value.push(`Item ${items.value.length + 1}`);
    }
    loading.value = false;
    page.value++;
  }, 1000);
};

onMounted(fetchMoreItems);
</script>

<template>
  <ul>
    <li v-for="item in items" :key="item">{{ item }}</li>
  </ul>
  <button @click="fetchMoreItems" :disabled="loading">
    Load More
  </button>
</template>
Enter fullscreen mode Exit fullscreen mode

This ensures that only the necessary data is loaded as needed.

4. Debounce Input Search for Filtering Lists

When filtering a large list based on user input, debounce the search to reduce unnecessary calculations. Let's take a look at the following example with VueUse's useDebounceFn:

<script setup>
<script setup>
import { ref, computed } from 'vue';
import { useDebounceFn } from '@vueuse/core';

const items = ref(Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`));
const searchQuery = ref('');

const filteredItems = computed(() => {
  return items.value.filter(item => item.toLowerCase().includes(searchQuery.value.toLowerCase()));
});

const updateSearch = useDebounceFn((value) => {
  searchQuery.value = value;
}, 300);
</script>

<template>
  <input type="text" @input="(e) => updateSearch(e.target.value)" placeholder="Search..." />
  <ul>
    <li v-for="item in filteredItems" :key="item">{{ item }}</li>
  </ul>
</template>
Enter fullscreen mode Exit fullscreen mode

This reduces the number of filtering operations while typing.

5. Paginate Large Lists Instead of Rendering All at Once

Instead of displaying all items at once, break them into pages.

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

const items = ref(Array.from({ length: 1000 }, (_, i) => `Item ${i + 1}`));
const currentPage = ref(1);
const itemsPerPage = 20;

const paginatedItems = computed(() => {
  const start = (currentPage.value - 1) * itemsPerPage;
  return items.value.slice(start, start + itemsPerPage);
});
</script>

<template>
  <ul>
    <li v-for="item in paginatedItems" :key="item">{{ item }}</li>
  </ul>
  <button @click="currentPage--" :disabled="currentPage === 1">Previous</button>
  <button @click="currentPage++" :disabled="currentPage * itemsPerPage >= items.length">Next</button>
</template>
Enter fullscreen mode Exit fullscreen mode

Pagination improves performance by displaying only a subset of the data at a time.

📖 Learn more

If you would like to learn more about Vue, Nuxt, JavaScript or other useful technologies, checkout VueSchool by clicking this link or by clicking the image below:

Vue School Link

It covers most important concepts while building modern Vue or Nuxt applications that can help you in your daily work or side projects 😉

✅ Summary

Efficiently handling large lists in Vue 3 requires a combination of all the techniques mentioned above. By implementing them, you can ensure your Vue 3 applications remain fast and responsive, even with massive datasets.

Take care and see you next time!

And happy coding as always 🖥️

Comments 5 total

  • Peter Vivo
    Peter VivoMar 31, 2025

    This method is total work in any FW or no-FW, nice job.

  • Alois Sečkár
    Alois SečkárMar 31, 2025

    lazy-loading can also be easilly achieved with vueuse.org/core/useInfiniteScroll

    it works like in social media feeds - you just keep scrolling down the page and the new results keep appearing - but ofc it is not suitable for all purposes - e.g. if you actually need to be able to display certain "page" - then your pagination solution is indeed better

  • Avelino Nogueira
    Avelino NogueiraApr 1, 2025

    Is there any performance issues with
    <li v-for="(item, index) in items" :key="somelist-${index}">?

    • Jakub Andrzejewski
      Jakub AndrzejewskiApr 2, 2025

      Haven't benchmarked that but I suppose it will work better than just the plain index. But if you can, I would recommend to use an id that is fully unique rather than 0,1,2,3,4 :)

    • Miko Klarzuk
      Miko KlarzukJun 25, 2025

      "You should avoid using array indexes as keys since indexes represent an item's position in the array, not the item’s unique identity." link from vueschool article

Add comment