Todos los artículos
Rendimiento· Avanzado

Cómo usar v-memo en Vue 3: dependencias y medición de rendimiento

Optimiza una lista con v-memo, elige dependencias completas y compara el resultado antes de adoptarlo.

LA

Luis Aldair Quispe Rios

5 min de lectura
Diagrama ilustrativo del uso de la directiva v-memo en Vue.js para optimizar el rendimiento de componentes.

Objetivo y requisitos

Vamos a comparar la actualización de una lista de tareas con y sin v-memo. Necesitas Vue 3.2 o superior y un proyecto Vue con Vite y TypeScript. Instala el generador de datos de ejemplo con npm install @faker-js/faker; usa una versión compatible con el Node de tu proyecto. Crea la carpeta src/components/task para los dos componentes siguientes.

¿Qué es v-memo?

v-memo es una directiva en Vue.js que permite memoizar un subárbol del DOM. Esto significa que Vue solo volverá a renderizar ese subárbol si alguno de los valores en el array de dependencias cambia. En el contexto de una lista de tareas, esto puede ser especialmente útil para evitar re-renderizados innecesarios de elementos que no han cambiado.

<div v-memo="[dependencia1, dependencia2]">
  <!-- Contenido que solo se re-renderiza si dependencia1 o dependencia2 cambian -->
</div>

Ejemplo Práctico

En el siguiente ejemplo, se tiene un componente TaskContainer.vue que muestra una lista de 1000 tareas generadas aleatoriamente. Cada tarea tiene un id, un title y un estado completed. El componente permite filtrar las tareas por estado (Todas, Completadas, Pendientes) y cambiar el estado de una tarea individual.

<script setup lang="ts">
import { ref, computed } from 'vue'
import { faker } from '@faker-js/faker'
import TaskItem, { type TaskItemInterface } from './TaskItem.vue'

export type FilterStatus = 'completed' | 'pending' | 'all'

export interface Props {
  filter: FilterStatus
}

const props = defineProps<Props>()

function generateRandomTask(): TaskItemInterface {
  return {
    id: faker.string.uuid(),
    title: faker.lorem.words(),
    completed: faker.datatype.boolean(),
  }
}

const tasks = ref(Array.from({ length: 1000 }, () => generateRandomTask()))

const toggleTaskStatus = (taskId: string) => {
  const task = tasks.value.find((t) => t.id === taskId)
  if (task) {
    task.completed = !task.completed
  }
}

const filteredTasks = computed(() => {
  if (props.filter === 'completed') {
    return tasks.value.filter((task) => task.completed)
  } else if (props.filter === 'pending') {
    return tasks.value.filter((task) => !task.completed)
  } else {
    return tasks.value
  }
})
</script>

<template>
  <div class="mt-4">
    <div
      v-for="task in filteredTasks"
      :key="task.id"
      v-memo="[task.title, task.completed]"
      class="mb-4"
    >
      <TaskItem :task="task" @toggle="toggleTaskStatus(task.id)" />
    </div>
  </div>
</template>

Al pasar filter como una prop, se logra una clara separación de responsabilidades entre el componente padre (App.vue) y el componente hijo (TaskContainer). Esto significa:

  • El componente padre (App.vue) se encarga de manejar el estado global de la aplicación, como el valor actual del filtro. Esto permite que el padre controle cómo y cuándo cambia el filtro, lo que facilita la gestión del estado en aplicaciones más grandes.

  • El componente hijo (TaskContainer) se enfoca únicamente en recibir el filtro y mostrar las tareas correspondientes. Esto hace que el componente sea más reutilizable y fácil de mantener, ya que no necesita preocuparse por cómo se obtiene o cambia el filtro.

Componente hijo TaskItem.vue usado en TaskContainer.vue.:

<script setup lang="ts">
export interface Props {
  task: TaskItemInterface
}

export interface TaskItemInterface {
  id: string
  title: string
  completed: boolean
}

const props = defineProps<Props>()

const emits = defineEmits<{
  toggle: []
}>()
</script>

<template>
  <div class="p-4 bg-white rounded-lg shadow-md">
    <small>
      {{ props.task.id }}
    </small>
    <p
      :class="{
        'line-through text-gray-500': props.task.completed,
        'text-black': !props.task.completed,
      }"
    >
      {{ props.task.title }}
    </p>
    <button
      @click="emits('toggle')"
      class="mt-2 px-4 py-2 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75"
    >
      {{ props.task.completed ? 'Marcar como pendiente' : 'Marcar como completada' }}
    </button>
  </div>
</template>

Componente padre App.vue:

<script setup lang="ts">
import { ref } from 'vue'
import TaskContainer, { type FilterStatus } from './components/task/TaskContainer.vue'

const filter = ref<FilterStatus>('all')

const setFilter = (newFilter: FilterStatus) => {
  filter.value = newFilter
}
</script>

<template>
  <main class="container mx-auto p-4 w-full">
    <h1 class="text-4xl font-bold">Lista de tareas</h1>
    <div class="h-[calc(100vh-72px)] overflow-auto mt-4">
      <div class="flex space-x-2 mb-4">
        <button
          @click="setFilter('all')"
          class="px-4 py-2 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75"
        >
          Todas
        </button>
        <button
          @click="setFilter('completed')"
          class="px-4 py-2 bg-green-500 text-white font-semibold rounded-lg shadow-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-opacity-75"
        >
          Completadas
        </button>
        <button
          @click="setFilter('pending')"
          class="px-4 py-2 bg-yellow-500 text-white font-semibold rounded-lg shadow-md hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-yellow-400 focus:ring-opacity-75"
        >
          Pendientes
        </button>
      </div>
      <TaskContainer :filter="filter" />
    </div>
  </main>
</template>

En este ejemplo, el componente TaskContainer recibe la prop filter desde el componente padre (App.vue). Esto permite que el padre controle el estado del filtro, mientras que el hijo se enfoca únicamente en mostrar las tareas correspondientes. Esta separación de responsabilidades hace que el código sea más modular y fácil de mantener.Separación de responsabilidades

Dependencias completas y resultado esperado

La identidad ya se expresa con :key="task.id". En este ejemplo el contenido visible depende de title y completed, por eso ambos aparecen en v-memo. Si añades otro dato visible o una prop del padre, incluye también esa dependencia. Omitirla puede congelar contenido que debería actualizarse.

Ejecuta npm run dev, cambia entre Todas, Completadas y Pendientes y pulsa el botón de una tarea. Su estado debe cambiar y, en una lista filtrada, la tarea debe desaparecer si ya no cumple el filtro. Ejecuta npm run build antes de comparar rendimiento.

Mide antes de adoptarlo

Compara el mismo conjunto de datos, el mismo dispositivo y la misma secuencia de acciones con y sin v-memo. Usa el panel Performance del navegador o Vue Devtools para observar el trabajo de actualización. No cambies datos aleatorios entre ambas ejecuciones: guarda un conjunto fijo para la comparación. El filtrado y la creación inicial de nodos siguen teniendo coste; v-memo no convierte una lista completa en una lista virtualizada.

shallowRef requiere otro patrón de actualización

El ejemplo utiliza ref y modifica task.completed. Cambiar solo ref por shallowRef dejaría esa mutación anidada sin disparar una actualización. Si eliges shallowRef, reemplaza el array y los objetos modificados:

import { shallowRef } from 'vue'

type Task = { id: string; title: string; completed: boolean }
const tasks = shallowRef<Task[]>([{ id: '1', title: 'Aprender Vue', completed: false }])

function toggleTaskStatus(taskId: string) {
  tasks.value = tasks.value.map((task) =>
    task.id === taskId ? { ...task, completed: !task.completed } : task,
  )
}

Este bloque es una alternativa independiente; no lo pegues junto a las declaraciones del componente anterior. Para miles de filas, considera también paginación o virtualización. Adopta v-memo cuando la medición justifique la complejidad y verifica que ningún dato queda desactualizado. Consulta v-memo y la guía de rendimiento.

Sigue leyendo

defineAsyncComponent en Vue 3: Implementación y casos de uso.

defineAsyncComponent en Vue 3: Implementación y casos de uso.

Carga componentes bajo demanda y muestra estados de carga y error con defineAsyncComponent.
¿Cómo integrar Tailwind 4 en Vue.js 3 + Vite?

¿Cómo integrar Tailwind 4 en Vue.js 3 + Vite?

Configura Tailwind CSS 4 en Vue 3 con los plugins de Vue y Tailwind para Vite.
watch vs. watchEffect en Vue 3: evita cotizaciones de envío desactualizadas

watch vs. watchEffect en Vue 3: evita cotizaciones de envío desactualizadas

Evita respuestas HTTP desactualizadas con watch, debounce y cancelación en un cotizador de envíos.