2023-02-17What to expect from your framework
There's been a bit of a brouhaha these last weeks as parts of the Internet suddenly woke up to the realization that most Single Page Apps are slow and overcomplicated, and — shock horror — it turns out that Facebook, the lovable scamps that brought us React and QAnon, have conducted themselves badly! Refreshingly, rather than launching a thousand caustic subtweets, this little stormwind has caused the blog posts to start flying again. Thank you, Elon "Vain gobshite" Musk. I'm totally here for it.
For a bit of backstory, the best place to start is probably Zach Leatherman's History of React criticism followed by Alex Russell's long and eloquent assault on SPA stacks. You can easily lose an entire afternoon reading the various pieces linked from those (a fun afternoon! so much tech drama!) but what got my delicious brainmeat reaching for the blog this time was the most recent shot, fired over at Laurie Voss' site. It's called "The case for frameworks" and you should go read that, too.
All caught up? Great.
Bro, do you even framework
So half of this blog post has actually been sitting on my drive for a couple of months because I've had a good long sad about the distressingly low bar we've set for something to bill itself as a Framework. I suppose this is partly because we've never really agreed about what constitutes one, and spoiler alert I don't have a proper working definition either. But let's get this out of the way first:
React is not a framework.
Search your feelings, you know it to be true! Another thing that isn't a framework is Vue. And Svelte. And Solid. I could go on. I know, I know: look a little closer at their marketing and you'll find that most of them brand themselves as Libraries, which is quite separate from branding yourself as a Framework. And that would be absolutely fine, except the industry clearly hasn't grasped the distinction and it certainly isn't helped by the gushing brochure sites! They promise "efficient, whether simple or complex" and "small composable pieces that serve as building blocks for applications" while hand-waving everything from data access to CSP to cookie protection to worker instances to form handling with, in essence, "draw the rest of the fucking owl." I can't believe how much time is spent arguing about which view layer is the blazingest fastest or which one has the best DX when we're missing, what, 98% of what constitutes a web application? Who cares about hooks!!
But people already know that these are libraries
Do they? I mean do they really? Look at where we are in 2023: regular websites are rife with things like *grey placeholders on first load* which, if this was a fair and just world, would be an immediately fireable offense that left your tech career scorched and plowed into the earth with a humorous little blank "loading" tombstone, and then that earth would be liberally salted and danced upon.
But people have implemented these atrocities on many years of bad advice. The React documentation, for example, has told people for the longest time that the best way to get started is to use "create-react-app" which is, and let's be frank here, terrible advice. The first paragraph even tells you in a chipper tone that You Might Not Need a Toolchain but can actually add React as a script
tag on your site which, impressively, is even worse advice. If you take any of this at face value, I assure you, even if you're a seasoned developer with many years of JavaScript experience, you will faceplant directly and hilariously into the trough of disillusionment.
Not only that: you'll follow the insidious trajectory of "wow, this feels great, so fast and ergonomic" to "oof, this edge case was hard" to "we're sure missing a lot of deadlines" to "wow, turns out this needed an entire Silicon Valley VC-funded organization behind it to work and now we're bankrupt" which has the unfortunate side effect of imprisoning you behind a sturdy lock-in within the first few months. How do I know this? Uhhh, a friend that worked at, uhh, next question please
Next.js though
React and its brethren have never lived in a vacuum. It didn't take long for various developer communities to realize that a fun and componentized view layer does not an application make. So after the first tentative attempts at "isomorphic" JS we've ended up with some decent stabs at something that can scale from your standard Blazing Fast™ TodoMVC demo to something that end-users can actually consume. We have Next.js, Nuxt.js, SvelteKit, Blitz, etc. If you're convinced that a stateful client-side view layer is important for your site, you can absolutely ship something good with one of these.
But from where I sit? Most of them are still barely frameworks. The frontend component ecosystem is a very bring-your-own-ideas, have-fun-reimplementing-CSRF kind of party. And if you muck any of these wheel-reinventions up or hold the tool facing the wrong way, your Blazing Fast™ goes out the window along with your stakeholders' confidence. And regrettably, even after putting all that work in, you're likely staring down the barrel of multi-second Time To Interactive rehydration hijinks because... well, those sub 50ms benchmarks never took a lot into account beyond view rendering, and it turns out real apps are messy and weird? Oh, and there's always the wholly separate JSON pump living somewhere that feeds your app. You gotta build and maintain that too, and make it spit out correct data very quickly. They gloss that over in the pamphlets.
It's all SO MUCH WORK, and why are we doing this again? Optimistic commits? Fewer redraws on state change? Avoiding jQuery soup? I mean I agree that these are laudable goals: we probably don't want to devolve back to an imperative programming hairball, but man, I wish there was a better way.
So far: many complaints, few solutions
I'm sure most of you already know what I'm going to say next. But look at the hilarity that ensues in my shell when I run rails middleware
:
use ActionDispatch::HostAuthorization
use ActionDispatch::SSL
use Rack::Sendfile
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use RequestStore::Middleware
use ActionDispatch::RemoteIp
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use Appsignal::Rack::RailsInstrumentation
use ActionDispatch::Callbacks
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use ActionDispatch::PermissionsPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
use Warden::Manager
use OmniAuth::Strategies::Facebook
use OmniAuth::Strategies::GoogleOauth2
run Highway::Application.routes
I think there might be even more of them in production
First, let's all just pause for a second and admire the fact that I can type rails middleware
in my console and get a list of my app's installed middleware. Rails has so many of these tricks up its little sleeves, which is big part of what makes these mature frameworks so appealing. Anyway, I copied all that output to make a point — this is a very standard Rails 7 app, and oh man: it looks like a lot. People will often slap you with the classic BLOAT!! and YAGNI!! and KITCHEN SINK!! complaints when you talk about Rails, Phoenix, Django, and friends. But you know what every line in that middleware stack does?
Something important.
They all do something important. These are things that will all have to get done eventually, and for each and every one you really only have three choices:
- Write the solution yourself
- Pull in a dependency and glue it onto your app
- You don't have to do squat, your framework already has your back
For some of it I'm sure you could make a good argument for 2) and in some rare cases you gotta do it yourself. But come on: we're living in the 2020s and if you're not at like 95% "framework does it for you" for things that aren't business logic, then you've lashed yourself to a very bad mast ‒ you're stuck building rote infrastructure instead of working on your app, likely creating a bunch of nasty attack surfaces along the way. And what are you getting in return? The warm and fuzzy feeling of deciding precisely how your session cookie is stored and encrypted?
There's always a sense of invincibility when greenfielding a project. We'll keep it lean and mean, we tell ourselves. We'll stay nimble. And look at the performance numbers of this first iteration! It's a very common story behind the decision to just strike it out on your own with nothing but a view layer and npm install
. But let me borrow Laurie's chart for a second:
I can prove this on an abacus
I completely agree with his little graph but where I think we differ is that Laurie applies this to "using React instead of something hand-rolled", whereas I would argue that React in itself gets you basically nowhere. It's a templating language with benefits. Once you start hitting the cursed parts of your application, the places full of dragons and cobwebs and sweary code comments, React won't be there to hold your hand. It'll shrug and say "add a Provider or something, idk." That's not enough. You need more. A lot more! But at this point you've already committed your entire over-the-wire JS budget to rendering and routing, basically. Are you absolutely sure you want to put all of this in the client? I don't think code splitting is going to save you, not if those 3MB mega-pints of JS I get slammed with on a daily basis are any indication. (Side note: please stop. I work for a tiny startup and can't afford your ad-monopoly top-of-the-line M2 laptops.)
The warm and friendly server
We can at least rejoice in the fact that the pendulum has, at last, swung firmly back into Server-Side Rendering territory. Now everyone seems to be more-or-less in agreement that it's better to send users a real HTML and CSS page containing the thing you wanted to show rather than feeding the browser a big fat JS payload to choke on before it can think about fetching and rendering data. Great! It took too long, but at least we're back where we started. The question that remains, though, is "why are we so intent on piping our server code through the uncanny valley of a client-side frontend system?" It feels like putting the horse before the cart.
I enjoy working with components as much as the next guy, but practically every mature server framework has support for a component-based workflow today. And you know what? They'll also handle however much mundane busywork you want to throw at them: things like input validation, SQL sanitization, database access, e-mails, authorization, timezones and dates, asset bundling, job running, the list just goes on and on and on. It will also do all this in a place where you have full control and observability. Remember that a web browser is just about the most hostile code environment you can think of! You can't trust anything. I believe the dream of writing code that runs equally well on server and client is a mirage, even if framework authors can wrangle it to work without too many special-case contortions or completely torpedoing your TTI. I wrote about this way back in 2015 which makes me look a lot smarter than I actually am.
Don't go it alone, but don't get swept up
Wrapping up, I feel like the single-minded focus on stateful frontends over the last decade has left a lot of the dev community fairly stunted when it comes to knowing what to expect from an application framework. It's all been a merry cavalcade of people insisting that their solution, fresh off npm, is the panacea for all your worries while at the same time being exempt from basic hygiene like progressive enhancement because "app, not page" or "zero-interest phenomenon" or something? Swimming against this current has been exhausting, so I really hope we're in for some kind of correction. We've generally gotten back to the basic idea of rendering on the server and then handing off to something else, which hopefully ends up being a meaningful shift and then we can keep chipping away at that "something else." Isolated components were a great idea ‒ they still are ‒ but starting from React and then slowly working backwards towards the server through some fragile jumble of transforms has cost a lot of time and effort I wish would have been spent elsewhere.
I don't know what the best solution is, or what it will look like. I hope we get many of them. You can obviously build great stuff using React and Typescript and WASM and whatever your depraved little heart desires. We've all seen it done, and I've written (and sometimes totally enjoyed writing) applications and parts of applications using reactive frontend libraries. But in the absolute majority of cases you'll be better off with something incredibly boring. Something that renders on the server, takes care of all the everyday stuff, is easily debuggable and testable, something that just... you know. Concatenates text and fires it off to the browser in a straightforward and predictable way. It might feel retro or oldtimey and maybe not as fun while you're building, but your application will be better for it.