vue-performance

SkillDev tools

Vue Best Practices

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the vue-performance skill

What this skill tells your AI

The instructions your AI receives, as published by caido-community/shift in .agents/skills/vue-performance/SKILL.md and read by ahel’s review.

Update Optimizations

Props Stability

  • Keep props passed to child components as stable as possible.
  • Instead of passing reactive IDs and comparing inside child:
<!-- Bad: Every ListItem updates when activeId changes -->
<ListItem
  v-for="item in list"
  :id="item.id"
  :active-id="activeId" />
  • Compute the derived value in the parent and pass it directly:
<!-- Good: Only items whose active status changed will update -->
<ListItem
  v-for="item in list"
  :id="item.id"
  :active="item.id === activeId" />

Computed Stability

  • Computed properties only trigger effects when their value changes.
  • Avoid returning new objects from computed properties when the underlying data hasn't changed:
// Bad: Creates new object every time, always triggers updates
const computedObj = computed(() => {
  return {
    isEven: count.value % 2 === 0
  }
})

// Good: Returns old value if nothing changed
const computedObj = computed((oldValue) => {
  const newValue = {
    isEven: count.value % 2 === 0
  }
  if (oldValue && oldValue.isEven === newValue.isEven) {
    return oldValue
  }
  return newValue
})
  • Always perform the full computation before comparing and returning the old value to ensure dependencies are collected.

Reduce Reactivity Overhead for Large Immutable Structures

  • For large arrays of deeply nested objects, use shallowRef() and shallowReactive() to opt-out of deep reactivity.
  • Shallow APIs create state reactive only at the root level, keeping nested property access fast.
  • When using shallow reactivity, treat nested objects as immutable and trigger updates by replacing the root state:
const shallowArray = shallowRef([/* big list of deep objects */])

// Bad: Won't trigger updates
shallowArray.value.push(newObject)
shallowArray.value[0].foo = 1

// Good: Replace the root state
shallowArray.value = [...shallowArray.value, newObject]
shallowArray.value = [
  { ...shallowArray.value[0], foo: 1 },
  ...shallowArray.value.slice(1)
]

MaybeRefOrGetter with toValue

  • Use MaybeRefOrGetter<T> type for flexible function parameters that can accept refs, getters, or plain values.
  • Use toValue() to extract the actual value from MaybeRefOrGetter parameters:
  1. Handle Async Operations with Error and Loading States

Always handle every possible state for data fetching or async logic, using separate components for each state: loading, success, error, and empty.

Example:

<script setup lang="ts">
import { ref } from "vue";
import type { User } from "@/types";

const user = ref<User | undefined>(undefined);
const loading = ref(true);
const error = ref<Error | undefined>(undefined);

async function fetchUserData(userId: string) {
  loading.value = true;
  error.value = undefined;

  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) throw new Error("Failed to fetch user data");
    user.value = await response.json();
  } catch (e) {
    error.value = e instanceof Error ? e : new Error("Unknown error");
    user.value = undefined;
  } finally {
    loading.value = false;
  }
}
</script>

<template>
  <div>
    <LoadingSpinner v-if="loading" />
    <ErrorMessage v-else-if="error !== undefined" :message="error.message" @retry="() => fetchUserData('replace-id')" />
    <UserProfile v-else-if="user !== undefined" :user="user" />
    <EmptyState v-else message="No user data available" />
  </div>
</template>

This ensures each state (loading, error, data, empty) is handled explicitly and the UI never displays an inconsistent result.

Signals

GitHub stars
57
Forks
13
Last commit
Jul 2026
Advanced
Catalog kind
skill
Gateway key
vue-performance
Source
github.com/caido-community/shift