If you’ve ever loaded a table with 10,000 rows in Vue 3 and watched the browser tab freeze, memory usage spike, and the CPU fan spin up like a jet engine, you’ve fallen into the reactivity trap.
Vue 3’s reactivity system, powered by ES6 Proxies, is incredibly elegant. It "just works" for 95% of use cases. But when you start dealing with large datasets—think financial dashboards, log viewers, or complex data grids—that elegance becomes a massive performance bottleneck.
In this deep dive, we’ll explore exactly why Vue’s default reactivity chokes on large arrays, how to measure the impact, and the production-ready patterns to fix it.
The Naive Approach: Deep Reactivity by Default
Let's look at a common scenario. You fetch a large array of objects from an API and store it in a ref.
<script setup>
import { ref, onMounted } from 'vue'
const logs = ref([])
onMounted(async () => {
const response = await fetch('/api/system-logs')
// Assume this returns 50,000 log objects
logs.value = await response.json()
})
</script>
<template>
<div v-for="log in logs" :key="log.id">
{{ log.timestamp }} - {{ log.message }}
</div>
</template>
Why This Breaks in Production
When you assign that 50,000-item array to logs.value, Vue doesn't just store the array. It recursively walks through every single object and every single nested property, wrapping them in ES6 Proxies to track changes.
If each log object has 10 properties, Vue just created 500,000 Proxies.
This deep conversion takes significant synchronous CPU time, blocking the main thread. The browser freezes. Furthermore, each Proxy consumes memory. A 5MB JSON payload can easily balloon into 50MB of reactive overhead in RAM.
The Fix: Opting Out of Deep Reactivity
If you are rendering a massive list, ask yourself: Do I actually need to mutate individual properties of these objects?
In most cases (like a log viewer or a data table), the answer is no. You might replace the entire list, or append to it, but you aren't doing logs.value[4021].message = 'new message'.
Pattern 1: shallowRef
The easiest and most effective fix is to use shallowRef instead of ref.
<script setup>
import { shallowRef, onMounted } from 'vue'
// Only the .value reassignment is tracked.
// The array elements themselves remain plain objects.
const logs = shallowRef([])
onMounted(async () => {
const response = await fetch('/api/system-logs')
logs.value = await response.json()
})
// This WILL trigger a re-render:
const refreshLogs = (newLogs) => {
logs.value = newLogs
}
// This WILL NOT trigger a re-render (and won't work reactively):
const mutateSingleLog = () => {
logs.value[0].message = 'Updated'
}
</script>
By using shallowRef, Vue only tracks the .value property. It skips the recursive Proxy generation entirely. The performance difference is staggering—what took 800ms to process now takes 5ms.
Pattern 2: markRaw for Mixed State
Sometimes you have a reactive object (like a Pinia store or a complex component state) where most properties need deep reactivity, but one specific property holds a massive dataset.
You can't use shallowRef inside a reactive object. Instead, use markRaw.
<script setup>
import { reactive, markRaw } from 'vue'
const state = reactive({
isLoading: false,
filterQuery: '',
// Tell Vue: "Never wrap this specific array in Proxies"
largeDataset: markRaw([])
})
const loadData = async () => {
state.isLoading = true
const data = await fetchLargeData()
// We must mark the new array as raw before assignment
state.largeDataset = markRaw(data)
state.isLoading = false
}
</script>
markRaw adds a hidden __v_skip flag to the object, instructing Vue's reactivity system to ignore it.
The Next Bottleneck: DOM Rendering
Fixing the reactivity overhead solves the memory and CPU spike during data assignment. But if you try to render 50,000 <div> elements, the browser will still crash. The DOM is inherently slow.
The Solution: Virtualization (Windowing)
To handle massive lists, you must use virtualization. This technique only renders the DOM nodes that are currently visible in the viewport, plus a small buffer. As the user scrolls, the DOM nodes are recycled and updated with new data.
Instead of building this from scratch, use a proven library like @vueuse/core (specifically useVirtualList) or vue-virtual-scroller.
Here is how you combine shallowRef with VueUse's useVirtualList:
<script setup>
import { shallowRef } from 'vue'
import { useVirtualList } from '@vueuse/core'
// 1. Fast reactivity
const massiveList = shallowRef(Array.from({ length: 50000 }, (_, i) => ({
id: i,
text: `Item ${i}`
})))
// 2. Fast DOM rendering
const { list, containerProps, wrapperProps } = useVirtualList(
massiveList,
{
itemHeight: 40, // Fixed height is most performant
overscan: 10 // Render 10 items outside viewport for smooth scrolling
}
)
</script>
<template>
<!-- The scrollable container -->
<div v-bind="containerProps" style="height: 400px; overflow-y: auto;">
<!-- The wrapper that simulates the full height -->
<div v-bind="wrapperProps">
<!-- Only the visible items are rendered -->
<div
v-for="item in list"
:key="item.data.id"
style="height: 40px;"
>
{{ item.data.text }}
</div>
</div>
</div>
</template>
Conclusion
Vue 3's reactivity is powerful, but with great power comes the responsibility to know when to turn it off.
-
Default to
reffor primitives and small objects. -
Switch to
shallowRefthe moment you are dealing with large arrays or complex objects that don't require deep mutation tracking. -
Use
markRawwhen you need to embed non-reactive massive datasets inside a deeply reactive state object. - Always virtualize the DOM when rendering lists longer than a few hundred items.
By combining shallowRef and virtualization, you can render lists of millions of items in Vue 3 without dropping a single frame.
What's your approach?
Have you run into reactivity bottlenecks in your Vue applications? Did you use shallowRef, or did you find another workaround? Let me know in the comments!
About the Author: Ameer Hamza is a Top-Rated Full-Stack Developer with 7+ years of experience building SaaS platforms, eCommerce solutions, and AI-powered applications. He specializes in Laravel, Vue.js, React, Next.js, and AI integrations — with 50+ projects shipped and a 100% job success rate. Check out his portfolio at ameer.pk to see his latest work, or reach out for your next development project.
Top comments (0)