How do you optimize large tables?
1. Don't render everything at once: pagination
The simplest approach:
- show only part of the data (10-50 rows per page)
- the rest, through buttons/pagination/"show more"
<tr v-for="row in paginatedRows" :key="row.id">
...
</tr>const page = ref(1)
const perPage = 50
const paginatedRows = computed(() => {
const start = (page.value - 1) * perPage
return rows.value.slice(start, start + perPage)
})Even better: server-side pagination, fetching only the needed page from the backend.
2. Virtualization / windowing (viewport-only rendering)
For very large tables (10k+ rows) you need virtual scrolling:
- only the visible rows plus a small buffer above/below are rendered in the DOM
- the rest is just empty space with the correct height
In Vue, libraries like these are usually used:
vue-virtual-scrollervue-virtual-scroll-list
An example of the idea (illustrative):
<VirtualList
:data-key="'id'"
:data-sources="rows"
:data-component="RowComponent"
:kept-alive="50"
/>This drastically reduces the number of DOM elements.
3. Minimize data reactivity
If the table simply displays data and the rows themselves aren't edited:
- you don't need to make every row super-reactive
- you can use:
markRawfor large objects/arraysshallowReffor the whole list
import { shallowRef, markRaw } from 'vue'
const rows = shallowRef([])
// if huge objects arrive:
rows.value = markRaw(bigArray)This way Vue won't deeply track every nested field.
4. A proper key in v-for
Always:
<tr v-for="row in rows" :key="row.id">- a stable, unique
key→ Vue reuses rows - fewer DOM element recreations
- better performance during filtering/sorting
Don't use the index as the key (:key="index") if rows can be added/removed/reordered.
5. Heavy logic goes into computed, not the template
Don't do this:
<td>{{ formatPrice(row.price) }}</td>
<td>{{ complexCalculation(row) }}</td>Better:
- precompute the data ahead of time in
computed - or prepare the fields before passing them into the table
const preparedRows = computed(() =>
rows.value.map(row => ({
...row,
priceLabel: formatPrice(row.price),
total: calcTotal(row)
}))
)6. Don't create new objects/functions in the template
Every render:
<Cell :options="{ align: 'right' }" />creates a new object → Vue thinks the prop changed → unnecessary updates.
Extract such data:
const rightAlignOptions = { align: 'right' }<Cell :options="rightAlignOptions" />7. Optimize filtering and sorting
- filters/search → debounce on input
- sorting/filtering → in
computed, not in awatch deepover the whole array - for huge data, delegate sorting and filtering to the server
const debouncedQuery = ref('')
watch(query, debounce((v) => {
debouncedQuery.value = v
}, 300))8. Simplify cell content
Inside <td>:
- fewer complex components
- don't render what the user won't see anyway (conditional blocks, expand-on-click)
- lazy details: expand details/detail cards only on request
9. Don't pull everything into the global store "just in case"
If a huge table is stored in the global store:
- any store change can trigger many components
- it's better to keep such data local to the page/module
10. Visually: skeletons and lazy-loading
From a UX standpoint:
- show a skeleton instead of the full table
- load data in chunks
- don't block the UI
This isn't strictly a "render optimization", but it greatly improves perception.
How to phrase this in an interview (briefly):
To optimize large tables in Vue, I:
- use pagination or virtual scrolling so I don't render thousands of rows at once;
- minimize reactivity (shallowRef/markRaw for large structures);
- watch the
keyinv-forso Vue reuses rows;- move heavy computations into
computedand avoid creating new objects/functions in the template;- when needed, do filtering and sorting on the server, and debounce input.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.