skip to content

In a Vue 3 template, what does the v-once directive do, and when is it the right tool?

level: juniorimportance: should knowfreq 45%

answer

  1. render once, then static
  2. no expression needed
  3. later re-renders skip it
  4. stale if data changes

basics

~20 s

v-once renders an element, component or subtree once with the current data, then treats it as static content that every later re-render skips. Use it for content that reads data but never needs to update.

solid answer

~50 s

`v-once` takes no expression. On the first render the subtree is created normally, reading whatever data it needs; the result is cached, and on every later re-render of the component Vue reuses the cached vnodes and skips diffing them, so bindings inside never update even if their data changes. It works on elements, components and `v-for` lists, where it freezes the whole list, length included. It fits content computed from data that is fixed for the component's lifetime - a rendered terms text, a header built from an initial prop - inside a component that re-renders often for other reasons. It is the wrong tool when the data can change and the user should see it: the view silently goes stale. For 'skip unless these values change', `v-memo` is the conditional version, and `v-memo="[]"` behaves like `v-once`.

go deeper

for a junior

Remember that v-once renders once, takes no value, and never updates afterwards even if the data it read changes.

for a middle

Explain the render-cache mechanism: the cached vnode is reused and not diffed, and why static markup gains nothing from it.

for a senior

Judge when freezing is safe, and prefer splitting the fast-changing state into a child before reaching for v-once.

for a principal

Weigh the maintenance risk: silent staleness is a bug class, so reserve v-once for documented, provably fixed content.

## What v-once does `v-once` is a built-in Vue directive that **renders once and skips all future updates**. It takes no expression. The element, component or subtree it sits on is rendered on the first pass using the current data; after that, the Vue API reference says, the subtree and all its children are treated as **static content** and skipped on re-render. ```html <span v-once>This will never change: {{ msg }}</span> ``` If `msg` changes later, the span keeps showing the first value. ## How it works The template compiler rewrites a `v-once` subtree so that its vnode is created on the first render and stored in the component's **render cache**. Every later render returns that cached vnode instead of building a new one, and the renderer does not diff it. The saving is therefore twofold: - **No vnode creation** for the subtree on re-render. - **No patching** of the subtree's DOM. Because static markup without any bindings is already optimized by the compiler, `v-once` only adds value where the subtree **does** read data. ## Where it fits - **Data fixed for the component's lifetime**: a formatted legal text passed once, a title computed from an initial prop, an expensive-to-render help panel. - **A busy parent**: a component that re-renders often because of a clock, an input or a live counter, and that also contains a large block that does not depend on those changes. - **A `v-for` list whose items and length are fixed**, such as archived history loaded once. On the `v-for` element, `v-once` caches the whole list, so items appended later do not appear either. ## Where it goes wrong 1. **Stale UI.** Anything inside reads its data once. If that data changes, the user never sees it, and nothing warns you. 2. **Components under `v-once`** are never re-rendered by the parent, so new prop values are never passed down. Their own internal reactive state still drives their own render, which can make the stale parts hard to spot. 3. **Rarely re-rendering components gain nothing.** If the component almost never re-renders, there are no updates to skip. ## v-once compared with its neighbours | Tool | What it skips | Updates when | |---|---|---| | `v-once` | the subtree on every re-render after the first | never | | `v-memo="[a, b]"` | the subtree while `a` and `b` are unchanged | any listed value changes | | `v-memo="[]"` | same as `v-once` | never | | plain template | nothing | any tracked data it reads changes | ## A worked example Picture a support chat panel. The component shows a typing indicator and a live timer, so it re-renders several times a second. Above them sits the conversation history loaded when the panel opened - 300 archived messages that will never change - and below that, the messages arriving live: - Rendering the archived history as one `v-for` with `v-once` on the `v-for` element means the first render builds those 300 subtrees and every later re-render reuses them without building or diffing anything. - `v-once` on a `v-for` element caches the **whole list expression**, not each item separately, so the list's length is frozen too. Live messages must therefore go in a second `v-for` without `v-once`; appending them to the archived array would never show them. - An edit feature would break this design: once an archived message can change after it is shown, `v-once` becomes a stale-UI bug, and the right tool changes to `v-memo` keyed on the message's revision, or to a smaller child component that owns the fast-changing timer. The example shows the real question to ask before adding `v-once`: is the content fixed, or does it merely seem fixed today? ## Choosing it in practice - Reach for `v-once` only after seeing that re-rendering the block costs something measurable. - Prefer **moving the frequently changing state** into a smaller child component first; that stops the parent from re-rendering the big block at all, without freezing data. - Leave a short comment explaining why the block may never change, because the next developer who binds new data into it will otherwise lose an afternoon wondering why it does not update. `v-once` has existed since Vue 2 and behaves the same way in Vue 3; its conditional sibling `v-memo` was added in Vue 3.2.

  • What happens to a component rendered with v-once when the parent passes it a new prop value?
    Nothing reaches it. The parent reuses the cached vnode, so the child is not updated with the new prop and keeps showing the first value. The child's own internal reactive state still re-renders the child itself, which is why a v-once component can look half-updated.
  • When would you choose v-memo over v-once?
    When the block should update, but only when a few known values change. `v-memo="[a, b]"` skips re-rendering the subtree while `a` and `b` are the same as last render and updates it normally when either changes. `v-once` has no condition at all and never updates after the first render.

v-once is like a printed name badge: it shows whatever your name was when it was printed, and it does not change when you do.

saying these in an interview costs you the question

  • v-once makes an element update only once per tick.
  • v-once cannot contain interpolations or bindings.
  • v-once only works on plain elements, not components.
  • v-once speeds up a component's very first render.
  • Data inside v-once still updates when it changes.