Ir al contenido
wannacode

Velocidad: cuándo se vuelve a renderizar un componente

En resumen: En Vue se vuelve a renderizar el componente que lee un valor cambiado, y un hijo cuando sus props son nuevas; primero mides, y después computed mantiene el mismo resultado.

Problema

Cada letra renderiza toda la lista

Arriba hay un campo de búsqueda y un botón «Buscar». La lista se filtra solo después de «Buscar». ProductList escribe en la consola cada vez que se renderiza. Escribe «leche» en el campo y mira la consola.

Vue

Cinco letras, cinco líneas nuevas «renderizo ProductList». La lista es la misma: todavía no has pulsado «Buscar». Aquí hay ocho productos y no lo notas. En un catálogo real hay 2000, cada uno con imagen y precio, y el campo empieza a ir por detrás de tus dedos.

La idea

Cuándo se vuelve a renderizar un componente

Un componente se vuelve a renderizar cuando cambian los datos que muestra. Pero qué son «sus datos» lo deciden de forma distinta las dos bibliotecas. Las dos dan el resultado correcto; la diferencia es cuánto trabajo hacen.

Vue sigue qué valores reactivos lee cada componente mientras se renderiza. Cuando cambia draft, Vue vuelve a renderizar solo los componentes que leyeron draft: aquí App. Un hijo se vuelve a renderizar si sus props son distintas de las anteriores (las compara una por una).

El contador de ProductList.vue está en onBeforeMount y onBeforeUpdate: Vue los llama antes del primer render del componente y antes de cada render siguiente.

Aquí la plantilla pasa :products="visible()": una función que filtra. Se ejecuta en cada render de App y devuelve un array nuevo. Para Vue eso es una prop nueva, así que renderiza también ProductList.

Primero mide, después arregla. Un contador en la consola, como aquí, es la medición más sencilla. Las Vue DevTools tienen una Timeline con los renders de los componentes y sus tiempos. Arreglas solo un render que sea a la vez innecesario y lento.

Míralo

Mira quién se ilumina

<script setup>
const draft = ref('');
const query = ref('');
const visible = computed(() => filterByName(PRODUCTS, query.value));
</script>
<template>
<input v-model="draft" />
<button @click="query = draft">Buscar</button>
<ProductList :products="visible" @add="add" />
</template>
<!-- ProductList.vue -->
<script setup>
defineProps(['products']);
onBeforeUpdate(() => console.log('renderizo ProductList'));
</script>
Paso a paso, en palabras
  1. App guarda el texto del campo (draft) y la búsqueda (query). ProductList dibuja las tarjetas. La consola cuenta cuántas veces se renderiza la lista.
  2. Antes del arreglo la plantilla llamaba a una función, visible(). Cada render de App creaba un array nuevo, así que Vue veía una prop nueva y volvía a renderizar ProductList.
  3. computed conserva el mismo array mientras query sea la misma. Vue sigue lo que lee cada componente y compara las props del hijo.
  4. Ahora una tecla renderiza solo App y el campo. Las props de ProductList son las mismas, así que se omite, y el contador se queda.
  5. «Buscar» cambia query. El array es nuevo, así que ProductList se renderiza. Este render es necesario: la lista muestra ahora dos productos.
  6. Primero mide: un contador en la consola o el Profiler de las DevTools. Arregla solo los renders que son a la vez innecesarios y lentos.
Vue no renderiza el hijo si sus props son las mismas. Por eso importa lo que pasas: computed conserva el array, mientras que una función en la plantilla da uno nuevo en cada render.

Pruébalo

Solo «Buscar» renderiza la lista

Haz que escribir en el campo y «Añadir» no vuelvan a renderizar ProductList. Solo «Buscar» debe renderizarla, porque solo entonces cambia la lista. La comprobación escribe «leche», añade dos productos, pulsa «Buscar» y cuenta las líneas «renderizo ProductList» de la consola.

Vue

Pruébalo

Encuentra el error

visible es computed, y la lista se sigue renderizando con cada letra. ¿Qué línea tiene la culpa?

Elige la línea con el error.

Pruébalo

Completa

¿Cómo se mantiene igual la lista cara entre renders?

const visible = (
  () => PRODUCTS.filter((p) => p.name.includes(query.value)),
);

La idea

Una lista larga: renderizar menos

El elemento más rápido es el que no se dibuja. Una persona ve 10–20 productos en la pantalla, no 2000. Por eso una lista larga dibuja solo una parte:

  • «Mostrar más»: los primeros 20, y con un clic 20 más. Solo se recuerda cuántos se muestran.
  • Páginas: «1, 2, 3 …», a menudo con el número en la dirección (/?page=2), para poder compartirla.
  • Una lista virtual (windowing): dibuja solo las filas visibles y las cambia al desplazarse. Es difícil de escribir, así que usas una biblioteca: TanStack Virtual o vue-virtual-scroller.

Las claves de la lección sobre listas ayudan también aquí: con key={p.id} (:key="p.id") los 20 productos añadidos son elementos nuevos, y los antiguos se quedan sin rehacerse.

La idea

Carga diferida

No todas las partes de la página hacen falta de golpe. Las reseñas del producto, el mapa de las tiendas, el editor del perfil: pueden cargarse solo cuando la persona los abre. Entonces la primera carga descarga menos código.

import { defineAsyncComponent } from 'vue';
const Reviews = defineAsyncComponent(() => import('./Reviews.vue'));

import('./Reviews.vue') es una llamada a una función que carga el archivo y devuelve una promesa. defineAsyncComponent la convierte en un componente que se usa como cualquier otro: <Reviews v-if="open" />.

Vue

Abre la consola: la línea de reviews.js aparece solo después del clic. Para ser sinceros, en el sandbox todos los archivos ya están en la página, así que aquí import() solo ejecuta el archivo más tarde. En un proyecto real Vite pone cada uno de esos archivos aparte y el navegador lo descarga del servidor en el primer clic. Lo mismo vale para las páginas: en Vue Router component: () => import('./CartPage.vue') carga la página la primera vez que se navega a ella.

Más precisamente, para curiosos

v-memo. Para un v-for muy largo Vue tiene v-memo="[p.id === selected]": la fila se vuelve a renderizar solo si cambian los valores del array. La documentación lo llama una microoptimización para casos raros. Un array equivocado significa una actualización perdida.

El estado hacia abajo. El campo con su draft puede ser un componente aparte SearchBox que emite search hacia arriba solo con «Buscar». Entonces las letras cambian solo SearchBox.

computed con un resultado estable. Desde Vue 3.4, un computed que devuelve el mismo valor primitivo (por ejemplo true) no dispara lo que depende de él. Para un objeto puedes devolver el anterior (computed((old) => …)) si el contenido es el mismo.

El modo de desarrollo es más lento. Las advertencias y las comprobaciones cuestan tiempo. Mide la velocidad de la versión construida (vite build y vite preview), no solo en desarrollo.

Ejercicio

Mostrar más

El catálogo tiene 10 productos y los dibuja todos. Muestra los primeros 4, y debajo «Mostrando 4 de 10» y un botón «Mostrar más» que añade 4 más. Cuando todos están mostrados, el botón desaparece. La comprobación cuenta los productos después de cada clic y mira el texto y el botón.

Vue

Test

Ponte a prueba

1. La página de producto va lenta. ¿Qué es lo primero que haces?
2. App pasa :products="visible()", donde visible es una función que filtra. Escribes en el campo de App. ¿Se renderiza ProductList?
3. El catálogo tiene 5000 productos y la página se abre despacio. ¿Qué ayuda más?
4. ¿Qué aporta la carga diferida de un componente (lazy / defineAsyncComponent) en un proyecto real?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·