¿Comó usar v-memo?

En el desarrollo de aplicaciones con Vue.js, especialmente cuando se trabaja con listas largas de elementos, es crucial optimizar el renderizado para garantizar un buen rendimiento. Una de las herramientas que Vue.js ofrece para este propósito es la directiva v-memo. En este artículo, se analiza un ejemplo práctico de cómo utilizar v-memo para mejorar el rendimiento en una lista de tareas y se exploran algunas mejoras adicionales que pueden aplicarse.

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

¿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.

// src/components/task/TaskContainer.vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import { faker } from '@faker-js/faker'
import TaskItem, { type TaskItemInterface } from '@/components/task/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.id, 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.:

// src/components/task/TaskItem.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:

//src/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

¿Mejora el Renderizado?

En este caso, el uso de v-memo puede mejorar significativamente el rendimiento, especialmente cuando se trabaja con listas largas. Al memoizar cada TaskItem basado en su id y su estado completed, Vue evitará re-renderizar los elementos de la lista cuyos id y completed no hayan cambiado. Esto es particularmente útil en dos escenarios:

  1. Cambio de Filtro: Al cambiar el filtro de "Todas" a "Completadas" o "Pendientes", Vue solo re-renderizará los elementos que cumplen con el nuevo filtro, evitando re-renderizar los que ya estaban en el estado correcto.

  2. Cambio de Estado de una Tarea: Al cambiar el estado de una tarea (de completada a pendiente o viceversa), solo ese TaskItem específico se re-renderizará, no toda la lista.

Mejoras Adicionales

Aunque el uso de v-memo es una buena práctica, hay algunas mejoras adicionales que pueden considerarse para optimizar aún más el rendimiento:

  1. Evitar el Uso de v-for con v-memo en Listas Muy Grandes:
    Aunque v-memo ayuda a optimizar el renderizado, en listas extremadamente grandes (por ejemplo, miles de elementos), el uso de v-for con v-memo puede no ser suficiente. En esos casos, se recomienda utilizar técnicas de "virtualización" de listas para renderizar solo los elementos visibles en la pantalla.

  2. Optimizar la Generación de Tareas:
    Si bien faker es útil para generar datos de prueba, en un entorno real, es posible que no sea necesario generar 1000 tareas de inmediato. Se podría considerar cargar las tareas de manera dinámica o paginada para mejorar el rendimiento inicial.

  3. Uso de key Único:
    Es importante utilizar una clave única (:key="task.id") para que Vue pueda identificar y reutilizar correctamente los elementos de la lista. Asegúrese de que task.id sea único y estable.

  4. Considerar el Uso de shallowRef:
    Si tasks no necesita ser reactivo en profundidad (es decir, no se necesitan cambios reactivos en las propiedades de cada tarea), se podría usar shallowRef en lugar de ref. Esto puede reducir la sobrecarga de reactividad en listas grandes.

    typescript

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

Conclusión

El uso de v-memo en Vue.js es una técnica efectiva para optimizar el renderizado de listas largas, evitando re-renderizados innecesarios. Sin embargo, en aplicaciones con listas extremadamente grandes o con requisitos de rendimiento más estrictos, es recomendable considerar técnicas adicionales como la virtualización de listas o el uso de shallowRef.

Con estas mejoras, se puede garantizar un rendimiento óptimo en aplicaciones Vue.js que manejan grandes volúmenes de datos, proporcionando una experiencia de usuario fluida y eficiente.