Skip to content
← All questions
🔥 Advanced

How do props stability optimizations work?

⚡ Performance🧩 Components

Vue skips re-rendering a child component when none of its props changed. Props stability means structuring your props so that only the components that truly need to update receive changed values. The biggest win is in lists: passing a shared value like activeId to every item forces all items to re-render, even though only two items actually changed state.

The problem

vue
<!-- Parent -->
<script setup>
const items = ref([
  /* 100 items */
])
const activeId = (ref < number) | (null > null)
</script>

<template>
  <ListItem
    v-for="item in items"
    :key="item.id"
    :id="item.id"
    :active-id="activeId"
  />
</template>
Open in Vue Playground
vue
<!-- ListItem.vue -->
<script setup>
const props = defineProps<{ id: number; activeId: number | null }>()
</script>

<template>
  <div :class="{ active: id === activeId }">{{ id }}</div>
</template>
Open in Vue Playground

When activeId changes from 1 to 2, the activeId prop changes for ALL 100 items. Vue re-renders every single ListItem, even though only two items actually need a visual update (the previously active one and the newly active one).

The fix: pre-compute in the parent

vue
<!-- Parent -->
<template>
  <ListItem
    v-for="item in items"
    :key="item.id"
    :id="item.id"
    :active="item.id === activeId"
  />
</template>
Open in Vue Playground
vue
<!-- ListItem.vue -->
<script setup>
defineProps<{ id: number; active: boolean }>()
</script>

<template>
  <div :class="{ active }">{{ id }}</div>
</template>
Open in Vue Playground

Now when activeId changes from 1 to 2:

  • Item 1: :active goes from true to false (re-renders)
  • Item 2: :active goes from false to true (re-renders)
  • Items 3-100: :active stays false (skipped)

2 re-renders instead of 100.

Common unstable prop patterns

Passing the whole selection set:

vue
<!-- BAD: all items re-render when any selection changes -->
<Item v-for="item in items" :key="item.id" :selected-ids="selectedIds" />

<!-- GOOD: only affected items re-render -->
<Item
  v-for="item in items"
  :key="item.id"
  :selected="selectedIds.has(item.id)"
/>
Open in Vue Playground

Passing the list length or index:

vue
<!-- BAD: total changes whenever the list changes -->
<Item
  v-for="(item, index) in items"
  :key="item.id"
  :index="index"
  :total="items.length"
/>

<!-- GOOD: pass only what the child actually needs -->
<Item
  v-for="item in items"
  :key="item.id"
  :is-last="item === items[items.length - 1]"
/>
Open in Vue Playground

Passing an inline object or function:

vue
<!-- BAD: new object on every render → always different reference -->
<Item
  v-for="item in items"
  :key="item.id"
  :style="{ color: item.color }"
  :on-click="() => select(item.id)"
/>
Open in Vue Playground

Inline objects and arrow functions create a new reference every render. Vue sees a "new" prop and re-renders the child. Move them to computed or methods if performance matters.

Impact at scale

List sizeUnstable prop (activeId)Stable prop (:active boolean)
100 items100 re-renders2 re-renders
1,000 items1,000 re-renders2 re-renders
10,000 items10,000 re-renders2 re-renders

The optimization has a constant cost (always 2) regardless of list size. The naive approach has linear cost.

The rule

If a prop value changes for ALL children but only SOME children need to react, pre-compute the derived value in the parent and pass the result. The child should receive only values that are specific to its own state.

See also: How does reducing component abstraction improve list performance? · What is the Virtual DOM and how does Vue use it? · How do you declare props with TypeScript?

References

Released under the MIT License.