Image by Susie Lu, take from the Addy Osmani's post
La diferencia entre una web mediocre y una web instantánea raramente está en el framework. Casi siempre está en entender la plataforma.
El listón real: el sitio aprueba CWV solo si el 75% de las visitas cumple los umbrales en el p75 del CrUX
Patrón "Facade": vista estática (imagen + botón fake). Cargar el módulo real al primer click.
// Antes: 580 KB de embed de YouTube en cada artículo
<YouTubeEmbed videoId="abc123" />
// Después: 2 KB de placeholder. 580 KB solo si hay click.
<LiteYouTubeEmbed videoId="abc123" />const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
import('./HeavyChart.js').then(({ render }) => render(entry.target));
observer.unobserve(entry.target);
}
});
}, { rootMargin: '200px' });
document.querySelectorAll('[data-chart]').forEach(el => observer.observe(el));El navegador ya hace loading="lazy" para imágenes, videos e iframes.
Esto es lo mismo para tu código.
.tab[hidden-view] {
content-visibility: hidden;
}El problema: `display: none` reflow al volver.
Destruir = perder estado = reconstruir
El navegador:
.heavy-section {
content-visibility: auto;
contain-intrinsic-size: auto 500px;
}content-visibility: auto, el navegador omite layout, estilo y pintura de elementos fuera de pantalla.
contain-intrinsic-size (OBLIGATORIO): define el tamaño estimado. Sin esto, el elemento colapsa a `0px` afecta a CLS al hacer scroll.
LIVE DEMO
.kanban-column {
contain: layout;
}Sin `contain: layout`: el navegador recalcula las 100 tarjetas, no puede garantizar que el cambio no afecte a las demás columnas.
Con `contain: layout`: recalcula solo las 20 tarjetas de esa columna. La promesa al navegador: nada dentro puede cambiar la geometría de nada fuera.
LIVE DEMO
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
// Vienes del bfcache — refresca solo lo que necesites
}
});Adelante/atrás = restauración en milisegundos.
No hay TTFB. No hay parse. No hay nada.
LIVE DEMO
¿Por qué la mayoría de SPAs lo rompen sin saberlo?
// SPA: cualquier cambio de estado animable
document.startViewTransition(() => updateDOM());
// MPA: transiciones cross-document
@view-transition { navigation: auto; }
```
```css
::view-transition-old(hero) { animation: slide-out 300ms; }
::view-transition-new(hero) { animation: slide-in 300ms; }
.product-image { view-transition-name: hero; }LIVE DEMO
::view-transition-old(hero) { animation: slide-out 300ms; }
::view-transition-new(hero) { animation: slide-in 300ms; }
.product-image { view-transition-name: hero; }LIVE DEMO
// SPA: cualquier cambio de estado animable
document.startViewTransition(() => updateDOM());
// MPA: transiciones cross-document
@view-transition { navigation: auto; }JS
CSS
El navegador toma un screenshot del estado actual, ejecuta el cambio, y anima la transición en una capa del compositor, fuera del hilo principal.
LIVE DEMO
<script type="speculationrules">
{
"prerender": [{
"source": "document",
"where": { "selector_matches": "a.product-card" },
"eagerness": "moderate"
}]
}
</script>Prefetch = descarga el HTML.
Prerender = ejecuta la página entera: parse, CSS, JS, fetch de datos.
<canvas layoutsubtree>
<article>Texto real, buscable, accesible</article>
</canvas>El futuro de los editores visuales en la web. Y de los juegos con UI accesible.
HTML
const ctx = canvas.getContext('2d');
ctx.drawElementImage(articleEl, x, y); //dibuja el DOM dentro del canvas
canvas.onpaint = () => redraw(); //redibuja cuando cambia el HTMLJS