Vue Performance
Keep Vue applications responsive and production bundles efficient.
Overview
Performance describes how quickly an application loads, responds, and updates while using resources efficiently. Vue provides an optimized reactive renderer, but component design, bundle size, network activity, assets, and unnecessary reactive work still affect the user experience.
Key concepts
- Measure before optimizing
- Use stable keys and cached derivations
- Lazy-load routes and expensive features
- Reduce unnecessary reactivity and network work
- Test production builds on realistic devices
What is performance?
A high-performing Vue application loads useful content quickly, responds promptly to interactions, scrolls and animates smoothly, and avoids wasting memory, CPU, and network bandwidth.
Performance matters because delays interrupt users. Slow product images, unresponsive controls, or a long initial download can make an otherwise correct application frustrating to use.
Start by measuring
Do not optimize based only on guesses. Test a production build with browser performance and network tools, Vue Devtools, and realistic data. Identify whether the actual bottleneck is loading, rendering, JavaScript work, memory, an API, or an asset.
- Measure initial loading and bundle size
- Record slow interactions
- Inspect unnecessary component updates
- Throttle the network and CPU
- Test on representative mobile hardware
- Compare measurements after each change
How Vue updates efficiently
Vue tracks reactive dependencies used during rendering. When state changes, it updates the affected component output and patches only the necessary DOM instead of rebuilding the entire page.
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<button @click="count++">Count: {{ count }}</button>
<footer>This unrelated content remains unchanged.</footer>
</template>Use computed values for derived state
A computed value is cached until one of its reactive dependencies changes. It avoids duplicating derived state and prevents repeated calculations during renders when the source values are unchanged.
import { computed, ref } from "vue";
const price = ref(25);
const quantity = ref(2);
const totalPrice = computed(() => price.value * quantity.value);Use a method when the result should run on every call or when the operation performs an action. Use computed for a value derived from reactive state.
Render lists with stable keys
A stable key tells Vue which record each DOM element represents. Use a unique item ID rather than the array index when items can be inserted, removed, filtered, or reordered.
<ul>
<li v-for="product in products" :key="product.id">
{{ product.name }}
</li>
</ul>Handle very large lists
Even efficient rendering becomes expensive when thousands of DOM nodes are visible. Pagination or list virtualization renders only the records the user currently needs, reducing DOM work and memory usage.
Keep component boundaries meaningful
Focused components improve organization and can isolate updates, but splitting every small fragment into a component also adds overhead. Create boundaries around reusable interface pieces and features rather than assuming more components are always faster.
Lazy-load routes
Dynamic imports let the bundler create separate chunks. Vue Router can download a page component only when a user visits that route instead of placing every page in the initial bundle.
const routes = [
{
path: "/courses",
component: () => import("@/views/CoursesView.vue")
},
{
path: "/reports",
component: () => import("@/views/ReportsView.vue")
}
];Lazy-load large components
Features such as editors, charts, or large dialogs can load only when needed with defineAsyncComponent.
import { defineAsyncComponent } from "vue";
const AnalyticsChart = defineAsyncComponent(() =>
import("./AnalyticsChart.vue")
);Control reactive overhead
Keep only values that affect the interface or reactive logic in reactive state. Avoid broad deep watchers over large object graphs. For large external objects or datasets that do not need deep tracking, shallowRef can reduce overhead.
import { shallowRef } from "vue";
const largeDataset = shallowRef([]);
// Replacing the root triggers an update.
largeDataset.value = nextDataset;Avoid unnecessary child updates
Keep props stable when possible. If a parent derives a new value for every child on every update, more children may need to render. Calculate stable booleans or display values before passing them to repeated child components.
Use v-once and v-memo carefully
v-once renders content once and skips future updates, making it suitable only for content that is truly permanent. v-memo can skip updates for a subtree when listed dependencies are unchanged, but it should be introduced only after profiling confirms a rendering bottleneck.
<h1 v-once>{{ permanentTitle }}</h1>
<article v-memo="[product.id, product.price]">
{{ product.name }} — {{ product.price }}
</article>Load API data efficiently
- Request data when it is needed rather than on every render
- Reuse valid results instead of immediately refetching
- Debounce rapidly changing search input
- Cancel or ignore obsolete requests
- Paginate large responses
- Display loading and error feedback
- Define when cached data becomes stale
Optimize images and assets
Images are often larger than the application JavaScript. Use appropriate dimensions and modern formats, provide responsive image sources, reserve image space to prevent layout shifts, and lazy-load images that begin outside the viewport.
<img
src="/images/course-card.webp"
alt="Vue course"
width="640"
height="360"
loading="lazy"
/>Clean up external resources
Timers, subscriptions, observers, and window event listeners can continue consuming resources after a component is gone. Remove them with onUnmounted or through the cleanup mechanism of the API that created them.
import { onMounted, onUnmounted } from "vue";
function handleScroll() {
// Update only what the interface needs.
}
onMounted(() => window.addEventListener("scroll", handleScroll));
onUnmounted(() => window.removeEventListener("scroll", handleScroll));Build and test for production
Development mode includes warnings and debugging support, so its timing and bundle characteristics differ from production. Evaluate the optimized production build before making final performance decisions.
Best practices
- Measure real bottlenecks before changing code
- Use computed for cached derived values
- Provide stable list keys
- Lazy-load routes and expensive optional features
- Paginate or virtualize very large lists
- Avoid unnecessary deep reactivity and watchers
- Optimize images and fonts
- Prevent duplicate API requests
- Clean up external resources
- Confirm improvements in a production build
Common beginner mistakes
- Optimizing without measuring
- Using array indexes as keys for changing lists
- Calling expensive methods repeatedly from templates
- Fetching the same data unnecessarily
- Loading every page and feature at startup
- Rendering thousands of rows at once
- Making very large datasets deeply reactive without need
- Leaving timers and listeners active
- Testing only on a fast development computer
- Sacrificing clarity for tiny unproven optimizations
Key takeaways
- Performance includes loading, responsiveness, smoothness, and resource efficiency
- Vue already updates the DOM selectively
- Measurement reveals where optimization is valuable
- Computed values cache reactive derivations
- Stable keys make list updates predictable
- Code splitting reduces the initial download
- Virtualization controls the cost of huge lists
- Network requests and media often matter as much as rendering
- Production testing confirms whether an optimization helps