Skip to content
← All questions
Advanced

How do props stability optimizations work?

PerformanceComponents

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>
vue
<!-- ListItem.vue -->
<script setup>
const props = defineProps<{ id: number; activeId: number | null }>()
</script>

<template>
  <div :class="{ active: id === activeId }">{{ id }}</div>
</template>

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>
vue
<!-- ListItem.vue -->
<script setup>
defineProps<{ id: number; active: boolean }>()
</script>

<template>
  <div :class="{ active }">{{ id }}</div>
</template>

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)"
/>

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]"
/>

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)"
/>

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.

Released under the MIT License.