How to optimize large lists in Vue
Large lists cause performance issues through excessive DOM nodes, re-rendering overhead, and memory consumption when displaying thousands of items simultaneously. As the creator of CoreUI, a widely used open-source UI library, I’ve optimized large data tables and lists throughout my 12 years of frontend development since 2014. The most effective approach is combining pagination, virtual scrolling, computed property caching, and v-memo directive to minimize rendering and reactivity overhead. This method reduces DOM size, prevents unnecessary re-renders, and maintains smooth scrolling performance even with massive datasets.
Implement pagination with computed filters and Object.freeze for immutable data optimization.
<script setup>
import { ref, computed } from 'vue'
const items = ref([])
const currentPage = ref(1)
const itemsPerPage = ref(50)
const searchQuery = ref('')
const sortKey = ref('name')
const sortOrder = ref('asc')
const loadItems = async () => {
const response = await fetch('/api/items?limit=10000')
const data = await response.json()
items.value = data.map(item => Object.freeze(item))
}
const filteredItems = computed(() => {
let result = items.value
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
result = result.filter(item =>
item.name.toLowerCase().includes(query) ||
item.description.toLowerCase().includes(query)
)
}
result = [...result].sort((a, b) => {
const aVal = a[sortKey.value]
const bVal = b[sortKey.value]
const order = sortOrder.value === 'asc' ? 1 : -1
if (aVal < bVal) return -1 * order
if (aVal > bVal) return 1 * order
return 0
})
return result
})
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * itemsPerPage.value
const end = start + itemsPerPage.value
return filteredItems.value.slice(start, end)
})
const totalPages = computed(() => {
return Math.ceil(filteredItems.value.length / itemsPerPage.value)
})
const changePage = (page) => {
currentPage.value = page
window.scrollTo(0, 0)
}
const changeSort = (key) => {
if (sortKey.value === key) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
} else {
sortKey.value = key
sortOrder.value = 'asc'
}
currentPage.value = 1
}
loadItems()
</script>
<template>
<div class='list-container'>
<div class='controls'>
<input
v-model='searchQuery'
type='text'
placeholder='Search items...'
class='search-input'
/>
<select v-model='itemsPerPage' @change='currentPage = 1'>
<option :value='25'>25 per page</option>
<option :value='50'>50 per page</option>
<option :value='100'>100 per page</option>
</select>
</div>
<div class='results-info'>
Showing {{ ((currentPage - 1) * itemsPerPage) + 1 }}
to {{ Math.min(currentPage * itemsPerPage, filteredItems.length) }}
of {{ filteredItems.length }} items
</div>
<table class='data-table'>
<thead>
<tr>
<th @click='changeSort("id")'>
ID {{ sortKey === 'id' ? (sortOrder === 'asc' ? '↑' : '↓') : '' }}
</th>
<th @click='changeSort("name")'>
Name {{ sortKey === 'name' ? (sortOrder === 'asc' ? '↑' : '↓') : '' }}
</th>
<th @click='changeSort("description")'>
Description {{ sortKey === 'description' ? (sortOrder === 'asc' ? '↑' : '↓') : '' }}
</th>
<th @click='changeSort("status")'>
Status {{ sortKey === 'status' ? (sortOrder === 'asc' ? '↑' : '↓') : '' }}
</th>
</tr>
</thead>
<tbody>
<tr v-for='item in paginatedItems' :key='item.id' v-memo='[item.id, item.status]'>
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.description }}</td>
<td>
<span :class='`status-badge status-${item.status}`'>
{{ item.status }}
</span>
</td>
</tr>
</tbody>
</table>
<div class='pagination'>
<button
@click='changePage(currentPage - 1)'
:disabled='currentPage === 1'
>
Previous
</button>
<template v-for='page in totalPages' :key='page'>
<button
v-if='Math.abs(page - currentPage) < 3 || page === 1 || page === totalPages'
@click='changePage(page)'
:class='{ active: page === currentPage }'
>
{{ page }}
</button>
<span v-else-if='Math.abs(page - currentPage) === 3'>...</span>
</template>
<button
@click='changePage(currentPage + 1)'
:disabled='currentPage === totalPages'
>
Next
</button>
</div>
</div>
</template>
<style scoped>
.list-container {
padding: 1rem;
}
.controls {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.search-input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
}
.data-table th {
background: #f5f5f5;
padding: 0.75rem;
text-align: left;
cursor: pointer;
user-select: none;
}
.data-table th:hover {
background: #e0e0e0;
}
.data-table td {
padding: 0.75rem;
border-bottom: 1px solid #eee;
}
.pagination {
display: flex;
gap: 0.5rem;
justify-content: center;
align-items: center;
}
.pagination button {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 4px;
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination button.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.status-badge {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.875rem;
}
.status-active {
background: #d4edda;
color: #155724;
}
.status-inactive {
background: #f8d7da;
color: #721c24;
}
</style>
Use v-memo for optimized re-rendering:
<script setup>
import { ref } from 'vue'
const users = ref([
{ id: 1, name: 'John', email: '[email protected]', clicks: 0 },
{ id: 2, name: 'Jane', email: '[email protected]', clicks: 0 },
{ id: 3, name: 'Bob', email: '[email protected]', clicks: 0 }
])
const incrementClicks = (user) => {
user.clicks++
}
</script>
<template>
<div>
<div v-for='user in users' :key='user.id' v-memo='[user.clicks]'>
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<p>Clicks: {{ user.clicks }}</p>
<button @click='incrementClicks(user)'>Increment</button>
</div>
</div>
</template>
Here Object.freeze prevents Vue from adding reactive getters and setters to large datasets that do not need reactivity. The computed properties cache filtered and sorted results, recalculating only when dependencies change. The paginatedItems computed slices the filtered array to show only current page items, drastically reducing DOM nodes. The v-memo directive skips re-rendering list items when specified dependencies have not changed, improving performance for large lists. The pagination controls limit rendered items to manageable chunks while providing navigation. The sortable columns use computed sorting to avoid mutating original array and maintain reactivity efficiency.
Best Practice Note:
This is the large list optimization strategy we use in CoreUI for Vue data tables handling thousands of records with smooth performance. Implement virtual scrolling using vue-virtual-scroller for extremely large lists where pagination is not suitable, use shallowRef for large arrays that do not need deep reactivity tracking, and consider Web Workers for heavy filtering and sorting operations to keep the main thread responsive during data processing.



