I started coding primarily in JavaScript back in 2012. I had built a PHP app for a local business from the ground up, a basic CMS and website, and they decided that they wanted to rewrite it and add a bunch of features. The manager of the project wanted me to use .NET, partially because it’s what he knew, but also because he wanted it to feel like a native application - no page refreshes or long pauses between actions. After a bit of research and prototyping, I convinced him that we could do the same thing with the web, using one of the brand new JS frameworks that were just starting to come out.
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.
A lot has changed since then - frameworks have come, gone, and evolved massively. The idea that you could build apps in JavaScript, in the browser, went from somewhat fringe to a standard practice. And the infrastructure that we build on has completely changed, enabling a host of new possibilities.
There’s also been a fair share of competition and conflict between ideas in that time. I think most of us who’ve been in the frontend space for a while have probably been in some debates about… well, everything. Which JavaScript framework to use, how to write your CSS, functional vs object-oriented programming, how best to manage state, which build system or tool was the most flexible and the fastest, and so on. Looking back, it’s funny to me how often we were arguing about the wrong things and missing the larger patterns, but of course that’s the benefit of hindsight.
So I wanted to do a retrospective, looking back at the last few decades of JavaScript development and at how far we’ve come. I think we can roughly divide it into four main eras:
- 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.
Today the debates rage on: Has the web grown too bloated? Does the average website really need to be written in React? Should we even use JavaScript at all? I don’t think we can see into the future here, and in the end I suspect we’ll probably discover that once again, we were talking past each other and missing the bigger picture. But maybe getting a bit of perspective from the past will help us to move forward.
JavaScript was first released in 1995. Like I mentioned above, I started writing JS in 2012, almost two decades later, near the beginning of the era I’m dubbing the First Frameworks. As you can imagine, I’m probably going to gloss over a lot of history here, and this era could probably be broken down into many sub-eras in their own right, each dominated by its own patterns and libraries and build tools and so-on.
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.
Prior to these, the state of the art had been libraries like jQuery and MooTools. These libraries were very important in their time - they helped to smooth over the differences between the way that browsers implemented JavaScript, which were very significant. For instance, Internet Explorer implemented eventing completely differently than Netscape - bubbling events vs capturing events. That’s why the standard we have today implemented both, ultimately, but before that you needed to use libraries to write code that would work in both browsers. These libraries were primarily used to make small, self-contained UI widgets. The majority of application business logic still took place via forms and standard HTTP requests - rendering HTML on the server and serving it up to the client.
There also weren’t many build tools to speak of in this era, at least that I was aware of. JavaScript didn’t have modules yet (at least not standard ones), so there wasn’t any way to import code. Everything was global, and it was pretty difficult to organize things.
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. The most common thing you would do was include jQuery, throw together some scripts for a few UI widgets, and call it a day. 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.
Doing all of that with JavaScript was seen as ludicrous at first. But as time went on, applications started to get more ambitious. Social networks added chat and DMs and other real-time features, Gmail and Google Docs showed that desktop-equivalent experiences could be written in the browser, and more and more companies were turning to writing web apps for more and more use cases, since the web worked everywhere and was easier to maintain over time. This pushed the envelope forward - it was now clear that JS could be used to write non-trivial apps.
Doing so, however, was the hard part. JavaScript did not have all the features it has today - like I said, everything was global and you generally had to manually download and add every external library to your static assets folder. NPM didn’t yet exist, modules weren’t a thing, and JS didn’t have half the features it has today. For the most part, every app was bespoke, with a different setup of plugins on every page, a different system for managing state and rendering updates in every plugin. To solve these issues, the very first JavaScript frameworks started to emerge.
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:
There are, of course, plenty of others, and probably some that were even bigger in some circles. These are the ones that I remember, mostly because I used them to prototype or build things and they were relatively popular.
This was a generation of frameworks that were setting out into uncharted territory. On the one hand, what they were trying to do was highly ambitious, and plenty of people thought it would never really work. There were many detractors who thought that single-page JS apps (SPAs) were fundamentally worse, and in a lot of ways they were right - client side rendering meant that bots couldn’t crawl these pages easily, and that users would have to wait seconds for apps to even start to paint. A lot of these apps were accessibility nightmares, and if you turned off JavaScript they wouldn’t work at all.
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.
This was also an era when we really began to experiment with how to compile JavaScript applications. Node.js was released in 2009, with NPM following it in 2010, introducing packages to (server-side) JavaScript. CommonJS and AMD competed for how best to define JS modules, and build tools like Grunt, Gulp, and Broccoli competed over how to put those modules together into a shippable final product. For the most part these were very general task-runner-like tools, which could really build anything and just happened to be building JavaScript - and HTML, and CSS/SASS/LESS, and the many other things that go into a web app.
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.
- JavaScript had a lot of issues as a language, and it really needed to be improved to make things better - frameworks couldn’t do it alone.
- 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.
Component-Centric View Layers
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.
For one thing, this was not the framing that was encountered very often when debating which framework to use at work, or if we should rewrite our apps. Instead, very often it was “it’s faster!” or “it’s smaller!” or “it’s all you need!“. There was also the debate over functional programming vs object-oriented programming, with a lot of folks pushing FP as the solution to all of our problems. And to be fair, all of these things were true: View-layer-only-frameworks were smaller (at first) and faster (at first) and all you needed (if you built or stitched together a lot of things yourself). And absolutely, functional programming patterns solved a ton of problems that plagued JavaScript, and I think that on average JS has gotten a lot better because of them.
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.
But on the other hand, the existing full-service first-gen frameworks weren’t doing too well at solving these problems either. Partially, this was due to a lot of tech-debt baggage. The first generation of frameworks were built before ES6, before modules, before Babel and Webpack, before we’d figured out so many things. Evolving them iteratively was extremely difficult (I know this all too well from my experience as a former-Ember core team member), and rewriting them entirely, like Angular did with Angular 2, killed a ton of their community’s momentum. So, developers were in between a rock and a hard place when it came to JavaScript frameworks - either choose an all-in-one solution that was starting to show its age, or jump into the free-for-all and DIY half your framework, hoping for the best.
Like I said, at the time this was deeply frustrating, but a ton of innovation came out of all of it in the end. The JavaScript ecosystem advanced really quickly as these frameworks figured out their best practices, and a few other key changes happened:
- 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.
- Edge computing was released, giving JavaScript based server apps the benefits of SPAs in terms of distribution/response times (SPAs could previously generally start loading faster due to being static files on a CDN, even if it took them longer to fully load and render in the end).
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.
Full-Stack Frameworks
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:
- Next.js (React)
- Nuxt.js (Vue)
- Remix (React)
- SvelteKit (Svelte)
- Gatsby (React)
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.
And this is just the tip of the iceberg. There are really so many cool things about this model, one of the biggest ones being how it is revitalizing the idea of progressive enhancement, using the combined nature of the server and client to allow the client to fallback to basic HTML + HTTP in cases where the user has disable JavaScript. I had fully given up on this practice myself when I began working on SPAs, assuming that they were the future, but it is really cool that we could possibly see a world where it makes a comeback.
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.
Overall, I think the JavaScript community is heading in the right direction. We are finally developing mature solutions that can build full apps from the ground up, solutions that are not “just a view-layer”. We’re finally starting to compete on the same playing field as SDKs for native apps, providing a full toolkit out of the box. We still have a lot of work to do here. Accessibility has long been an afterthought in the SPA space, and outside of GraphQL I still think that the data story could use some work (like it or not, much of the web still runs on REST). But the trend is in the right direction, and if we keep moving toward shared solutions I think we could solve these problems in a better way than ever before.
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.