Vue 3 Performance Optimization: Practical Techniques That Actually Matter
Optimize Vue 3 applications with techniques that make a real difference — lazy loading, virtual scrolling, memoization, and smart reactivity patterns.
Strategic Systems Architect & Enterprise Software Developer
Most Vue 3 performance advice starts with "use v-once and v-memo" and stops there. Those directives have their place, but they solve a narrow set of problems. The performance issues I see in real production applications are almost always structural — unnecessary re-renders caused by how the component tree is organized, bloated bundles from eager loading, or reactive state that tracks far more than it needs to.
Here is what actually moves the needle when a Vue 3 application starts feeling slow.
Understand What Vue Is Actually Re-rendering
Before optimizing anything, you need to see what is happening. Vue DevTools has a performance tab that highlights component re-renders in real time. Turn it on and interact with your application. You will likely be surprised by how many components re-render in response to a single state change.
The most common cause of unnecessary re-renders is passing reactive objects as props when only a primitive value is needed. If a parent component has a reactive user object and passes it to a child, that child will re-render whenever any property on the user object changes — not just the properties the child actually uses.
<!-- Instead of this -->
<UserAvatar :user="user" />
<!-- Pass only what the component needs -->
<UserAvatar :name="user.name" :avatar-url="user.avatarUrl" />
This is the single most impactful change you can make in most applications. It is not glamorous, but it eliminates the majority of wasted renders. The Composition API makes this pattern easier to maintain because you can structure reactive state around logical concerns rather than dumping everything into a monolithic object.
The shallowRef and shallowReactive functions are underused tools for the same problem. If you have a large object that gets replaced entirely — like a response from an API — wrapping it in shallowRef means Vue only tracks the reference itself, not every nested property. Deep reactivity is the default because it is safer, but it is expensive for large data structures.
Lazy Loading and Code Splitting
Vue's defineAsyncComponent and Nuxt's auto-imported lazy prefix let you defer loading components until they are needed. The mistake I see most often is applying lazy loading indiscriminately. Loading a 2KB button component asynchronously adds overhead that exceeds the savings. Lazy loading pays off for heavy components — rich text editors, chart libraries, complex forms — that are not needed on initial render.
const HeavyEditor = defineAsyncComponent(() =>
import('./components/HeavyEditor.vue')
)
Route-level code splitting matters more than component-level splitting for most applications. In Nuxt, this happens automatically through the file-based routing system. If you are using Vue Router directly, make sure every route uses dynamic imports. A single eagerly imported route can pull an entire feature's dependencies into the main bundle.
Beyond components, look at third-party library imports. A single import _ from 'lodash' pulls in the entire library. Use named imports from specific subpaths, or better yet, use native JavaScript methods. I wrote more about reducing bundle size for better performance if this is a concern in your project.
Virtual Scrolling and List Rendering
Rendering long lists is where Vue performance problems become visible to users. A list of 500 items with moderately complex components will cause noticeable jank on scroll and interaction. The solution is virtual scrolling — only rendering the items currently visible in the viewport plus a small buffer.
Libraries like vue-virtual-scroller handle the mechanics well. The key implementation detail that trips people up is item height. If your list items have variable heights, you need to either measure them dynamically or provide an estimate function. Fixed-height items are significantly easier to virtualize and perform better.
<RecycleScroller
:items="items"
:item-size="72"
key-field="id"
v-slot="{ item }"
>
<ListItem :item="item" />
</RecycleScroller>
For simpler cases where you just need to avoid rendering off-screen content, the native content-visibility: auto CSS property is surprisingly effective. It tells the browser to skip rendering for off-screen elements entirely, and it requires zero JavaScript.
When dealing with pagination versus infinite scroll, performance considerations should drive the decision as much as UX preferences. Pagination naturally limits the DOM size, while infinite scroll requires virtual scrolling to stay performant at scale.
Computed Properties and Memoization
Computed properties in Vue are memoized — they only re-evaluate when their dependencies change. But there is a subtlety that causes performance problems: if a computed property depends on a reactive array or object, it re-evaluates whenever anything in that array or object changes, even if the change does not affect the computed result.
// This re-evaluates on ANY change to the items array
const expensiveComputed = computed(() => {
return items.value.filter(item => item.category === 'active')
.sort((a, b) => b.score - a.score)
.slice(0, 10)
})
If items is large and changes frequently — say, from a real-time WebSocket feed — this computed property runs the full filter-sort-slice pipeline on every update. The fix is to break the computation into stages. Use a separate computed for the filtered set and another for the sorted-and-sliced result. If only the sorting criteria changes but not the filter, the filter computation is skipped.
For truly expensive computations that do not map cleanly to Vue's reactivity system, useMemoize from VueUse provides a general-purpose memoization wrapper. But reach for it only after you have confirmed the computation is actually expensive with profiling. Premature memoization adds complexity without measurable benefit.
The Pinia state management guide covers related patterns for keeping store getters efficient — the same principles apply, but the execution context differs enough that it is worth reviewing separately.
Measure Before and After
The most important optimization technique is not a technique at all — it is measurement. Use the Performance tab in Chrome DevTools to record interactions. Use Lighthouse for load performance. Use Vue DevTools for render tracking. Every optimization should have a measurable before-and-after. If you cannot measure the improvement, the optimization is either unnecessary or you are measuring the wrong thing.
Performance work is iterative. Fix the biggest bottleneck, measure again, and fix the next one. The diminishing returns come fast, and knowing when to stop is as valuable as knowing where to start.