defineAsyncComponent: carga lazy de componentes
defineAsyncComponent permite cargar componentes de forma diferida, es decir, solo cuando se necesitan. Esto reduce el tamaño del bundle inicial y acelera la carga de la aplicación.
Uso básico
import { defineAsyncComponent } from 'vue'
const HeavyChart = defineAsyncComponent(() =>
import('./components/HeavyChart.vue')
)
El componente se carga solo cuando se renderiza por primera vez en el template:
<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue'
const HeavyChart = defineAsyncComponent(() =>
import('./components/HeavyChart.vue')
)
const showChart = ref(false)
</script>
<template>
<button @click="showChart = true">Mostrar gráfico</button>
<HeavyChart v-if="showChart" />
</template>
Opciones avanzadas
defineAsyncComponent acepta un objeto con opciones para manejar estados de carga y error:
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
import LoadingSpinner from './LoadingSpinner.vue'
import ErrorMessage from './ErrorMessage.vue'
const HeavyDashboard = defineAsyncComponent({
loader: () => import('./HeavyDashboard.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorMessage,
delay: 200, // Milisegundos antes de mostrar el loading (evita flash)
timeout: 10000, // Tiempo límite antes de mostrar error
})
</script>
<template>
<Suspense>
<HeavyDashboard />
</Suspense>
</template>
loadingComponent: componente mostrado mientras se cargaerrorComponent: componente mostrado si la carga falladelay: retraso en milisegundos antes de mostrar el loading (por defecto 200ms)timeout: tiempo límite en milisegundos; si se supera, se muestra el error
v-memo: memorizar subárboles del template
v-memo permite memorizar una porción del template para evitar re-renderizados innecesarios. El subárbol solo se actualiza cuando cambian los valores del array de dependencias.
El caso de uso principal es optimizar listas grandes con v-for:
<script setup lang="ts">
import { ref } from 'vue'
interface Item {
id: number
name: string
selected: boolean
}
const items = ref<Item[]>([
{ id: 1, name: 'Elemento A', selected: false },
{ id: 2, name: 'Elemento B', selected: false },
{ id: 3, name: 'Elemento C', selected: true },
// ... miles de elementos
])
const selectedId = ref<number | null>(null)
function selectItem(id: number) {
selectedId.value = id
}
</script>
<template>
<div
v-for="item in items"
:key="item.id"
v-memo="[item.selected, selectedId === item.id]"
:class="{ selected: selectedId === item.id }"
@click="selectItem(item.id)"
>
<span>{{ item.name }}</span>
<span v-if="item.selected">✓</span>
</div>
</template>
En este ejemplo, cada elemento solo se re-renderiza si cambia item.selected o si pasa a ser el elemento seleccionado. Sin v-memo, todos los elementos se re-renderizarían cada vez que cambia selectedId.
Es importante no usar v-memo indiscriminadamente. Solo aporta beneficio en listas con muchos elementos (cientos o miles) donde el re-renderizado tiene un coste medible.
v-once: contenido estático renderizado una sola vez
v-once renderiza un elemento y sus hijos una única vez. Después del renderizado inicial, Vue no rastrea cambios en las expresiones contenidas, lo que ahorra memoria y procesamiento:
<script setup lang="ts">
import { ref } from 'vue'
const appVersion = ref('3.5.0')
const dynamicCounter = ref(0)
</script>
<template>
<!-- Este bloque se renderiza una vez y nunca se actualiza -->
<header v-once>
<h1>Mi aplicación v{{ appVersion }}</h1>
<p>Información estática que no cambia</p>
</header>
<!-- Este bloque si se actualiza normalmente -->
<main>
<p>Contador: {{ dynamicCounter }}</p>
<button @click="dynamicCounter++">Incrementar</button>
</main>
</template>
v-once es útil para bloques de contenido que dependen de datos que se cargan una vez y no cambian, como información de configuración o textos legales.
shallowRef y shallowReactive
Cuando se trabaja con objetos grandes que se reemplazan completos en lugar de mutar sus propiedades internas, shallowRef evita el coste de la reactividad profunda:
<script setup lang="ts">
import { shallowRef, triggerRef } from 'vue'
interface LargeDataset {
rows: Array<{ id: number; values: number[] }>
metadata: Record<string, unknown>
}
// Solo rastrea cambios en .value, no en propiedades internas
const dataset = shallowRef<LargeDataset>({
rows: [],
metadata: {},
})
async function loadData() {
const response = await fetch('/api/dataset')
const data = await response.json()
// Reemplazar el objeto completo dispara la reactividad
dataset.value = data
}
function updateRow(index: number, newValues: number[]) {
// Mutar internamente NO dispara actualización automática
dataset.value.rows[index].values = newValues
// Se debe forzar manualmente si se necesita
triggerRef(dataset)
}
</script>
<template>
<div>
<p>Total filas: {{ dataset.rows.length }}</p>
<button @click="loadData">Cargar datos</button>
</div>
</template>
shallowReactive funciona de forma similar para objetos reactivos, rastreando solo el primer nivel de propiedades:
import { shallowReactive } from 'vue'
const state = shallowReactive({
items: [], // Cambiar state.items = [...] SI dispara reactividad
config: {}, // Cambiar state.config = {...} SI dispara reactividad
})
// Pero state.config.theme = 'dark' NO dispararia reactividad
Computed caching vs methods
Las propiedades computed almacenan su resultado en caché y solo se recalculan cuando cambian sus dependencias reactivas. Los métodos se ejecutan cada vez que se accede a ellos en el template:
<script setup lang="ts">
import { ref, computed } from 'vue'
const items = ref([
{ name: 'A', price: 10, quantity: 2 },
{ name: 'B', price: 25, quantity: 1 },
{ name: 'C', price: 5, quantity: 10 },
])
// Cacheado: solo se recalcula si items cambia
const totalPrice = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
// Sin cache: se ejecuta cada vez que el template se re-renderiza
function calculateTotal(): number {
return items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
</script>
<template>
<!-- Usa el valor cacheado -->
<p>Total (computed): {{ totalPrice }}</p>
<!-- Ejecuta la función cada vez -->
<p>Total (method): {{ calculateTotal() }}</p>
</template>
Para cálculos costosos a los que se accede múltiples veces en el template, computed es significativamente más eficiente.
Gestión eficiente de keys en v-for
La propiedad key en v-for permite a Vue identificar qué nodos del DOM corresponden a cada elemento de la lista. Usar keys únicas y estables evita re-renderizados innecesarios:
<script setup lang="ts">
import { ref } from 'vue'
interface User {
id: number
name: string
email: string
}
const users = ref<User[]>([])
function addUser(user: User) {
users.value.push(user)
}
function removeUser(id: number) {
users.value = users.value.filter(u => u.id !== id)
}
</script>
<template>
<!-- Correcto: usar un identificador único y estable -->
<div v-for="user in users" :key="user.id">
{{ user.name }}
</div>
<!-- Evitar: usar el índice como key causa problemas al reordenar o eliminar -->
<div v-for="(user, index) in users" :key="index">
{{ user.name }}
</div>
</template>
Cuando se usa el índice como key, al eliminar un elemento del medio de la lista, Vue reutiliza nodos DOM incorrectamente, lo que puede causar errores visuales y de estado en componentes hijos.
Virtual scrolling para listas grandes
Cuando una lista contiene miles de elementos, renderizar todos en el DOM es ineficiente. El virtual scrolling renderiza solo los elementos visibles en el viewport:
<script setup lang="ts">
import { ref, computed } from 'vue'
const allItems = ref(Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `Elemento ${i + 1}`,
})))
const itemHeight = 40 // px
const containerHeight = 400 // px
const scrollTop = ref(0)
const visibleCount = Math.ceil(containerHeight / itemHeight) + 1
const startIndex = computed(() =>
Math.floor(scrollTop.value / itemHeight)
)
const visibleItems = computed(() =>
allItems.value.slice(startIndex.value, startIndex.value + visibleCount)
)
const totalHeight = computed(() =>
allItems.value.length * itemHeight
)
const offsetY = computed(() =>
startIndex.value * itemHeight
)
function onScroll(event: Event) {
scrollTop.value = (event.target as HTMLElement).scrollTop
}
</script>
<template>
<div
class="virtual-list"
:style="{ height: containerHeight + 'px', overflow: 'auto' }"
@scroll="onScroll"
>
<div :style="{ height: totalHeight + 'px', position: 'relative' }">
<div :style="{ transform: `translateY(${offsetY}px)` }">
<div
v-for="item in visibleItems"
:key="item.id"
:style="{ height: itemHeight + 'px' }"
class="virtual-list__item"
>
{{ item.text }}
</div>
</div>
</div>
</div>
</template>
Para casos más avanzados, se recomienda usar bibliotecas especializadas como vue-virtual-scroller que manejan alturas variables, scroll horizontal y reciclaje de nodos DOM.
Tree-shaking y bundle analysis
Vue está diseñado para ser tree-shakeable. Las APIs que no se importan se eliminan del bundle final. Esto significa que se debe importar solo lo necesario:
// Bien: importa solo lo que se usa
import { ref, computed, watch } from 'vue'
// Evitar: no importar todo Vue si solo se necesitan unas pocas APIs
// import * as Vue from 'vue'
Para analizar el tamaño del bundle y detectar dependencias pesadas, se puede usar rollup-plugin-visualizer:
npm install -D rollup-plugin-visualizer
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
vue(),
visualizer({
open: true,
filename: 'bundle-report.html',
gzipSize: true,
}),
],
})
Al ejecutar npm run build, se genera un archivo HTML interactivo que muestra el tamaño de cada módulo en el bundle.
Lazy loading de imágenes
Las imágenes son con frecuencia el recurso más pesado de una página. Se pueden cargar de forma diferida usando el atributo nativo loading="lazy" o un IntersectionObserver:
<script setup lang="ts">
interface ImageItem {
id: number
src: string
alt: string
}
defineProps<{
images: ImageItem[]
}>()
</script>
<template>
<div class="gallery">
<img
v-for="image in images"
:key="image.id"
:src="image.src"
:alt="image.alt"
loading="lazy"
decoding="async"
/>
</div>
</template>
El atributo loading="lazy" delega en el navegador la decisión de cuándo cargar la imagen, generalmente cuando está cerca del viewport. decoding="async" permite que la decodificación de la imagen no bloquee el hilo principal.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Vuejs es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Vuejs
Explora más contenido relacionado con Vuejs y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje