v-once renders an element once and skips all future updates. v-memo conditionally skips re-renders based on a dependency array. Both reduce render work by telling Vue that certain parts of the template don't need to be re-evaluated.
v-once
Marks content as static after the first render. Vue creates the vnode once and reuses it on every subsequent update.
<template>
<!-- Rendered once, never re-evaluated -->
<footer v-once>
<p>Copyright {{ year }} {{ company }}</p>
</footer>
</template>
<script setup>
const year = 2024
const company = 'Acme Corp'
</script>Even though year and company are interpolated at runtime, v-once tells Vue their values will never change, so the subtree is frozen after the first render.
v-memo
Memoizes a subtree based on a dependency array. Vue skips re-rendering when all values in the array are the same as the previous render. This is most useful inside v-for loops.
<template>
<div
v-for="item in items"
:key="item.id"
v-memo="[item.id === selectedId]"
>
<div :class="{ selected: item.id === selectedId }">
<ExpensiveComponent :data="item" />
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([/* 1,000 items */])
const selectedId = ref<number | null>(null)
</script>When selectedId changes, only two items re-render: the previously selected one (true to false) and the newly selected one (false to true). The other 998 items are skipped entirely.
v-memo with multiple dependencies
<template>
<div
v-for="item in items"
:key="item.id"
v-memo="[item.id === selectedId, item.id === editingId]"
>
<ItemCard
:item="item"
:selected="item.id === selectedId"
:editing="item.id === editingId"
/>
</div>
</template>The item re-renders only when its selection or editing state changes.
v-memo with empty array
v-memo="[]" is equivalent to v-once: the dependency array never changes, so the content is never re-rendered.
<div v-for="item in staticList" :key="item.id" v-memo="[]">
{{ item.name }}
</div>When NOT to use them
<template>
<!-- Wrong: count will never update in the UI -->
<div v-once>
<span>Count: {{ count }}</span>
</div>
<!-- Wrong: v-model inside memoized subtree won't work properly -->
<div v-memo="[selected]">
<input v-model="item.name" />
</div>
<!-- Pointless: overhead of memoization exceeds the cost of re-rendering a <span> -->
<span v-once>{{ label }}</span>
</template>When to use which
| Scenario | Use |
|---|---|
| Content that uses runtime data but never changes after mount | v-once |
| Large list where only a few items change at a time | v-memo with the changing condition |
| Completely static markup (no interpolation) | Neither, the compiler already hoists it |
| Content with interactive children (inputs, v-model) | Neither, they need to re-render |
| Small, simple elements | Neither, the optimization isn't worth it |
Profile with Vue DevTools before adding these directives. They're a targeted optimization for measured bottlenecks, not something to sprinkle everywhere.