Skip to content
← All posts
5 min read Dawid Skłodowski

Server-rendered SPAs with Turbolinks 5

You can get most of the feel of a single-page app without building one. A deep-dive on Turbolinks 5 plus Stimulus — fast navigation and dynamic interactivity over server-rendered HTML — and why this 'HTML over the wire' approach is quietly the right default for many apps.

There is a false dichotomy in front-end choices: either a traditional multi-page app with full-page reloads on every click, or a full single-page application in React with all the machinery that entails. Turbolinks 5 — significantly rewritten from earlier versions — stakes out a third position, and combined with Stimulus it gives you much of the feel of a SPA while remaining a server-rendered Rails app. This “HTML over the wire” approach is quietly the right default for a large class of apps, and it is worth understanding properly.

Turbolinks intercepts link clicks and form submissions, fetches the new page over the network in the background, and swaps the <body> — replacing the page content without a full browser reload. Because the browser never tears down and rebuilds the JavaScript runtime, never re-parses your CSS and JS, navigation feels near-instant. Add its caching, and the experience gets even snappier:

<!-- Turbolinks is largely transparent — links just work, faster -->
<a href="/products/42">View product</a>

The version-5 rewrite made it more robust than the original (which we discussed warily back in the Rails 4 days), and crucially it ships a real cache and progress bar: Turbolinks keeps a cache of recently-visited pages and shows a cached version instantly on navigation while fetching the fresh one in the background, then swaps it in. Back/forward navigation is immediate. The result is that a plain server-rendered app navigates with the responsiveness people associate with SPAs — for free, with no client-side router, no API, no client state to manage.

The catch, and how Stimulus resolves it

The historic objection to Turbolinks was exactly the one we raised years ago: because the page never fully reloads, $(document).ready fires once and your JavaScript stops re-initialising on navigation. That objection is now answered, and the answer is Stimulus. Stimulus controllers attach themselves automatically to matching markup whenever it appears — including the new <body> Turbolinks swaps in — and tear themselves down when it leaves. The two are designed to work together:

<div data-controller="dropdown">
  <button data-action="dropdown#toggle">Menu</button>
  <div data-dropdown-target="menu" hidden>...</div>
</div>

When Turbolinks swaps in a page containing this markup, Stimulus instantiates the dropdown controller automatically; when you navigate away, it disconnects it. The “my handlers died after the first page” problem that made early Turbolinks painful simply does not exist with Stimulus, because Stimulus’s whole model is “behaviour attached to markup, wherever and whenever that markup appears.” Together they are the complete package: Turbolinks for fast navigation, Stimulus for interactivity — both over server-rendered HTML.

The architecture, and why it is appealing

Step back and look at what this gives you compared to a SPA:

  • The server renders HTML; there is no API to build. Your Rails controllers and views produce HTML as they always did. You are not maintaining a separate JSON API and a client application that consumes it — you have one application.
  • No client-side state management. The DOM and the server are the state. There is no Redux store to keep in sync, no client/server duplication of logic, no cache invalidation between two representations of the same data.
  • SEO and first paint are free. Server-rendered HTML is fully crawlable and paints fast on first load — none of the SSR gymnastics a React SPA needs to be SEO-friendly.
  • You use the framework you know. It is just Rails, with two small JavaScript libraries layered on. The whole team can work in it without learning a separate front-end stack.

For the large class of apps that are mostly server-rendered content and forms with a need for fast navigation and some interactivity — which is most CRUD and commerce apps — this is a genuinely better trade than a SPA: you get most of the responsiveness for a fraction of the complexity.

Where it stops

Honesty, as always. This approach gives you SPA-like navigation and interactivity, but it is not a SPA, and there are things it does not do. Truly app-like experiences with complex, long-lived client state that does not map to the DOM — a collaborative editor, a drawing tool, a live dashboard with many independently-updating widgets — still want a real client framework. And while Turbolinks + Stimulus handles navigation and discrete interactions beautifully, partial page updates (replacing just one fragment in response to an action, without a full navigation) are still a bit manual in 2019 — you reach for an Ajax request and some Stimulus to swap content. This is precisely the gap that the next evolution of this idea is moving to close.

Where this is heading

It is worth seeing Turbolinks + Stimulus as a direction, not an endpoint. The “HTML over the wire” philosophy — server renders HTML, the wire carries HTML (not JSON), JavaScript stays modest — is gathering real momentum as a deliberate alternative to the SPA-by-default assumption. The obvious next step is making partial updates as easy as full-page navigation already is — sending HTML fragments over the wire to update pieces of a page, and pushing them from the server in real time. That evolution is clearly coming, and learning to build this way now — server-rendered HTML, Turbolinks for navigation, Stimulus for behaviour — positions you directly for it.

Verdict

Turbolinks 5 plus Stimulus dissolves the false choice between a clunky multi-page app and a complex SPA. You get fast, cached navigation and clean, resilient interactivity over plain server-rendered HTML — no API, no client state store, no SEO gymnastics, no separate front-end application — using the Rails you already know. It is not for every app: genuinely application-like front ends with rich client state still want React. But for the large middle — content-and-forms apps that want to feel fast and modern — it is a better default than the industry’s SPA-by-reflex, and it sits right on the path the framework is heading down. Build this way, and you get most of the SPA experience for a small fraction of the cost.