Mind the cost of JS frameworks

Marcos Sandrini
10 min readAug 26, 2021

In current Frontend development, as I speak, it is very hard indeed to separate JavaScript from its so-called Single-Page Application Frameworks. They are the first choice before a project is initiated, every FE developer has their own favourites and even job positions are defined by them.

And why we came to this state of affairs when it feels so basic to use a library like that? Although using only “vanilla” JS for FE projects would be in theory much better for many reasons which I’ll discuss later, it is simply not scalable for most real-life uses today (mostly single-page apps) especially when working in teams. The vast majority of times people pick a framework to use, even because if they refuse to do it they might end up building a new one.

One observation: I’m including React as a framework here for the sake of simplicity, although I know it’s technically not opinionated enough to be one. The fact that most people use React together with other companion tools like Redux or other complementing libraries means that, in practical terms, it has the same effect as a framework for all points presented below.

Let’s start then by saying the general motto here: all these libraries may have great features and use-cases, but they come with a cost. There are some components of this cost which are more obvious to see, some less obvious but, in the end, if correctly addressed, these points can be fixed or vastly reduced and we developers can be more successful in our endeavours.

Clarity and readability

One arguable selling point of frameworks is to deliver a slimmer, more direct codebase that is more understandable than something developed from scratch without them.

There are two main problems with this though:

  1. developers may be indirectly stimulated to go too far into abstractions offered by frameworks that steal clarity from the overall picture;
  2. some APIs are too extensive and/or hard to read.

A simple example for the first case I came across recently when working on a legacy project with Vue.js was stumbling upon global mixins. They are pieces of reusable code that are made globally available to everywhere in the application, like this:

const app = Vue.createApp({...})// in some other file
app.mixin({
methods: {
myMagicMethod() { ... }
}
})
// in another file
app.component('my-component', {...})

In the example above, MyComponent has access to a myMagicMethod even from another file without importing it. Global mixins are advised against now and, although they work in their intent well, they feel too “magic”: they are not explicitly imported, not part of the framework API but they are there. If one spots something like a myMagicMethod in a random component on a big project they will be scratching their heads, as it would be coming from seemingly nowhere. Again, they are not recommended anymore but this is just an example, as there are similar structures on many other frameworks out there and this may come as an issue on the project level, hindering readability and maintainability.

About the second point, some frameworks can be just too opinionated for their own good, especially if meant to be used on smaller or more general projects. Big frameworks (that may have a very important specific use) need to be evaluated as to whether all the luggage they carry is needed or not, both on the download/bundle size (which we’ll address later) and on the number of API commands needed to be memorised, which is an important point of discussion before adopting a brand new framework on a team that is not familiar with it.

Reinventing the wheel

Frameworks in general are somewhat known for sometimes offering a mild standardisation on top of browser differences and future HTML/CSS/JS structures not yet implemented or not standardised. Once browsers evolve and those structures become reliable on the native side they may be kept implemented “twice” both on the browser and on the framework, which is a waste of valuable bytes.

For that, be sure to keep your framework always updated and also make good use of tidbits like browserslist or equivalent, always following the usage trends of your audiences to make sure you are using and bundling not more than what is needed to be there.

Accessibility

I talked about accessibility in some of my previous articles, like this, but it suffices to say that I find it needlessly complicated and because of that its implementation ends up being very uneven.

I don’t blame frameworks for that, as in my opinion, the complexity of accessibility as an API is a native issue of JS and HTML. With this said, though, frameworks can be used in a way that makes the problem worse.

One of the issues is that most frameworks end up indirectly stimulating dummy wrapper elements on the resulting HTML, which breaks semanticity (which is a problem I wish we didn’t have). It can be argued that this stimulus just exists because of how easy it is to make flexible components, and it is mostly an issue of HTML itself. In any case, most of the frameworks, if not all modern ones, offer some kind of “transparent” wrapper like React’s (and Svelte’s) Fragment or Vue’s template that prevents developers from having dummy elements that break semanticity. Whenever

Another issue, representative but simple, is about page titles. On many SPAs of today, the page titles don’t change even when (virtual) pages change, and this can be impactful for users of screen readers. This is simple to fix: document.title = 'something' when the virtual page is initialised and that’s it: no reason to be extra lazy neither a reason to install any external libs.

Some people also talk about problems with field focus, keyboard usage and aria attributes but, at least in the frameworks I have worked with, I never had any trouble with those, as native events are usually available in frameworks just like as they are in native JS (so keyboard events and focus events are easily programmable) and aria-* attributes they are just HTML attributes one can put there, even when dealing with JSX or other custom templating.

Performance

This is one aspect that is often given more importance than needed, but it may be important nonetheless. The performance of a framework-driven app is always bound to be slower than the performance of a “vanilla-only” app in JS, and although this obviously would depend on implementation details this is a very safe general rule.

The first SPA frameworks were extremely slow, mostly because they aimed at changing the HTML structure on the fly by traversing the actual DOM in search of specific elements to modify. Virtual DOM seemed to be the ultimate idea made to tackle this issue, in which a copy of the HTML structure would lie on JS memory, removing the need for this slow traversing. Newer frameworks like Svelte and Solid.js claim to be even faster without using Virtual DOM, via a compilation scheme that generates tailored JS that aims to eliminate the need for traversing the DOM in the first place.

Regardless, if a developer has in their hand a project that has performance as the priority (which is usually not the case) then using no framework at all is worth considering. If there are still many things to be managed on a project and a framework is deemed needed, there are libs that prioritise speed like Inferno (that has the advantage of using most of React API) or Mikado, so these are worth considering.

For most real-life projects though “normal” frameworks are fine, as long as they are used wisely when it comes to speed optimisations, which is sometimes a craft in itself. Generally speaking, it is recommended to avoid too much useless reprocessing like repaints on the screen, which in most frameworks tend to happen automatically after dependent data changes, for example, so developers may lose track of what is happening.

Bundle size

This is a very important aspect that arguably used to be bigger than it is now. However, even if it may be so, it is advisable not to be fooled by the fact that connection speeds have increased a lot and use this as an excuse to shove more and more bytes needlessly on our applications. Our users are very frequently still dealing with data-limited carrier plans and connections are mostly subject to instabilities.

Moreover, one could make many cases for smaller delivery sizes: they usually are a sign that the underlying app performs better and has a better first interaction time (as we’re going to delve into), not to mention that less useless data exchanged can represent less money spent by the companies and, from a grander point of view, less energy spent. If we hypothetically squeezed the useless data that is delivered out of our applications, we could make for an actual ecological impact.

In this context, frameworks are not usually the best: they frequently pack a number of structures that may not be needed, they usually deliver they own monolithic structure to be served as a real-time processor for its functionalities and, last but not least, being a layer on top of pure “vanilla” JS they will almost necessarily be heavier in size than a non-framework app.

To address those reasons, we can have some counter-measurements:

  • Tree-shaking: many libs provide services that can be imported individually from the framework pack instead of being in a big monolythic package. This way, we can use them without necessarily adding other parts of the framework we are not bound to use.
  • Frameworks with compiled structure: frameworks like Svelte are compiled in a way they are meant to deliver as little of their own code as possible to the end-user, resembling as much as possible a native and optimised vanilla code.
  • Mind the lib sizes: it is customary for FE developers to resort to available libraries on the Internet for more custom UI controls, for example, or other situations where one can benefit from adopting libs already available out there. Going for the most popular or git-starred library to do something may not always be the best answer, as one particular lib may be too generic, delivering much more than needed for a simple kind of use. One can apply tree-shaking here too, if possible, to reduce the lib bundle size, but alternatives for simpler situations can be to either try native implementations, to build a custom solution or to search for smaller pieces of code instead of full-fledged libraries, which are also usually more customisable.

First interaction time

Analogue to the last two points above but even more critical for most apps today. Users, especially on mobile, are often used to the behaviour of native apps that tend to be very fast or feel “instantaneous” when it comes to showing the initial screen. Web apps that are not installed (or not installable) have a disadvantage in this aspect because they have to query the network to be downloaded and later initialised.

When using a framework this is especially true, because in the “classic” way frameworks do their jobs we have an empty HTML which is only a placeholder for the framework JS routines which will render the content. This means that the user will necessarily have to wait until those JS routines take over, which can be made fast but is never instantaneous.

One way this can be addressed is by providing means to make the app installable, transforming the app into a PWA (Progressive Web App). Migrating to a PWA is a forgiving process that can be done incrementally and PWAs can offer other nice advantages such as being able to make everything work offline and to be listed on Android’s Play Store just like a native app.

Other ways to improve the first interaction experience, especially useful when the app is not installable or for its first use, are server-side rendering, lazy-loading and prefetching resources. Let’s talk about those in depth.

Server-side rendering

Server-side rendering via tools like Next-js can be very useful, as without it there will be necessarily a time before the first dynamic render from the JS routines. Most frameworks now have their server-side rendering counterparts and some even support this mode of rendering out-of-the-box. On very simple apps, though, one doesn’t even need to apply complex tools: it is enough to have some placeholder/dummy content on the original “index.html” entry point, that can be later removed when the framework has it all loaded.

Lazy loading

It is an industry-standard behaviour to try deferring the loading of all resources the user is not going to need immediately, but this can be very tricky. When applied to visual resources like CSS rules and fonts, lazy loading can lead to a poorer experience inadvertently. Still, considering that human beings always take a minimal time to have their first active reaction to a page, it is fundamental to dive into this sort of optimisation, meant to use this natural “human delay time” before the first active reaction. This way, we can establish a prioritised load sequence that gives the impression to the user that everything is there and, most importantly, allows them to think and react naturally, even if there are still things to be loaded.

Prefetching resources

Prefetching for pages used to be a very simple way of optimising websites. URLs were provided in the HTML headers to be prefetched and so they were, and that was it. Now, in the age of single-page apps, prefetching is not as simple as that anymore, but it is still doable, which is especially useful for bigger applications with many different sections and functionalities.

One possible approach today is to load additional resources triggered by user interaction. Let’s say we have a component that is bound to be loaded when the user clicks something, and this component has its own resources to be loaded. If the situation implies that a click is imminent (with a mouseover, for example), the load of libraries can start when triggered by this mouseover event.

Another approach is to load resources anyway (lazily, of course) for subsequent pages in a wizard-like progression, when it is certain or almost certain that the user will go through the next step.

Conclusion

It is important to say that frameworks are not a bad thing at all and they are industry standards now for a very good reason. Also important is to say they are, per se, not meant to be structures that benefit only the developers to the detriment of the users, that have to pay for the extra data and wait some extra milliseconds (or seconds). If they introduce extra bytes to be downloaded and extra time to wait, it doesn’t mean necessarily a wasted resource and it may well be worth it. For everything they facilitate, frameworks are now part of the natural development of Frontend, created and kept as a necessity for the applications we demand today.

With this said though, we have to bear in mind all the costs they have in all the points mentioned above, acting to make sure we don’t indirectly punish our companies and our companies’ users for our framework choices, using those frameworks as an end to deliver them what is the best possible outcome.

--

--

Marcos Sandrini

Designer and front-end programmer with 20+ years of experience, also a keen observer of the world and its people