Files
stack/packages/mosaic/framework/skills/vue-best-practices/reference/rendering-excessive-rerenders-watch-vs-computed.md
fargo 1a822493ba format: apply repo prettier (3.8.1) to the folded skills tree
963 markdown files reformatted with the repository's pinned prettier so
pnpm format:check covers the folded tree like every other repo file.

The formatter's embedded-language pass also normalized code fences
(TS semicolons, closed HTML tags in examples, lowercased CSS hex colors,
one renumbered list that skipped an index). Alphanumeric token deltas vs
the fold commit were audited file-by-file; all are formatter-equivalent
markup normalizations plus the four sanitized skills.
2026-08-19 14:37:17 -05:00

4.6 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Avoid Excessive Re-renders from Misused Watchers HIGH Using watch instead of computed, or deep watchers unnecessarily, triggers excessive component re-renders gotcha
vue3
rendering
performance
watch
computed
reactivity
re-renders

Avoid Excessive Re-renders from Misused Watchers

Impact: HIGH - Improper use of watchers is a common cause of performance issues. Deep watchers track every nested property change, and using watch when computed would suffice creates unnecessary reactive updates that trigger re-renders.

Task Checklist

  • Use computed for derived values, not watch + manual state updates
  • Avoid deep: true on large objects unless absolutely necessary
  • When deep watching is needed, watch specific nested paths instead
  • Never trigger state changes inside watch that cause the watch to re-fire

Incorrect:

<script setup>
import { ref, watch } from 'vue';

const user = ref({ name: 'John', settings: { theme: 'dark', notifications: true } });
const displayName = ref('');

// BAD: Using watch to compute a derived value
// This triggers an extra reactive update cycle
watch(
  () => user.value.name,
  (name) => {
    displayName.value = `User: ${name}`;
  },
  { immediate: true },
);

// BAD: Deep watcher on a large object
// Fires on ANY nested change, even unrelated ones
const items = ref([
  /* 1000 items with nested properties */
]);
watch(
  items,
  (newItems) => {
    console.log('Items changed'); // Fires on every tiny change
  },
  { deep: true },
);
</script>

Correct:

<script setup>
import { ref, computed, watch } from 'vue';

const user = ref({ name: 'John', settings: { theme: 'dark', notifications: true } });

// GOOD: Use computed for derived values
// No extra reactive updates, automatically cached
const displayName = computed(() => `User: ${user.value.name}`);

// GOOD: Watch specific paths, not the entire object
const items = ref([
  /* 1000 items */
]);
watch(
  () => items.value.length, // Only watch the length
  (newLength) => {
    console.log(`Items count: ${newLength}`);
  },
);

// GOOD: Watch specific nested property
watch(
  () => user.value.settings.theme,
  (newTheme) => {
    applyTheme(newTheme); // Side effect - appropriate for watch
  },
);
</script>

When to Use Watch vs Computed

Use Case Use This
Derive a value from state computed
Format/transform data for display computed
Perform side effects (API calls, DOM updates) watch
React to route changes watch
Sync with external systems watch

Infinite Loop from Watch

<script setup>
import { ref, watch } from 'vue';

const count = ref(0);

// DANGER: Infinite loop!
watch(count, (newVal) => {
  count.value = newVal + 1; // Modifies watched source -> triggers watch again
});

// CORRECT: Use computed or avoid self-modification
const doubleCount = computed(() => count.value * 2);
</script>

Efficient Deep Watching

When you must watch complex objects:

<script setup>
import { ref, watch, toRaw } from 'vue';

const formData = ref({
  personal: { name: '', email: '' },
  address: { street: '', city: '' },
  preferences: {
    /* many properties */
  },
});

// BAD: Watches everything, including preferences changes
watch(
  formData,
  () => {
    saveForm();
  },
  { deep: true },
);

// GOOD: Watch only the sections you care about
watch(
  () => formData.value.personal,
  () => savePersonalSection(),
  { deep: true }, // Deep only on this small subtree
);

// GOOD: Watch serialized version for change detection
watch(
  () => JSON.stringify(formData.value),
  () => {
    markFormDirty();
  },
);
</script>

Array Mutation Gotcha

<script setup>
import { ref, watch } from 'vue';

const items = ref([1, 2, 3]);

// This watch won't trigger on sort/reverse without deep!
watch(items, () => {
  console.log('Items changed');
});

items.value.sort(); // Watch doesn't fire - array reference unchanged

// Solution 1: Use deep (performance cost)
watch(items, callback, { deep: true });

// Solution 2: Replace array instead of mutating
items.value = [...items.value].sort();
</script>

Reference