Descubre qué son LazyVStack y LazyHStack en SwiftUI — stacks perezosos para el renderizado eficiente de listas desplazables, cuadrículas y carruseles en iOS, macOS, watchOS y tvOS. A diferencia de VStack y HStack normales, los stacks perezosos crean elementos solo cuando aparecen en el área visible, lo que reduce críticamente el consumo de memoria al trabajar con grandes conjuntos de datos. La arquitectura de los stacks perezosos se basa en el protocolo Layout y está integrada con identificación a través de ForEach y ScrollView.
Puntos clave
LazyVStack y LazyHStack son contenedores de diseño en SwiftUI que crean y muestran vistas hijas solo cuando es necesario, al volverse visibles en el área desplazable. LazyVStack dispone los elementos verticalmente (de arriba abajo), mientras que LazyHStack los dispone horizontalmente (de izquierda a derecha).
Ambos stacks fueron introducidos por Apple en SwiftUI 2.0 (iOS 14, macOS 11, watchOS 7, tvOS 14) junto con LazyVGrid y LazyHGrid. Antes de la aparición de los stacks perezosos, los desarrolladores tenían que usar UITableView y UICollectionView a través de UIViewRepresentable para trabajar eficientemente con listas grandes. LazyVStack eliminó esta necesidad proporcionando una interfaz nativa de SwiftUI con carga perezosa automática.
Según la sesión WWDC 10031 de Apple (2020), los stacks perezosos utilizan un mecanismo de creación diferida de vistas: SwiftUI almacena los datos de origen (por ejemplo, un array de modelos) y crea instancias de vista justo antes de renderizarlas en pantalla. Al desplazarse, los stacks reutilizan las vistas ya creadas, evitando nuevas asignaciones — esto reduce la carga en el asignador de memoria y el recolector de basura de Swift.
Para trabajar con stacks perezosos, colócalos siempre dentro de un ScrollView — sin desplazamiento, los elementos que se extienden más allá de los límites de la pantalla simplemente se recortarán, no se crearán perezosamente.
El mecanismo de carga perezosa en LazyVStack se basa en la geometría: SwiftUI rastrea la posición de cada vista hija en relación con el contenedor ScrollView. Cuando un elemento cruza el límite del área visible (con un pequeño búfer de unos pocos puntos), el sistema llama a su inicializador y renderiza el contenido. Cuando un elemento sale de la pantalla, SwiftUI destruye la vista pero conserva el estado a través de @State si está marcado como preservable.
Este enfoque difiere de VStack, donde todas las vistas hijas se crean inmediatamente al inicializar el contenedor, independientemente de su visibilidad. Para una lista de 10 000 elementos, VStack creará 10 000 instancias de vista en memoria, mientras que LazyVStack creará solo las que caben en la pantalla (generalmente 8–15).
LazyVStack acepta tres parámetros de configuración: alignment (HorizontalAlignment — leading, center, trailing), spacing (CGFloat — espacio entre elementos), y pinnedViews (PinnedScrollableViews — fijación de encabezados de sección). LazyHStack usa los mismos parámetros, pero alignment acepta VerticalAlignment (top, center, bottom).
La principal diferencia entre LazyVStack y VStack es la estrategia de creación de elementos hijos. VStack (stack eager) calcula el tamaño y la posición de todas las vistas hijas en el momento del renderizado, lo que lo hace inadecuado para listas dinámicas grandes. LazyVStack (stack lazy) pospone la creación hasta que el elemento se vuelve visible.
Comparemos el comportamiento con una lista de 1000 líneas de texto. VStack cargará las 1000 líneas en memoria de inmediato, llamando al inicializador de cada línea y asignando memoria para ella. Esto provoca degradación del rendimiento en dispositivos débiles (iPhone SE, iPad mini) y aumenta el tiempo de inicio de pantalla. LazyVStack cargará solo las 10–12 líneas visibles, creando el resto a medida que se desplaza.
Una prueba práctica (usando Xcode Instruments, perfil Allocations) muestra: en un iPhone 12 mini, una lista de 5000 elementos con LazyVStack consume 3–5 MB de memoria, mientras que VStack con el mismo contenido consume 150–250 MB — 50 veces más. Mientras tanto, el tiempo de renderizado inicial para LazyVStack es de ~50 ms frente a ~800 ms para VStack en el mismo dispositivo.
Elige VStack para listas estáticas o cortas (hasta 10–15 elementos), y LazyVStack para cualquier lista dinámica o potencialmente larga. Apple recomienda usar LazyVStack por defecto si no estás seguro del tamaño máximo de la lista.
VStack sigue siendo la mejor opción para interfaces estáticas: pantalla de perfil, formulario de inicio de sesión, tarjeta de producto — donde el número de elementos es conocido y no supera los 10–15. VStack funciona más rápido en el renderizado inicial para tales cantidades porque no desperdicia recursos en el seguimiento de geometría y la carga perezosa. Además, VStack funciona correctamente fuera de ScrollView (por ejemplo, dentro de ZStack o Group), mientras que LazyVStack sin ScrollView pierde su propósito.
Los stacks perezosos son óptimos para escenarios con un número grande o impredecible de elementos: feeds de redes sociales, catálogos de productos, listas de chats, bibliotecas de archivos multimedia, registros de eventos, paneles de administración con miles de registros.
Casos de uso específicos: lista de mensajes en un mensajero (decenas de miles de mensajes), carrusel de imágenes en una aplicación de galería, feed de noticias con carga infinita, lista de pedidos en una tienda en línea. LazyHStack es especialmente útil para carruseles horizontales — por ejemplo, Historias de Instagram o banners promocionales.
Contraindicaciones: interfaces con animaciones de aparición de elementos (los stacks lazy no admiten transiciones entre estados de eliminación de elementos sin lógica adicional), casos donde todos los elementos deben ser visibles simultáneamente (una lista corta de casillas de verificación), y cuando necesitas un control preciso sobre la reutilización de celdas (en cuyo caso List o Table pueden ser preferibles).
Un ejemplo básico muestra 1000 elementos con consumo mínimo de memoria. Elementos clave: ScrollView como contenedor de desplazamiento, LazyVStack para carga perezosa, ForEach con un identificador para la iteración de datos.
import SwiftUI
struct LazyListExample: View {
let items = Array(0..<1000)
var body: some View {
ScrollView {
LazyVStack(spacing: 8) {
ForEach(items, id: \.self) { index in
Text("Elemento #\(index)")
.font(.body)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
}
.padding()
}
}
}
El código crea un ScrollView que contiene un LazyVStack con espaciado de 8pt entre elementos. ForEach itera sobre el array items y crea un Text para cada índice. Gracias a la carga perezosa, de 1000 elementos solo los 10–12 visibles están en memoria a la vez.
Este ejemplo muestra la agrupación de elementos por secciones con encabezados fijados, similar a los contactos de iOS. Section define el encabezado y el contenido, pinnedViews: .sectionHeaders fija el encabezado en la parte superior de la pantalla al desplazarse.
import SwiftUI
struct SectionedList: View {
let cities = ["Moscú", "Londres", "Tokio", "Nueva York", "París"]
let countries = ["Rusia", "Reino Unido", "Japón", "EE. UU.", "Francia"]
var body: some View {
ScrollView {
LazyVStack(pinnedViews: .sectionHeaders) {
Section(header: Text("Ciudades").font(.title).bold()) {
ForEach(cities, id: \.self) { city in
Text(city).padding(8)
}
}
Section(header: Text("Países").font(.title).bold()) {
ForEach(countries, id: \.self) { country in
Text(country).padding(8)
}
}
}
}
}
}
Los encabezados fijados (.sectionHeaders) se comportan como los section headers de UITableView: al desplazar una sección, el encabezado se "adhiere" al borde superior de la pantalla hasta que toda la sección desaparece, luego es reemplazado por el encabezado de la siguiente sección. pinnedViews se pueden combinar: .sectionHeaders y .sectionFooters simultáneamente.
LazyHStack se usa para desplazamiento horizontal — carruseles de imágenes, listas horizontales de categorías. El parámetro alignment: .top alinea los elementos al borde superior.
import SwiftUI
struct HorizontalCarousel: View {
let colors: [Color] = [.red, .blue, .green, .orange, .purple, .pink]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 16, alignment: .top) {
ForEach(0..<100, id: \.self) { index in
RoundedRectangle(cornerRadius: 12)
.fill(colors[index % colors.count])
.frame(width: 150, height: 200)
.overlay(Text("\(index + 1)").foregroundColor(.white).bold())
}
}
.padding(.horizontal)
}
.frame(height: 220)
}
}
El código crea un ScrollView horizontal con LazyHStack. De 100 rectángulos, solo 2–3 se muestran simultáneamente (dependiendo del ancho de pantalla y el tamaño de los elementos). Al desplazarse a la izquierda, los nuevos elementos se cargan perezosamente. La altura del contenedor es fija (220pt) para evitar altura infinita en el desplazamiento horizontal.
PinnedScrollableViews es una opción de configuración para LazyVStack y LazyHStack que controla la fijación de encabezados y pies de sección al desplazarse. Se admiten dos valores: sectionHeaders (los encabezados se adhieren al inicio del contenedor) y sectionFooters (los pies se adhieren al final).
El mecanismo de pinned views funciona solo dentro de un contenedor Section anidado en LazyVStack. Cada Section tiene un header y/o footer que automáticamente obtienen el comportamiento de adhesión. SwiftUI rastrea la posición de cada sección en relación con los límites de ScrollView y cambia la visibilidad del elemento fijado al hacer la transición entre secciones.
Importante: pinnedViews aumenta la complejidad del cálculo del diseño, ya que SwiftUI debe recalcular constantemente qué encabezado está actualmente fijado. Usa pinnedViews solo cuando la funcionalidad sea realmente necesaria — para listas simples sin secciones, es mejor omitir este parámetro. Apple en su documentación (Human Interface Guidelines, 2024) recomienda usar encabezados fijados para índices alfabéticos y agrupación por fechas.
El uso correcto de identificadores es el factor de rendimiento más importante para LazyVStack. Cada elemento en ForEach debe tener un id único y estable. Usar \.self con primitivos (Int, String) es aceptable, pero para modelos de datos siempre implementa el protocolo Identifiable. Los ids inestables (por ejemplo, UUID generado cada vez) hacen que SwiftUI recreé todas las vistas en cada actualización.
Evita cálculos pesados dentro del body de cada elemento del stack. Si un elemento contiene un diseño complejo o procesamiento de datos — extrae la lógica en una estructura de vista separada con su propia carga perezosa. Usa EquatableView para evitar redibujados innecesarios cuando los datos del elemento no han cambiado.
Para imágenes dentro de LazyVStack, usa siempre carga asíncrona (AsyncImage) o caché mediante Kingfisher/Nuke. Cada elemento no debe cargar una imagen sincrónicamente al aparecer en pantalla — esto causará tirones en el desplazamiento. Según WWDC Session 10031, el tamaño óptimo del búfer de prefetch es de 3–5 pantallas adelante y atrás de la posición actual.
Mide el rendimiento usando Xcode Instruments con el perfil SwiftUI. Presta atención a las métricas: evaluaciones del body, asignaciones y tasa de fotogramas (FPS). Valores objetivo: FPS > 55 al desplazarse, tiempo de renderizado por elemento < 1 ms.
Preguntas frecuentes
List proporciona capacidades integradas: edición por deslizamiento (swipeActions), eliminación mediante .onDelete, reordenación mediante .onMove, estilo agrupado .insetGrouped. LazyVStack es una herramienta de nivel más bajo sin soporte integrado para gestos de edición. List usa LazyVStack internamente pero añade el estilo de tabla nativo de iOS. Si necesitas un diseño de celda personalizado y no necesitas edición integrada — elige LazyVStack. Si necesitas swipeActions, .onDelete y trabajo con @FetchRequest — usa List.
Los stacks perezosos usan prefetching — SwiftUI crea elementos con un pequeño búfer de avance (prefetch buffer) para garantizar un desplazamiento suave. El tamaño del búfer se ajusta automáticamente a la velocidad de desplazamiento y al rendimiento del dispositivo. Según datos de perfilado de Apple, el búfer de prefetch suele ser de 1 a 3 pantallas en la dirección de desplazamiento. Si ves que se crean demasiados elementos invisibles, verifica si tienes identificadores generados cada vez o cálculos pesados en el inicializador de la vista.
Sí, pero con limitaciones. Anidar LazyVStack dentro de VStack no tiene sentido — el VStack externo creará todos los elementos del LazyVStack interno inmediatamente, anulando la carga perezosa. Anidar VStack dentro de LazyVStack está bien y no rompe el mecanismo lazy. Anidar LazyVStack dentro de otro LazyVStack es aceptable para secciones anidadas, pero vigila el rendimiento: cada nivel añade sobrecarga por el seguimiento de geometría.
SwiftUI no proporciona separadores integrados para LazyVStack. Añádelos manualmente: coloca Divider() después de cada elemento en ForEach, o usa el modificador .overlay(Divider(), alignment: .bottom) en cada elemento. Para separadores personalizados, dibuja Rectangle().frame(height: 1).foregroundColor(.gray.opacity(0.3)).
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.