Absortio

Email → Summary → Bookmark → Email

It’s time for modern CSS to kill the SPA

Extracto

Native CSS transitions have quietly killed the strongest argument for client-side routing. Yet people keep building terrible apps instead of performant websites.

Resumen

Resumen Principal

El artículo argumenta que la aparición de las transiciones CSS nativas, en particular la View Transitions API, ha eliminado el principal argumento a favor del enrutamiento del lado del cliente y las Single Page Applications (SPAs). Históricamente, las SPAs surgieron como la única forma de lograr una navegación fluida y sin interrupciones, pero a menudo fallan en su promesa de pulcritud, resultando en problemas como restauración de desplazamiento rota, cambios de diseño (layout shifts) y una carga desproporcionada de JavaScript. Esto llevó a la creación de aplicaciones pesadas que simulaban la velocidad en lugar de ofrecerla. La premisa de que "la navegación fluida requiere una aplicación" se considera ahora obsoleta, ya que las capacidades nativas del navegador, como la View Transitions API y las Speculation Rules, permiten construir experiencias ricas y fluidas con URLs reales y sin los hacks de enrutamiento basados en JavaScript, promoviendo así sitios web más performantes y robustos.

Elementos Clave

  • La Falacia de la "Sensación de Aplicación": La búsqueda de una "sensación de aplicación" llevó a la adopción de arquitecturas de SPA (comúnmente con React, Vue, desplegadas en Vercel/Netlify con un CMS headless y API GraphQL). Sin embargo, estas SPAs a menudo no cumplen con la promesa de una experiencia fluida, presentando problemas como transiciones que solo ocultan estados de carga, restauración de desplazamiento fallida, comportamiento inconsistente del foco, navegación retrasada por la rehidratación de componentes, y cambios de diseño significativos, todo ello con una carga de JavaScript desproporcionadamente alta.
  • La Obsolecencia de las SPAs gracias a las Características Nativas: La suposición de que "la navegación fluida requiere construir una aplicación" ha quedado obsoleta. Los navegadores modernos, especialmente los basados en Chromium, ahora soportan transiciones de página declarativas y nativas a través de APIs como View Transitions. Esto permite animar entre dos documentos, incluyendo navegaciones de página completa, con unas pocas líneas de CSS y sin la necesidad de JavaScript para la lógica de enrutamiento o hidratación.
  • La View Transitions API: Esta API es la pieza central de las "CSS modernas" mencionadas, permitiendo animaciones entre dos páginas o documentos completos. Con solo unas pocas líneas de CSS (@view-transition { navigation: auto; } y un @keyframes para la opacidad), se pueden lograr transiciones suaves de desvanecimiento. También facilita la animación de elementos compartidos, como miniaturas a imágenes de producto completas, asignando el mismo view-transition-name al elemento en ambas páginas, y el navegador se encarga de la animación de posición, escala, opacidad, etc.
  • Beneficios del Enfoque Declarativo y Nativo: El artículo subraya que "CSS moderno" (View Transitions, Speculation Rules y el retorno a funciones nativas del navegador) permite construir experiencias ricas y fluidas sin reescribir el navegador en JavaScript. Este enfoque declarativo es resiliente, expresivo, escalable y accesible, y permite fade entre páginas, animar elementos compartidos y mantener elementos persistentes como cabeceras, todo con URLs reales, cargas de página reales y sin los "hacks" de enrutamiento de JS.

Análisis e Implicaciones

Este análisis sugiere un cambio de paradigma en el desarrollo web, donde la prioridad se desplaza de complejas soluciones basadas en JavaScript hacia el aprovechamiento de las capacidades nativas del navegador. Esto implica la construcción de sitios web inherentemente más rápidos, accesibles y resilientes, al reducir la dependencia de frameworks JS pesados para la experiencia de usuario fundamental. La adopción generalizada de estas tecnologías podría simplificar significativamente las pilas tecnológicas y mejorar el rendimiento global de la web.

Contexto Adicional

El autor refuerza este argumento aludiendo a sus trabajos anteriores, donde critica cómo la obsesión por el desarrollo "JS-first" ha erosionado los fundamentos de la web y defiende la importancia del HTML semántico como base para el rendimiento, la mantenibilidad y la legibilidad automática.

Contenido

24th July, 2025

Native CSS transitions have quietly killed the strongest argument for client-side routing. Yet people keep building terrible apps instead of performant websites.

The app-like fallacy

Make it feel like an app.”

At some point during the scoping process, someone says the words. A CMO. A digital lead. A brand manager. And with that single phrase, the architecture is locked in: it’ll be an SPA. Probably React. Maybe Vue. Almost certainly deployed on Vercel or Netlify, bundled with a headless CMS and a GraphQL API for good measure.

But the decision wasn’t really about architecture. It wasn’t even about performance, scalability, or content management. It was about interactions. About how the site would feel when you click around. 

The assumption was simple: Seamless navigation requires us to build an app.

That assumption is now obsolete.

The false promise of SPAs

The reason SPAs became the default wasn’t because they were better. It was because, for a while, they were the only way to deliver something that felt fluid – something that didn’t flash white between pages or jank the scroll position.

But here’s the uncomfortable truth: most SPAs don’t actually deliver the polish they promise.

What you usually get is:

  • A page transition that looks smooth, until you realise it’s just fading between two loading states
  • Broken scroll restoration
  • Inconsistent focus behaviour
  • Delayed navigation while scripts rehydrate components
  • Layout shift, content popping, or full-page skeletons
  • A performance hit that’s entirely disproportionate to the effect

This isn’t theoretical. Look at most sites built with Next.js, Gatsby, or Nuxt. They’re shipping kilobytes (often megabytes) of JavaScript just to fake native navigation. Routing logic, hydration code, loading spinners – all just to stitch together something that browsers already knew how to do natively.

Instead of smoothness, you get simulation. And instead of a fast, stable, SEO-friendly experience, you get a heavy JavaScript machine trying to recreate the native behaviour we threw away.

We’ve been adding mountains of JS to “feel” fast, while making everything slower.

An aside – I went deeper on this in JavaScript broke the web, where I outlined how our obsession with JS-first development is actively eroding the web’s foundations.

The web grew up

While we were busy reinventing navigation in JavaScript, the platform quietly solved the problem.

Modern browsers – specifically Chromium-based ones like Chrome and Edge – now support native, declarative page transitions. With the View Transitions API, you can animate between two documents – including full page navigations – without needing a single line of JavaScript.

Yes, really.

What we’re calling “modern CSS” here is shorthand for View Transitions, Speculation Rules, and a return to native browser features that were always designed to handle navigation, interaction, and layout. These capabilities let us build rich, seamless experiences – without rewriting the browser in JavaScript.

An aside – CSS is also declarative, resilient, expressive, scalable, and increasingly intuitive. It’s accessible to anyone who can write plain HTML. And that structural clarity reinforces everything I argued in Why semantic HTML still matters – that clean, meaningful markup is the bedrock of performance, maintainability, and machine readability.

That means you can:

  • Fade between pages
  • Animate shared elements (e.g. thumbnails → product detail)
  • Maintain persistent elements like headers or navbars
  • Do it all with real URLs, real page loads, and no JS routing hacks

Let’s make this concrete.

🔄 Basic cross-page fade transition

With just a few lines of CSS, you can trigger smooth visual transitions between pages.

On both the current and destination page, add:

@view-transition {
  navigation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation: fade 0.3s ease both;
}

@keyframes fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

That’s it. The browser handles the transition – no client-side routing, no hydration, no loading spinners.

🔁 Shared element transitions

Want to animate a thumbnail image into its full-size product counterpart on the next page?

No JavaScript needed – just assign the same view-transition-name to the element on both pages:

On the product listing page:

<a href="/product/red-shoes">
  <img src="/images/red-shoes-thumb.jpg" style="view-transition-name: product-image;" />
</a>

On the product detail page:

<img src="/images/red-shoes-large.jpg" style="view-transition-name: product-image;" />

The browser matches and animates the elements between navigations. You can animate position, scale, opacity, layout – all with CSS.

🤖 But what if I need JS-driven transitions?

You can manually trigger transitions inside a page too:

document.startViewTransition(() => {
  document.body.classList.toggle('dark-mode');
});

Perfect for things like tab toggles or theme switches — without needing a framework or hydration layer.

🔮 Speculation rules: instant navigation without JS

View Transitions make things smooth. But what about fast?

That’s where Speculation Rules come in. This lets the browser preload or prerender full pages based on user behaviour – like hovering or touching a link – before they click.

<script type="speculationrules">
{
  "prerender": [
    {
      "where": {
        "selector_matches": "a"
      }
    }
  ]
}
</script>

The result? Navigation that’s instant. No waiting. No loading. No spinners.

⚠️ A Note of Caution

Speculation Rules are a performance multiplier. On a lean site, they make things feel instant. But if your pages are slow, bloated, or JS-heavy, speculation just front-loads those costs.

If your site is bloated, speculation will still speculate – and the user pays the price.

That means wasted CPU, network bandwidth, and mobile battery – often for pages the user never even visits.

Use them carefully. On a fast site, they’re magic. On a slow one, they’re a trap.

Browsers want to help – if we let them

Modern browsers are smarter than ever. They’re constantly looking for ways to improve speed, responsiveness, and efficiency – but only if we let them.

One of the clearest examples is the Back/Forward Cache (bfcache), which allows entire pages to be snapshotted and restored instantly when users navigate back or forward.

It’s effectively free performance – but only for pages that behave. That means no rogue JavaScript, no intercepted navigation, no lifecycle chaos. Just clean, declarative architecture. Just HTML and CSS.

Unsurprisingly, this plays beautifully with a well-structured, multi-page site. But for most SPAs, it’s a non-starter. The very design patterns that define them – hijacked routing, client-side rendering, complex state management – break the assumptions that bfcache relies on.

This is a microcosm of a much bigger theme: browsers are evolving to reward simplicity and resilience. They’re building for the kind of web we should have been embracing all along. And SPAs are increasingly the odd ones out.

📊 SPA vs MPA: a performance reality check

Average Next.js marketing site

  • JS bundle: 1 – 3MB
  • TTI: ~3.5 – 5s (depending on hydration strategy)
  • Route transitions: simulated
  • SEO: complex, fragile
  • Scroll/anchor behaviour: unreliable

Modern MPA + View Transitions + Speculation Rules

  • JS bundle: 0KB (optional enhancements only)
  • TTI: ~1s
  • Route transitions: real, native
  • SEO: trivial
  • Scroll/focus/history: browser-default and perfect

Modern CSS doesn’t just replace SPA behaviour – it outperforms it.

Don’t build a website like it’s an app

Most websites aren’t apps.

They don’t need shared state. They don’t need client-side routing. They don’t need interactive components on every screen. But somewhere along the way, we stopped making the distinction.

Now we’re building ecommerce stores, documentation portals, marketing sites, and blogs using stacks designed for real-time collaborative UIs. It’s madness.

A homepage with six content blocks and a contact form doesn’t need hydration, suspense boundaries, and a rendering strategy.

It needs fast markup, clean URLs, and maybe – maybe – a bit of interactivity layered on top.

And yet, on every project:

  1. A stakeholder says, “make it feel like an app.”
  2. A dev team reaches for Next.js or Nuxt.
  3. Routing goes client-side.
  4. Performance falls off a cliff.
  5. Now you need edge functions, streaming, ISR, loading strategies, and a debugging plan.
  6. And somehow… it still feels slower than a regular link click and a CSS animation.

This isn’t about being anti-framework. It’s about being intentional.

Use React if you want. Use Tailwind, Vite, whatever. Just don’t ship it all to the browser unless you need to.

Build a site like a site. Use HTML. Use navigation. Use the platform.

It’s faster, simpler, and better for everyone.

Build for the web we have

SPAs were a clever solution to a temporary limitation. But that limitation no longer exists.

We now have:

  • Native, declarative transitions between real pages
  • Instantaneous prerendered navigation via Speculation Rules
  • Graceful degradation
  • Clean markup, fast loads, and real URLs
  • A platform that wants to help – if we let it

If you’re still building your site as an SPA for the sake of “smoothness,” you’re solving a problem the browser already fixed – and you’re paying for it in complexity, performance, and maintainability.

Use modern server rendering. Use actual pages. Animate with CSS. Preload with intent. Ship less JavaScript.

Build like it’s 2025 – not like you’re trapped in a 2018 demo of Gatsby.

You’ll end up with faster sites, happier users, and fewer regrets.

Fuente: Jono Alderson