Epicmax
October 26, 2026·8 min read· by Pavel Mironov

Vue.js Performance Optimization: Practical Tips for Faster Apps

Practical Vue.js performance optimization tips: profiling, lazy loading, v-memo, shallowRef, virtual lists and Core Web Vitals fixes from Epicmax's Vue team.

Vue.jsPerformanceNuxt
Vue.js Performance Optimization: Practical Tips for Faster Apps

Vue.js performance is rarely the framework's fault: when a Vue app feels slow, the cause is almost always in how it's built. Too much JavaScript on the first load, lists that render ten thousand rows, reactive data that Vue has to track property by property. This guide covers the fixes we use on client projects, in the order we apply them: measure, then fix loading, then fix updates.

How to Measure Vue JS Performance First

Don't optimize on a hunch. Vue js performance problems fall into two groups that need different fixes: slow page loads and slow updates after the page is interactive. Measure first to find out which one you have.

For load performance, start with Lighthouse, either in Chrome DevTools or as PageSpeed Insights online. It reports Largest Contentful Paint, layout shifts and total blocking time, and points at oversized scripts and images. PageSpeed Insights also shows field data from real Chrome users when your site has enough traffic, which beats any lab score.

When a click or keystroke feels slow, open the browser Performance panel instead. Record the interaction and look for long tasks over 50 ms. With app.config.performance = true in development, Vue adds its own markers to that timeline, so you can see which component's render or patch took the time.

Vue DevTools fills in the rest: the component tree, each component's state, and its own performance profiling. A component that updates on every keystroke when nothing it displays has changed is a clear lead.

Page Load Optimizations

A page can't be used until the browser has downloaded, parsed and run its JavaScript. Ship less on the first visit and everything after gets faster.

Lazy Load Routes and Heavy Components

Load each route only when the user opens it. Vue Router accepts a function that returns a dynamic import, and the bundler splits that route into its own chunk:

const routes = [
  { path: '/', component: () => import('./views/HomeView.vue') },
  { path: '/reports', component: () => import('./views/ReportsView.vue') },
]

Vue Router's documentation recommends doing this for all routes. For heavy components inside a page, such as a chart, a rich text editor or a map, use defineAsyncComponent so they load when they first render:

import { defineAsyncComponent } from 'vue'

const SalesChart = defineAsyncComponent(() => import('./SalesChart.vue'))

Cut Bundle Size with Tree-Shaking

Most of Vue's APIs are tree-shakable: if you never use <Transition>, it isn't in your production bundle. Pre-compiling templates in a build step also saves shipping the Vue compiler, which the Vue docs put at 14 KB min+gzipped. The bigger wins are usually in your dependencies. Prefer ES module packages such as lodash-es over lodash, and check what a library costs before adding it. A modern bundler does most of this work for you; our Vite vs Webpack comparison covers why we default to Vite for Vue projects.

Render Content Pages on the Server

A client-rendered page shows nothing useful until its JavaScript loads and runs. For landing pages, blogs, docs and product pages, render the HTML on the server or at build time instead. Nuxt does this with SSR by default and lets you prerender or cache routes individually. Users see content sooner, and search engines get full HTML on the first request. If you're planning that move, our Nuxt development team does it for production apps.

Rendering and Update Optimizations

After the first load, speed comes down to how much work Vue does on each state change. Vue already skips most of it on its own. These patterns cover the rest.

Keep Props Stable

A child component updates when any of its props change. If every list item receives the same changing value, every item updates:

<ListItem v-for="item in list" :id="item.id" :active-id="activeId" />

Compute the comparison in the parent instead, so only the two items whose active value actually changes get updated:

<ListItem v-for="item in list" :id="item.id" :active="item.id === activeId" />

Skip Updates with v-once and v-memo

v-once renders an element or component once and treats it as static from then on. Use it for content that never changes after the first render.

v-memo skips an update unless the values in its array change. The Vue docs describe it as a micro-optimization that should rarely be needed, mostly for v-for lists longer than about 1,000 items:

<div v-for="item in list" :key="item.id" v-memo="[item.id === selectedId]">
  <p>{{ item.name }}: {{ item.id === selectedId ? 'selected' : '' }}</p>
</div>

Get the array wrong and the item stops updating when it should. If you reach for v-memo before profiling, you're guessing; add it only where a list is the measured bottleneck.

Choose Between v-if and v-show

v-if creates and destroys the element, so toggling it costs more. v-show always renders and toggles CSS, so the first render costs more. Use v-show for things users toggle often, such as tabs and dropdowns, and v-if for blocks that rarely appear.

Use Stable Keys in v-for

Give each list item a key that belongs to the data, such as a database id, not its array index. With index keys, sorting or removing an item makes Vue patch every row after it, and stateful children like inputs can end up attached to the wrong item.

Handling Large Data Sets

Tables with thousands of rows, live feeds, big API responses. This is where Vue apps most often slow down, and the fix is always some version of rendering or tracking less.

Virtualize Long Lists

No framework renders 10,000 DOM rows quickly. List virtualization renders only the rows near the viewport and swaps them as the user scrolls, so the number of DOM nodes stays small however long the list gets. The Vue docs point to community libraries such as vue-virtual-scroller; TanStack Virtual also ships a Vue adapter.

Use shallowRef for Large Immutable Data

Vue's reactivity is deep by default, so every nested property you read goes through a proxy that tracks dependencies. According to the Vue docs, this overhead becomes noticeable when a single render reads 100,000+ properties. For large data you replace rather than edit, such as an API response, use shallowRef. Only .value is reactive, so update it by assigning a new array or object:

import { shallowRef } from 'vue'

const rows = shallowRef([])

async function load() {
  rows.value = await fetchRows() // replacing the array triggers updates
}

Avoid Deep Watchers and Expensive Computed Properties

A deep watcher walks every nested property of its source each time it runs, which gets expensive on large objects. Watch the specific property you need, or limit depth with a number (deep: 1, available since Vue 3.5). Keep computed properties cheap and return primitive values where you can: since Vue 3.4, a computed only triggers its dependents when its value actually changes.

Vue Performance and Core Web Vitals

Core Web Vitals are how Google measures user experience. Good scores are an LCP within 2.5 seconds, an INP of 200 milliseconds or less, and a CLS of 0.1 or less, measured at the 75th percentile of page loads. INP replaced First Input Delay as a Core Web Vital in March 2024.

MetricWhat hurts it in Vue appsFixes from this guide
LCP (loading)Large bundles, client-only rendering of content pagesLazy routes, tree-shaking, SSR or prerendering with Nuxt
INP (responsiveness)Long tasks after clicks and typingStable props, v-memo, virtual lists, shallowRef, no deep watchers
CLS (visual stability)Async components and images that load without reserved spaceFixed dimensions and placeholders for lazy content

Carousels and image galleries are frequent CLS offenders. Our guide on how to fix CLS walks through them.

Vue Vapor Mode and the Future of Vue Performance

Vapor Mode is a new compilation mode in Vue 3.6 that turns components into direct DOM updates, without the virtual DOM. It's opt-in per component with <script setup vapor>, and the Vue team calls it feature-complete in the 3.6 release candidate. In the Chrome 154 run of js-framework-benchmark, the Vapor build created 1,000 rows in 21.8 ms against 24.5 ms for Vue 3.5. It doesn't support v-memo or the Options API. Vue 3.6 isn't stable yet, so read our Vue Vapor Mode migration guide before adopting it.

Conclusion

Most slow Vue apps get fast again with a handful of changes: measure, split the bundle, render content on the server, keep props stable and stop rendering or tracking data nobody sees. Start with whatever your measurements show users feel most. If you need help finding it, our frontend performance optimization team audits Vue and Nuxt apps and fixes what slows them down.

FAQs

How do you optimize the performance of a Vue.js application?

Measure first with Vue DevTools, the browser Performance panel and Lighthouse, then fix the biggest cost. For slow loading, lazy load routes, trim the bundle and render content pages on the server. For slow updates, keep props stable, virtualize long lists and use shallowRef for large data.

Is Vue faster than React?

In most benchmarks, yes, though not everywhere. In the js-framework-benchmark run for Chrome 154, Vue 3.5 created 10,000 rows in 259 ms against 401 ms for React 19.2 and shipped less than half the compressed code, while React was slightly quicker at creating 1,000 rows. In real apps, architecture matters more; see our Vue vs React comparison.

What slows down a Vue app?

Usually not Vue itself. The common causes are large JavaScript bundles, long lists rendered in full, deep reactivity on big data sets, deep watchers and props that change on every parent update. Unoptimized images and client-only rendering of content pages hurt load time too.

Does Vapor Mode make Vue faster?

Yes, for the components that use it. Vapor Mode compiles components to direct DOM updates without the virtual DOM, and in the Chrome 154 benchmark run the Vapor build of Vue 3.6 beat Vue 3.5 on every speed test we compared, used less memory and shipped a smaller bundle. Vue 3.6 is still a release candidate.

When does a Vue app need a performance audit?

When Core Web Vitals fail in field data, when users report lag that your team can't reproduce, or before a big release or migration. A Vue.js code audit finds the bottlenecks and ranks fixes by impact, so the team works on what users actually feel.

Vue.js is our passion. Open Source is our culture 😍