The first framework I chose was actually Angular 1. I built a decent chunk of the app, with a FuelPHP backend, before I ran into some issues with the community router - it would flicker whenever you rerendered subroutes/outlets, and really it just didn’t feel like it had been designed with that use case in mind. Someone recommended Ruby on Rails + Ember to me, and after giving it a shot I decided it worked pretty well. I liked the philosophy of both frameworks, liked the communities, and overall it was very productive compared to the alternatives at the time.
- The Before Times
- The First Frameworks
- Component-Centric View Layers
- Full-stack Frameworks (← We’re here)
Each era had its own main themes and central conflicts, and in each one we learned key lessons as a community and advanced, slowly but surely.
That said, I can’t write about what I didn’t experience. By the time I started writing frontend apps, there was a new generation of frameworks that had just started to reach maturity: Angular.js, Ember.js, Backbone, and more.
In this environment, it’s understandable that JS was generally seen as a toy language and not something you’d write a full app in. It was most commonly used for small, isolated UI widgets, adding a bit of flair to otherwise static sites. As time went on and XHR was introduced and popularized, people started to put parts of their UI flow into a single page, especially for complex flows that required multiple back and forth interactions between the client and the server, but the majority of the app stayed firmly on the server.
This contrasted pretty significantly with mobile apps when they started to hit the scene. From the get go, mobile apps on iOS and Android were full applications written in Serious Languages™ like Objective C and Java. Moreover, they were fully API driven - all of the UI logic lived on the device, and communication with the server was purely in data formats. This resulted a much better UX and mobile apps exploded in popularity, leading quite directly to where we are today with debates about which is better, mobile or the web.
Around the late 2000’s and early 2010’s the first JS frameworks specifically designed for writing full client applications started to come out. A few of the notable frameworks of this era were:
On the other hand, we had no experience building full apps in JS, collectively, and so there were tons of competing ideas about the best ways to do it. Most frameworks tried to mimic was was popular on other platforms, so almost all of them ended up being some iteration of Model-View-*: Model-View-Controller, Model-View-Producer, Model-View-ViewModel, etc. None of these really ended up working out though in the long run - they weren’t particularly intuitive and they got really complicated really quickly.
We learned a lot of things from this era, however; important fundamental lessons, including:
- URL-based routing is fundamental. Apps that don’t have it break the web, and it needs to be thought about from the beginning in a framework.
- Extending HTML via templating languages is a powerful abstraction layer. Even if it can be at times a bit clunky, it makes keeping your UI in sync with your state much easier.
- Performance for SPAs was hard, and the web has a lot of extra constraints that native apps do not. We need to ship all of our code over the wire, have it JIT, and then run just to get our apps started, whereas native apps are already downloaded and compiled. That was a massive undertaking.
- We absolutely needed better build tools, modules, and packaging in order to write apps at scale.
Overall, this era was fruitful. Despite the shortcomings, the benefits of separating clients from APIs were massive as apps grew in complexity, and in many cases the resulting UX was phenomenal. If things were different, this era may have continued on and we would still be iterating on MV* style ideas to this day.
But then an asteroid came out of nowhere, smashing the existing paradigms apart and causing a minor extinction event that propelled us into the next era - an asteroid named React.
I don’t think React invented components, but to be honest I’m not quite sure where they first came from. I know there’s prior art going back to at least XAML in .NET, and web components were also beginning to develop as a spec around then. Ultimately it doesn’t really matter - once the idea was out there, every major framework adopted it pretty quickly.
It made complete sense in hindsight - extend HTML, reduce long-lived state, tie the JS business logic directly to the template (be that JSX or Handlebars or Directives). Component-based applications removed most of the abstractions necessary to get things done, and also remarkably simplified the lifecycle of code - everything was tied to the lifecycle of the component instead of the app, and that meant you had much less to think about as a developer.
However, there was another shift at the time: frameworks started touting themselves as “view-layers” instead of full-fledged frameworks. Instead of solving all of the problems needed for a frontend app, they would focus on just solving rendering problems. Other concerns, like routing, API communication, and state management, were left up to the user. Notable frameworks from this era include:
And many, many others. Looking back now, I think that this was a popular framing for this second generation of frameworks because it did do two main things:
- It reduced scope dramatically. Rather than trying to solve all these problems up front, the core of the framework focused on rendering, and many different ideas and directions could be explored in the wider ecosystem for other functionality. There were plenty of terrible solutions, but there were also good ones, paving the way for the next generation to pick the best ideas from the cream of the crop.
- It made it much easier to adopt them. Adopting a full framework that took over your entire web page pretty much meant rewriting most your app, which was a non-starter with existing server-side monoliths. With frameworks like React and Vue, you could drop a little bit of them into an existing app one widget or component at a time, allowing developers to incrementally migrate their existing code.
These two factors led to second-gen frameworks growing rapidly and eclipsing the first-gen ones, and from a distance it all seems to make a lot of sense and is a logical evolution. But being in the midst of it was quite a frustrating experience at the time.
The reality, however, was that there are no silver bullets - there never are. Apps still grew enormous and bloated and complicated, state was still hard to manage, and fundamental problems like routing and SSR still needed to be solved. And for a lot of us, it seemed like what people wanted was to ditch the solution that was trying to solve all of those problems for one that left that exercise up to the reader. In my experience, this was also universally within engineering orgs which would gladly accept this change in order to ship a new product or feature, and then fail to fund the time needed to fully develop all of that extra functionality.
The result (in my experience, more often than not) was homegrown frameworks built around these view layers that were themselves bloated, complicated, and very difficult to work with. Many of the problems that people have with SPAs I think come from this fragmented ecosystem, which came right at the time when SPA usage was exploding. I still often come across a new site that fails to properly do routing or handle other small details well, and it definitely is frustrating.
- Transpilers like Babel became the norm, and helped to modernize the language. Rather than having to wait years for features to standardize, they could be used today, and the language itself started adding features at a much faster and more iterative pace.
- ES Modules were standardized and allowed us to finally start building modern build tools like Rollup, Webpack, and Parcel around them. Import based bundling slowly became the norm, even for non-JS assets like styles and images, which dramatically simplified configuration for build tools and allowed them to become leaner, faster, and overall better.
- The gap between Node and web standards closed slowly but surely as more and more APIs were standardized. SSR started to become a real possibility, and then something every serious app was doing, but it was still a somewhat bespoke setup each time.
By the end of this era, some problems still remained. State management and reactivity were (and are) still thorny problems, even though we had much better patterns than before. Performance was still a difficult problem, and even though the situation was improving, there were still many, many bloated SPAs out there. And the accessibility situation had improved, but it was (and is) still oftentimes an afterthought for many engineering orgs. But these changes paved the way for the next generation of frameworks, which I would say we are entering just now.
This last era of frameworks has really snuck up on me, personally. I think that’s because I’ve spent the last 4 years or so deep in the internals of Ember’s rendering layer, trying to clean up the aformentioned tech-debt that’s (still) affecting it as a first-gen framework. But it’s also because it was much more subtle, as all of these third-gen frameworks are built around the view-layer frameworks of the previous generation. Notable entries include:
These frameworks started up as the view-layers matured and solidified. Now that we all agreed that components were the core primitive to build on top of, it made sense to start standardizing other parts of the app - the router, the build system, the folder structure, etc. Slowly but surely, these meta-frameworks started to build up the same functionality that the all-in-one solutions of the first generation offered out of the box, picking the best patterns from their respective ecosystems and incorporating them as they matured.
And then they went a step further.
Up until this point, SPAs had been exclusively focused on the client. SSR was something every framework aspired to solve, but only as an optimization, a way to get something rendered that would ultimately be replaced once the megabytes of JS had finally loaded. Only one first-gen framework dared to think bigger, Meteor.js, but its idea of “isomorphic JS” never really took off.
But that idea was revisited as apps grew in size and complexity. We noticed that it was actually really useful to have a backend and frontend paired together, so that you could do things like hide API secrets for certain requests, modify headers when a page was returned, proxy API requests. And with Node and Deno implementing more and more web standards, with the gap between server-side JS and client-side JS shrinking every year, it started to seem like it wasn’t such a crazy idea after all. Combine this with edge-computing and amazing tooling to go with it, and you have some incredible potential.
This latest generation of frameworks makes full use of that potential, melding the client and the server together seamlessly, and I cannot stress enough how amazing this feels. I have, in the past 9 months of working with SvelteKit, sat back more times than I can count and said to myself “this is the way we should have always done it.”
Here are just a few of the tasks I’ve had recently that were made incredibly easy by this setup:
- Adding server-side OAuth to our applications so that auth tokens never leave the server, along with an API proxy that adds the tokens whenever a request is sent to our API.
- Proxying certain routes directly to our CDN so we can host static HTML pages built in any other framework, allowing users to make their own custom pages (a service we provide for some of our clients).
- Adding several different one-off API routes when we needed to use an external service that required a secret key (no need to add a whole new route to our APIs and coordinate with the backend folks).
- Moving our usage of LaunchDarkly server-side so that we can load less JS and incur lower costs overall.
- Proxying our Sentry requests through a backend route so we can catch errors that would otherwise go unreported due to ad-blockers.
These are the new features that, experientially, have me classifying these frameworks as a new generation. Problems that previously were either difficult or impossible to solve are now trivial, just a change to a little bit of response handling logic. Solid performance and UX is available out of the box, without any extra config needed. Instead of standing up entire new services, we’re able to add a few extra endpoints or middlewares as needed. It has been life changing.
I think that this generation has also addressed some of the main tension points between the first-gen and second-gen frameworks and their users. It started with the shift to zero-config terminology, but I think ultimately it was driven by the ecosystems around the second-gen frameworks maturing and stabilizing, and it’s been a cultural shift. Third-gen frameworks are now trying to be all-in-one solutions again, trying to solve all of the fundamental problems that we need to solve as frontend devs - not just rendering.
Now more than ever it feels like the community is aligned in solving all of the many problems that have plagued SPAs, and importantly, solving them together.
I’m also still excited about the potential behind bringing these patterns even further up, into the web platform itself. Web components are still quietly iterating, working on solutions to issues like SSR and getting rid of global registration, which would make them more compatible with these third-gen frameworks. In the other direction, WebAssembly could iterate on this model in an incredible way. Imagine being able to write a full-stack framework in any language. Isomorphic Rust, Python, Swift, Java, etc. could finally reduce the barrier between frontends and backends to almost zero - just a bit of HTML templating at the edge of your system (which ironically brings us pretty much full circle, though with a much better UX).
My biggest hope here is that we’re moving past the era of fragmentation, of every-day-a-new-JS-framework. Freedom and flexibility have bred innovation, but they’ve also resulted in a web experience that is messy, disjointed, and oftentimes fundamentally broken. And it makes sense that when developers have to choose between fifty-odd options and cobble them together themselves, with limited resources and tight deadlines, that this is the experience we would see. Some apps are brilliantly fast, consistent, reliable, and fun to use, while others are frustrating, confusing, slow, and broken.
If we can give devs easier to use tools that do-the-right-thing-by-default, maybe the average website will get a bit better, the average experience a bit smoother. It won’t fix every site - no amount of code can solve for bad UX design. But it would lay a common foundation, so every site starts out a little bit better, and every dev has a little more time to focus on the other things.