Performance and Best Practices
Introduction
Vue 3 is fast by default, but large lists, unnecessary reactivity, and bundle bloat still slow real apps. This chapter covers practical habits—lazy routes, v-memo, shallow refs, project structure, and accessibility basics—without premature optimization.
Prerequisites
Measure Before Optimizing
- Vue DevTools — component render times (Performance tab)
- Browser Performance panel — long tasks, layout thrashing
- Network — bundle size, API waterfall
- Lighthouse — Core Web Vitals (LCP, INP, CLS)
Fix the biggest bottleneck first—often network or one heavy component, not micro-tuning.
Route and Component Lazy Loading
{
path: "/reports",
component: () => import("../views/ReportsView.vue"),
}Split vendor libraries:
const ChartPanel = defineAsyncComponent(() => import("./ChartPanel.vue"));Code explanation:
- Users download code for routes they visit
- Pair with loading UI or
<Suspense>
Avoid Deep Reactive Overhead
Large immutable data (chart series, third-party instances):
import { shallowRef } from "vue";
const chartData = shallowRef(largeArray);
// Replace whole .value when data changes—not deep mutations
chartData.value = newData;markRaw for objects that should never be reactive:
import { markRaw } from "vue";
const editor = markRaw(createEditorInstance());v-memo (Vue 3.2+)
Skip re-render when deps unchanged:
<template>
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.done]">
<HeavyRow :item="item" />
</div>
</template>Use when profiling shows expensive list item re-renders.
List Performance
| Problem | Direction |
|---|---|
| 10,000 DOM rows | Virtual scroll (vue-virtual-scroller, @tanstack/vue-virtual) |
Unstable :key | Stable ids, not index |
| Filter in template | computed filtered list |
Do not render 10k <li> elements—virtualize.
computed vs Methods in Templates
<!-- Prefer -->
<li v-for="todo in filteredTodos" :key="todo.id">
<!-- Avoid re-filtering every render -->
<li v-for="todo in todos.filter(t => !t.done)" :key="todo.id">Methods called in template run every render unless memoized with computed.
Image and Asset Loading
<img src="/hero.webp" alt="Hero" loading="lazy" width="800" height="400" />- Modern formats (WebP, AVIF)
- Explicit width/height reduces layout shift (CLS)
loading="lazy"below the fold
Keep Components Focused
| Smell | Fix |
|---|---|
| 500-line SFC | Split UI + composable |
| God component | Child components + Pinia feature module |
| Duplicate fetch logic | usePosts() composable |
Feature folder layout:
src/features/posts/
├── components/
├── composables/
├── stores/
├── views/
└── types.tsVersus type-only layout (components/, views/)—pick one per team and stay consistent.
Pinia and State Shape
- Store only shared state—not every local ref
- Normalize lists by id for O(1) lookup when lists grow
- Avoid storing derived data—use getters
Accessibility Basics
- Use semantic HTML:
<button>not<div @click> <label for="email">linked to inputs- Focus visible styles—do not
outline: nonewithout replacement aria-livefor dynamic error messages
Link: Accessible CSS, HTML accessible forms.
Security Habits
- No
v-htmlon user content - Sanitize or use text interpolation
- CSP headers in production—Nginx
Production Build Settings
vite.config.ts:
build: {
target: "es2020",
cssCodeSplit: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ["vue", "vue-router", "pinia"],
},
},
},
},Run npm run build and inspect dist/assets/ sizes.
Anti-Patterns
| Anti-pattern | Why |
|---|---|
watch everything | Prefer computed |
| Global event bus | Use Pinia or provide/inject |
| Mutate props | One-way data flow breaks |
| Huge reactive trees | shallowRef / split stores |
| No error boundaries for async | Handle loading/error in UI |
FAQ
Is Vue slower than React?
Both are fast; architecture and bundle size matter more than framework choice.
v-once?
Renders once, never updates—rare; v-memo more flexible.
Devtools "Component rendered X times"?
Find unnecessary parent state changes; split components or memo.
SSR for performance?
SSR helps first paint and SEO—not always faster TTI; measure.
Tree-shaking Element Plus?
Import components on demand—full library import bloats bundle.
Profile script setup cost?
Setup runs once per instance—optimize render function and deps, not setup itself.