Vue Performance & Best Practices
Optimize your Vue applications for maximum performance and follow best practices for maintainable, scalable code:
Component Performance Optimization
v-once & v-memo
Use v-once to render elements only once and v-memo to memoize template sub-trees.
<template>
<!-- v-once: renders only once, never updates -->
<div v-once>
<h1>{{ staticTitle }}</h1>
<p>{{ staticDescription }}</p>
</div>
<!-- v-memo: memoizes based on dependency array -->
<div v-memo="[count]">
<!-- Only re-renders when count changes -->
<p>Count: {{ count }}</p>
<p>Other data: {{ otherData }}</p>
</div>
<!-- Memo for list items -->
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
<!-- Only re-renders when id or selected changes -->
{{ item.name }}
</div>
</template>Computed vs Methods
<script setup>
import { ref, computed } from 'vue'
const items = ref([/* lots of items */])
// ✅ Good: Computed - cached, only recalculates when items change
const expensiveComputation = computed(() => {
console.log('Computing...')
return items.value
.filter(item => item.active)
.map(item => item.price)
.reduce((sum, price) => sum + price, 0)
})
// ❌ Bad: Method - recalculates on every render
const expensiveMethod = () => {
console.log('Computing...')
return items.value
.filter(item => item.active)
.map(item => item.price)
.reduce((sum, price) => sum + price, 0)
}
</script>
<template>
<div>
<!-- Computed: Cached, efficient -->
<p>Total: {{ expensiveComputation }}</p>
<!-- Method: Recalculates every time -->
<p>Total: {{ expensiveMethod() }}</p>
</div>
</template>Virtual Scrolling
For rendering large lists, use virtual scrolling to only render visible items.
// Using vue-virtual-scroller
<script setup>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
const items = ref(
Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
description: `Description for item ${i}`,
}))
)
</script>
<template>
<RecycleScroller
:items="items"
:item-size="80"
key-field="id"
class="scroller"
style="height: 600px"
>
<template #default="{ item }">
<div class="item">
<h3>{{ item.name }}</h3>
<p>{{ item.description }}</p>
</div>
</template>
</RecycleScroller>
</template>Lazy Loading Components
<script setup>
import { defineAsyncComponent } from 'vue'
// Lazy load heavy components
const HeavyChart = defineAsyncComponent(() =>
import('./components/HeavyChart.vue')
)
// With loading and error states
const AsyncComponent = defineAsyncComponent({
loader: () => import('./components/HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200, // Delay before showing loading component
timeout: 3000, // Timeout for loading
})
const showChart = ref(false)
</script>
<template>
<div>
<button @click="showChart = true">Show Chart</button>
<Suspense v-if="showChart">
<template #default>
<HeavyChart :data="chartData" />
</template>
<template #fallback>
<div>Loading chart...</div>
</template>
</Suspense>
</div>
</template>Avoid Unnecessary Reactivity
<script setup>
import { ref, shallowRef, markRaw } from 'vue'
// For large, immutable data, use shallowRef
const hugeDataset = shallowRef({
/* thousands of items */
})
// For non-reactive objects (like third-party class instances)
const chartInstance = shallowRef(null)
onMounted(() => {
// markRaw prevents Vue from making it reactive
chartInstance.value = markRaw(new Chart())
})
// Constants don't need to be reactive
const API_URL = 'https://api.example.com' // ✅ Not reactive
const config = { timeout: 5000 } // ✅ Not reactive
// Only make data reactive if it needs to trigger updates
const userSettings = ref({ theme: 'dark' }) // ✅ Needs reactivity
</script>Production Build Optimization
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
// Enable tree-shaking
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // Remove console.log in production
drop_debugger: true,
},
},
// Code splitting
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-components': ['./src/components/ui'],
},
},
},
// Chunk size warnings
chunkSizeWarningLimit: 1000,
},
optimizeDeps: {
include: ['vue', 'vue-router', 'pinia'],
},
})Best Practices
Use <script setup> for cleaner, more performant code
Prefer computed() over methods for derived data
Use shallowRef/shallowReactive for large data structures
Lazy load routes and heavy components
Use v-show for frequently toggled elements, v-if for rare toggles
Extract reusable logic into composables
Keep components focused and single-responsibility
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free