Performance optimisation

Vue · Advanced · 14 min

Vue is fast by default, but large lists, heavy components and careless props can still make apps sluggish.

Measure first: the Vue devtools performance timeline and the browser profiler show which components render often or slowly. Then apply the matching fix.

Load less: lazy-load routes and heavy components with defineAsyncComponent(() => import('./Chart.vue')); check bundle sizes; import only what you use from big libraries.

Update less:

  • Props stability: a child re-renders when any prop it receives changes. Passing :active-id="activeId" to every row re-renders all rows on each change; passing :active="item.id === activeId" re-renders only the two affected rows.
  • `v-once` renders a subtree once and skips it forever after.
  • `v-memo="[item.selected]"` skips updating a subtree (often a v-for row) unless the listed values change.
  • Computed stability: since 3.4 computeds trigger only when their value changes, but returning a new object each time defeats that; compare manually if needed.

Track less: deep reactivity over huge arrays costs memory and time. Use shallowRef for large data you replace wholesale and markRaw for non-UI objects.

Render less: lists with thousands of rows should be virtualised (render only visible rows) with a library such as vue-virtual-scroller or TanStack Virtual. Avoid needless component abstraction in hot loops; each component instance has overhead.

Production builds strip dev warnings and are much faster than dev mode; always profile production.

Example

<script setup>
import { ref, shallowRef, defineAsyncComponent } from 'vue'

const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'))
const rows = shallowRef([])                 // large list, replaced as a whole
const selectedId = ref(null)
const showChart = ref(false)

async function load() {
  rows.value = await (await fetch('/api/rows')).json()
}
load()
</script>

<template>
  <footer v-once>Build {{ new Date().getFullYear() }}</footer>
  <div v-for="row in rows" :key="row.id"
       v-memo="[row.id === selectedId]"
       :class="{ selected: row.id === selectedId }"
       @click="selectedId = row.id">
    {{ row.label }}
  </div>
  <button @click="showChart = true">Show chart</button>
  <HeavyChart v-if="showChart" :rows="rows" />   <!-- chunk loads on demand -->
</template>

Remember

  • Profile in production mode before optimising
  • Keep props stable: pass derived booleans, not shared ids, to list rows
  • v-once, v-memo, shallowRef, async components and virtualisation cover most hot spots

Watch out for

  • Using v-memo with an incomplete dependency array, so rows don't update when other data changes.

Check yourself

  1. Every row receives :selected-id="selectedId" and compares it internally. Selecting a new row re-renders...

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: vuejs.org/guide/best-practices/performance.html

Builds on

Unlocks

Nothing builds on this yet.