2024-11-19Turbo considered harmful

Every techie dreams of one day writing a considered harmful post and today, my friends, today that honor is finally bestowed on me. Gather round the fire, and I'll tell you the blood-curdling tale of a library once known as PJAX, whose restless ghost still haunts these realms.

Now's the time on Sprockets when we dance

The Rails frontend is a hot mess at the moment. Rails 8 was just released, with a big focus on deployment and infrastructure. The SQLite and single-server deployment parts are exciting, but if I were BDFL (replacing the ODFL, or obnoxious dictator we have now) I'd instead start laying down the law when it comes to handling frontend code—we're frustratingly close to something great, but it's currently being scuppered by some questionable choices in the Hotwire stack. I'm going to leave the importmaps/bundling/package-manager parts out of this, because that's a hellscape all on its own, orchestrated by the incorrigible JS community: we can only do so much if we want access to their teetering tower of transpiled trouble. Sometimes we do.

No, my beef today is not with the JS-industrial complex, it's with Turbo. Turbo has outlived its usefulness. It did some good (probably) and spawned some cool ideas, but it's way past time to give it a few tender pats on the head and then take it 'round back. No, wait, put it to pasture? Whatever the euphemism is for shooting it between the eyes and then lighting a cigar.

The principle of least surprise

The driving force behind Turbo is to speed up your interactions with a page, and take some common JS patterns off your hands. I'm using "page" here deliberately—I'm still very much of the opinion that most Web Apps should be Web Pages and interactions should be progressively enhanced, dammit. Turbo slots perfectly into that idea the same way a bull slots perfectly into a china shop. If you're going to use Turbo you really need to understand what it's doing and why, or you're going to be constantly surprised, and surprised is not a good state for a developer to be in. There's a lot to take in. Say goodbye to DOMContentLoaded. Remember which tags you've used data-turbo-eval on. Don't put your script tags at the end of the document. Your third-party analytics probably won't Just Work™. Elements morph now if they have an id, which can turn out to be all kinds of interesting! Oh, and did I tell you Turbo has its own cache layer lurking around behind the scenes now, ready to ruin your weekend?

I'll concede that once you've nudged your tower of Turbo Jenga into a position where it chugs along, it mostly stays out of your way. But you're never really comfortable. So many browser overrides and concepts and gotchas, and... for what? Marginally faster page loads? Come on. Turbo is too invasive!

The puck's not there anymore

Turbo's original raison d'être was to make server-rendered markup competitive with the cool optimistic-updates SPA crowd. We didn't know any better, plus browsers didn't have a bfcache back then which made the process of tearing down the old page and building up the new one a bit jarring in comparison. But that was long ago! As long as you keep your view rendering below 100ms these days, you're golden. And it seems like Turbo is still skating to where the puck used to be: page preloading à la instant.page was added as late as this year, as was page morphing. Why on earth would you do that? There are things afoot in browser land that will completely obviate these efforts soon. I feel like Turbo, at this point, should focus on what it actually needs to do. And what it needs to do is A LOT LESS THAN IT DOES TODAY. As proof, your honor, I submit to you the total JS weight of a completely fresh Turbo+Stimulus install in Rails 8: 150kB, minified.

And that's without a single controller in it. Does absolutely bupkiss. Doesn't even print a jaunty "Hello, world!"

For comparison, Lit is 15.5kB minified. HTMX is 52kB, and perennial harbinger of obese websites React weighs in at a practically emaciated 109kB. Good lord. If you're losing the page-size fight to frickin' React, you're not in great shape.

The Good Parts

So if we start from scratch, is there anything we should keep? I still think Frames and Streams are excellent ideas. Turbo Frames could work pretty much as-is: build them as Web Components that hijack links and possibly form submissions. Streams can also work basically the same as today but they should be opt-in: slap a data-turbo-stream attribute on your link, form, or submit button and let Turbo hook in to it, rather than have it done automagically. I'm also a fan of the data-turbo-confirm type stuff you can put on links, so that can stay too. But BEGONE with the pre-emptive fetches, the pushState stuff, the local page cache, the awkward attempts at preserving scroll position, the page morphing, just... the wild, untamed complexity of it all!

Working with Hotwire is nice, it really is: there are layers to it that go from simple to complex that Matt Swanson wrote about. You can often get away with something as simple as a Turbo Frame, and if that's not enough you drop down to a Stream, and if that still doesn't do the trick you enhance it with a Stimulus controller. There's so much JS I just haven't written because I could simply lay down a Frame and call it a day. That's worth preserving. But I wish Turbo could be radically simplified, and if I were starting a fresh Rails app today I'd probably go with something like Alpine.js or HTMX instead. Wait, who am I kidding, I would totally code up the components I wrote about in the previous paragraph and then half-heartedly release them as open source. They wouldn't do as much, but that would be kind of the point. Hotwire is incredibly nice, but Turbo is holding it back. Let's Consider it Harmful and build something cooler!