Some people like to argue that the site we served ten years ago and the one we serve today are the same. But it’s not a fair comparison. The sites of today are way more complex.
And while some of the complexity comes from better interactions and functionality for the end-users, a lot of it comes from helping us gain insight into our users' behavior, which helps us build even better sites. Our sites ultimately get bigger because they do more stuff not only for our end-users, but for us as well.
Every year, the CPUs are getting faster, but most of the speedup comes from improved parallelism. And in recent years the performance improvements per core have been slowing down. We’re approaching the limits of how fast CPU clocks can run and how much cleverness the CPUs can do per clock cycle. As a result, we see more multi-core CPUs pop up (It is not uncommon for your phone to have 4 CPU cores and 4 GPU cores). So the main driver of CPU performance improvement these days comes from parallelism of multiple cores, not from improvements in a single core.
Ultimately, our current JS load is increasing faster than the rate of single CPU core performance. This is not sustainable.
Frameworks for desktop applications have existed way before the web. So when the web came, we just took our desktop framework philosophy of what the framework is and applied it to the web. Desktop frameworks can safely assume that all of the code is already available and that there is no server. The problem is that those two key points are not true for the web. We can't assume that the code is already available, and server pre-rendering has become an important part of our vocabulary.
We’re at a stage where the frameworks we have are pretending they’re desktop frameworks. They have not embraced SSR and lazy-loading to the core.
An application needs to boot up on the client. The bootup consists of a fixed cost of the framework and a variable cost of the application itself (the complexity of the application). So really, we are talking about a linear relationship which can be expressed as “y=mx+b”.
When frameworks argue who is smaller, they can really only compete on two points: the fixed cost of the framework or the variable cost of the application. So either a smaller “m” or a smaller “b”.
For the sake of my argument, the exact numbers aren’t important, but the table does a good job of illustrating how, as the application size increases, different frameworks have different slopes (“m”) and different initial values (“b”).
The problem is that all frameworks have approximately the same slope. And even if the framework completely compiles away so its “b” becomes zero, the application dominates the download and execution size. The “b” kind of doesn’t matter for sufficiently large applications.
All of the above lines are “O(n)”. As the application gets bigger, so does the initial bundle size. Applications will continue to get bigger as we continue to build better end-user experiences.
What we need is a new paradigm. We need a framework with a constant load time no matter the application complexity. At first glance, this may sound impossible—surely, the initial bundle is proportional to the application complexity. But what if we lazyload code rather than doing it eagerly? I covered why existing frameworks need to eagerly download code in my post, Hydration is pure overhead. Here’s what we’re looking for instead:
Current frameworks already know how to do this to some extent, as they all know how to download more code on route change. What’s needed is to extend that paradigm to the interaction level.
In the above image the site is delivered as HTML to the client. Notice that none of the components have the coresponded code loaded. The next image shows which components need to be downloaded and executed on user interectaion. The result is that a lot less code needs to be downloaded and the code which is downloaded is executed later. This offloads the amount of work the CPU has to do on site startup.
While different frameworks have different slopes in the end, any slope is too much. Instead, we need to lazy load on interaction rather than on initial load. Frameworks already know how to lazy load on route change, we just need to go deeper and do it on interaction as well. This is needed so that our initial load size can be O(1) and we can lazy load code on as needed basis. It’s the only way we can continue to build even more complex web applications in the future.