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

  1. Vue DevTools — component render times (Performance tab)
  2. Browser Performance panel — long tasks, layout thrashing
  3. Network — bundle size, API waterfall
  4. 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

typescript
{
  path: "/reports",
  component: () => import("../views/ReportsView.vue"),
}

Split vendor libraries:

typescript
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):

typescript
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:

typescript
import { markRaw } from "vue";
const editor = markRaw(createEditorInstance());

v-memo (Vue 3.2+)

Skip re-render when deps unchanged:

vue
<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

ProblemDirection
10,000 DOM rowsVirtual scroll (vue-virtual-scroller, @tanstack/vue-virtual)
Unstable :keyStable ids, not index
Filter in templatecomputed filtered list

Do not render 10k <li> elements—virtualize.

computed vs Methods in Templates

vue
<!-- 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

vue
<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

SmellFix
500-line SFCSplit UI + composable
God componentChild components + Pinia feature module
Duplicate fetch logicusePosts() composable

Feature folder layout:

text
src/features/posts/
├── components/
├── composables/
├── stores/
├── views/
└── types.ts

Versus 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: none without replacement
  • aria-live for dynamic error messages

Link: Accessible CSS, HTML accessible forms.

Security Habits

  • No v-html on user content
  • Sanitize or use text interpolation
  • CSP headers in production—Nginx

Production Build Settings

vite.config.ts:

typescript
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-patternWhy
watch everythingPrefer computed
Global event busUse Pinia or provide/inject
Mutate propsOne-way data flow breaks
Huge reactive treesshallowRef / split stores
No error boundaries for asyncHandle 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.