skip to content

Update Optimizations

Keeping child components from re-rendering: stable primitive props, v-once, v-memo with its dependency array, and stable computed results. Interviewers probe what lets Vue skip an update.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Vue 3, when a parent component re-renders, what decides whether each child component re-renders as well?

level: middleimportance: must knowfreq 55%

answer

  1. not parent-driven by default
  2. props compared per key
  3. primitives stable, literals not
  4. declared emits are exempt

basics

~20 s

A Vue child re-renders only if its own tracked state changed or the parent hands it changed props or dynamic slots. Props are compared per key by identity, so primitives stay stable while fresh objects, arrays or functions count as changed.

solid answer

~50 s

When a parent re-renders, Vue checks every child component vnode before updating it. The child updates if a prop value differs by identity (`!==`, with `style` objects compared by value), if the set of props changed, if it receives dynamic slot content (slots under `v-if` or `v-for`), or if a runtime directive or transition sits on the component. Otherwise Vue keeps the child's existing render. So props stability is the lever: pass primitives or references that survive re-renders. An inline `:options="{ dense: compact }"` or `:rows="rows.filter(...)"` builds a new object on every parent render and forces the child to update; a `computed` keeps the same reference until its inputs change. Listeners for events the child declares in `emits` are excluded from the check, and a child still re-renders on its own when state it read changes.

code

vue · 27 lines
vue
<script setup lang="ts">
import { computed, onUnmounted, ref } from 'vue'
import ReportTable from './ReportTable.vue'

interface Row { id: number; visible: boolean; name: string }

const rows = ref<Row[]>([])
const compact = ref(false)
const clock = ref(Date.now())
const timer = setInterval(() => (clock.value = Date.now()), 1000)
onUnmounted(() => clearInterval(timer))

// Stable: same array until rows change.
const visibleRows = computed(() => rows.value.filter(r => r.visible))
// Stable: same object until compact changes.
const tableOptions = computed(() => ({ dense: compact.value }))
</script>

<template>
  <p>{{ new Date(clock).toLocaleTimeString() }}</p>

  <!-- Unstable: a new array and a new object on every clock tick. -->
  <ReportTable :rows="rows.filter(r => r.visible)" :options="{ dense: compact }" />

  <!-- Stable: the same references across ticks, so this table is skipped. -->
  <ReportTable :rows="visibleRows" :options="tableOptions" />
</template>

go deeper

for a junior

Recall that a Vue child updates when its props or its own state change, not simply because its parent re-rendered.

for a middle

Explain the check: per-key identity comparison, dynamic slots, directives on the component, and the declared-emits exemption.

for a senior

Spot unstable props in review - inline literals, filter or map calls, undeclared events - and fix them with computed values and primitive props.

for a principal

Decide where stable props are worth enforcing: lists and heavy children, not every component, and make it a review norm there.

## Two separate reasons a child re-renders In Vue 3 every component has its own **render effect**. It re-runs for exactly two reasons: 1. **Its own dependencies changed.** Reactive state the child read during its last render - its own refs, a store, a nested field of an object prop - was mutated. 2. **The parent's update decided it must.** When the parent re-renders, it produces a new vnode for the child and the renderer asks whether that child needs updating. The second path is where props stability matters. A parent re-rendering does **not** automatically re-render its children; the renderer checks first. ## What the check looks at The renderer's internal check (`shouldUpdateComponent` in the runtime source) forces an update when: - a **prop value changed**: compared per key with `!==`, except `style`, whose objects are compared by value; - the **number of props** changed, for components bound with a spread such as `v-bind="obj"`; - the component receives **dynamic slots**, meaning slot content under `v-if` or `v-for` that may differ between renders; - a **runtime directive** or a **transition** is attached to the component vnode. One exemption matters in practice: a listener for an event the child **declares in `emits`** is skipped, so a fresh inline handler for a declared event does not count as a changed prop. An undeclared listener falls through as an ordinary attribute and does count. ## Stable and unstable props | Passed as | Same value next render? | Effect on the child | |---|---|---| | `:count="n"` (primitive) | yes, if `n` is unchanged | skipped | | `:user="user"` (same reactive object) | yes, same proxy | skipped by the parent's check | | `:options="{ dense: compact }"` | no, a new object | updated every parent render | | `:rows="rows.filter(r => r.visible)"` | no, a new array | updated every parent render | | `:rows="visibleRows"` (a `computed`) | yes, until its inputs change | skipped | | `@select="pick(item.id)"`, `select` declared in `emits` | new function, but exempt | skipped | ## Why this is not the 'parent renders, children render' model In Vue, a parent that re-renders because a clock ticked does not re-render a child whose props are the same primitives and references as before. Conversely, a child can re-render **without** its parent: if it read `user.name` through an object prop and that field is mutated, the child's own render effect tracked it and re-runs, while the parent - which only passed the reference - does nothing. ## Slots and the check Slots add one more case. Content passed to a child through a slot is compiled into a function that the **child** calls during its own render. Two consequences follow: - For **stable slots** - plain slot content with no `v-if` or `v-for` around the slot itself - the parent's check does not force the child to update. If the slot content reads parent state that changes, that read was tracked by the child's render effect, so the child updates for that reason alone. - For **dynamic slots** - a slot rendered conditionally or in a loop - the compiler marks the child vnode, and the renderer forces the child to update on every parent re-render, because the set of slots may have changed. So a child that receives a conditionally rendered slot cannot be skipped by props stability alone; if that child is expensive, restructure so the condition lives inside the slot content rather than around it. ## Practical rules - **Derive values in `computed`**, not inline in the template, when they are passed as object or array props. - **Pass the smallest primitive the child needs** - a boolean instead of an id to compare against - so most children see an unchanged value. - **Declare emitted events** with `defineEmits`; besides documenting the component, it keeps inline listeners out of the update check. - **Do not over-apply it.** For a handful of cheap children, an extra update costs microseconds; the lever pays off in lists and heavy children.

  • You pass the same reactive object as a prop and mutate one of its nested fields. Does the child update?
    The parent's check sees the same reference and does not force an update. But if the child's render read that field through the prop, it tracked it, so the child's own render effect re-runs. The update comes from the child's dependency, not from the parent.
  • Why doesn't a fresh inline @select handler inside a v-for force every row to update?
    Vue's child-update check skips listener props for events the child declares in `emits`, so a new function for a declared event is not treated as a changed prop. If `select` were undeclared, the listener would be an ordinary fallthrough attribute and a new function on each render would force the update.

saying these in an interview costs you the question

  • A Vue child component re-renders whenever its parent re-renders.
  • Vue deep-compares every object prop before deciding to update a child.
  • An inline object literal prop is free because its contents are the same.
  • A computed passed as a prop creates a new array on every parent render.
  • A child can never update from a nested change to an object prop it received.
open as a page

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

level: juniorimportance: should knowfreq 45%

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.

open as a page

In Vue 3, how does v-memo let a large v-for list skip update work, and what breaks when its dependency array is incomplete?

level: middleimportance: should knowfreq 35%

basics

~10 s

v-memo takes a fixed-length array; if every value equals last render's, Vue reuses the cached subtree, skipping vnode creation and diffing. Leave out a value the subtree displays and that part silently stops updating.

open as a page

In a Vue 3 list of 1,000 row components, changing the selected id makes every row update; how do you make only the affected rows re-render?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Stop passing selectedId to every row; pass a per-row boolean such as :selected="row.id === selectedId", keep the other props stable and declare the row's events, so only the two rows whose boolean flipped fail Vue's props check.

open as a page

In Vue 3.4+, why does a computed that returns a new object each run still trigger its dependents, and how do you stabilize it?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

Since Vue 3.4 a computed notifies dependents only when its result changed by identity. A freshly built object is always different, so return the previous value, passed as the getter's first argument, when the relevant fields are equal.

open as a page