The goal
To choose a right tech stack for building an SPA (Single Page App), which gets the best possible score in lighthouse audit. Just telling in advance, we are looking for something simple and elegant, not for a pure JS solution or some rocket science.
Why to even attempt?
There are multiple benefits:
Our app will get best technical rating by Google Search Engine. SEO is no longer the only indicator, responsible for sorting of search results. Performance and accessibility are getting more and more important. See more in searchmetrics.com study from 2019.
Users of Our app will be happy even when opening it with slow device or poor network connection.
We’ll be forced to make simple app, containing only what’s really needed. With no bloated, hard to understand functionality.
Lighthouse indices
Lighthouse audits sorts out its findings and proposals into four categories: Performance, Accessibility, Best Practices and SEO. All of them are important for ranking in google search, but some are more important than others. Getting the last three to 100% won’t be that hard. We just need to add all required metadata to a page and set up server correctly. Getting Performance to 100% is different story — we will focus on that later. Based on searchmetrics.com study Lighthouse Ranking Factors 2019, top ranked search results (when searching generic keywords like “shoes”) have something in common. They are best in: Performance, time to First Contentful Paint, following best Practices, size of DOM, they use webP images and run over https or better http/2.
searchmetrics white paper showing top 20 search results in google search with data about their speed of FCP
SEO and Accessibility are also important for them, but correlation between its score and ranking in top 20 is not obvious, and rather misleading.
searchmetrics white paper showing top 20 search results in google search with data about their SEO
Why making fast SPA is so hard in real world?
We as developers experience era of fancy frontend frameworks. Interactive functionality, which we could have dreamed about 10 years ago, is not only possible these days, it is often matter of minutes to implement. It has been a giant leap for developers, but rather small one for users. Our networks are 10 times faster; our computers are 10 times more powerful, our frameworks are 10 times more awesome, but it takes similar time to load average web page, as 10 years ago. How is that even possible?
You always wish to have extraordinary web, which is step ahead of your competition. Full of interactivity, animations, images and videos. Your product owner wants it as soon as possible. Your stake holders want all stats about visitors. Your FCO wants more ads to make more money, and so it goes on. It is a common pattern I have been observing over 13 years of my professional carrier as web developer ;) In the end, requirements in its MVP (Minimal Viable Product) are already so demanding and way beyond what users really need and want. Performance is simply not a priority for most of software teams. As Jeremy Wagner says in: Innovation Can’t Keep the Web Fast. A “hello world” app with all above mentioned requirements would already be 1 or 2 MB. Especially if you use something like React, or Angular. We can do better. We should start creating web for users again!
Let’s try to learn from big companies and their successful products. Facebook for instance; its SPA homepage (on desktop) is loaded in about 10s, has 9.8MB and it is all done in 350requests. There are some advanced techniques used, but it is not point of this article. I can just say it performs better than it looks. To be honest, its visible content is loaded in 4s; it is not bad, considered the app complexity. But do we need all that complexity on initial load? I would say no. Just check facebook.com lighthouse ratings; it is a disgrace.
facebook.com in desktop chrome MacBook Pro 2018 i7 (Simulated Slow 4G, 4x CPU Slowdown
Maybe it is not that great idea to have one of the most loaded pages on the internet as a model. We will try to achieve 4x100% with far less complex app. You always need to think twice, whether an app really needs to be so huge and complex. Especially when you can lazy load most of the stuff.
Choosing right tech stack for 4x100%
I am sorry, but very probably your favourite framework won’t do it. To choose one which will, we need to understand all restrictions and requirements.
First of all, we know, we need a JS code because we want to build SPA. We cannot do that with server rendered pages, without JS.
We need either vanilla JS or a lightweight framework. I can tell you straight away, that if we aim for 4x100% on slower mobile devices, we cannot use any of the holy trinity of JS frameworks (React, Angular, Vue).
Just for illustration, a React hello world app itself (one dummy screen with no routing and data management) won’t pass the audit with 100% Performance. Not even Next.js (server prerendered and optimised React app) can reach the desired Lighthouse Performance score, as it still contains a minified build of React and only gets us to 96% for Performance.
Why? Because even if an app is prerendered on server, React itself is still loaded for later hydration. Processing(parsing and compiling) React code by a browser is quite expensive operation, responsible for the score. It is important to understand that cost of 100kB of HTML is very different from 100kB of Javascript. An option is to load javascript in async mode, after page is rendered. But it has negative impact on other measured index —” Time to interactive”. Not even mentioning that above stated score was achieved with blank page, on very fast machine on local server. Once we start adding our own code and move the app to internet, we will be doomed; the score will drop significantly.
Inferno, Preact and some other clones of react would be able to reach the score, but we won’t use them. They wouldn’t give us the luxury of full featured framework and we would end up with writing a lot of pure JS code along those libraries.
Fortunately there is an unspoken demand for what we are attempting here. There are frameworks, popping up right now, trying to please both, users and developers. They offer rich features and minimal trace at the same time. For our purpose we need two things from such a framework:
1) to support easy lazy-loading of almost anything and
2) minimal or no size on its own.
Splitting code to lot of smaller bundles enables parallelisation of JS parsing and releases the main thread for more important tasks (see more on V8 blog The cost of Javascript). If we want to fulfil the second point, we will need to get rid of a framework. But as long we want to keep framework’s declarative syntax, there is only one solution to this riddle: a compiler.
A compiler
Probably the most endorsed “framework” of this type is Svelte. With Sapper, its pre-rendering counterpart, they fit to our requirements. So let’s focus on them.
It will be good to know, what are the limits of compiler compared to regular framework, The fundamental difference is in a way how its code is run in a browser. With regular FE framework you are able to load such a framework by <script src=”framework.js”>
tag and then just write your code inside another <script>
tag. None of the modern frameworks encourage developers to follow this pattern, and with most of them you would have hard time to get it running this way. But you can, it gives you power to dynamically create components and inject them during runtime to your app. It can be “must have” in some apps, but — let me exaggerate a bit — in 99.9% of them you don’t need it. For those 99.9% you can use a compiler.
Svelte is a compiler, which accepts code written in similar manner as in React, Angular or Vue (component centred architecture), but is compiled to direct DOM manipulation instructions. If you don’t use a feature of Svelte, it won’t be output to a production bundle. If your page is just 10KB of HTML and CSS, then the svelte generated page will have about 10KB. If you use one two-way binding in that page, you will get maybe 0.1KB extra. Definitely there won’t be a 100KB framework handling your one two-way binding ;) With Svelte you can write high level declarative code and you will end up with highly optimised native JS code. With svelte we know, we start with minimal burden.
The Jamstack
So we chose the tech, but there are still decisions to do. Should we just bundle an app to big JS file, prerender it on server, or serve it as static assets? Best option seems to be be the last one. It has several advantages: 1) Code is split to smaller chunks. 2) Content loaded for first meaningful paint is served from a static HTML file which can be easily served over CDN.
Once static page is loaded, we can fetch JS and add some dynamic functionality to it. We can even do some api requests and customise the page for a user. This approach is called Jamstack, it is successor of statically generated pages. Jamstack is bringing API and more custom content to static generators. Jamstack stands for JS, Api, Markup.
Fortunately for us Sapper does support static generation of pages. It also provide basic setup of service worker for subsequent loads of the app. It comes with some minor bells and whistles supporting prefetching of pages before you hit them, basic in app routing. It all in cost of 13KB (before G-zipping).
FE is just one side of the puzzle. We will also need reliable CDN server, fast API (cloud - optimally - distributed database). User Authentication and authorization of requests. These BE and middleware related topics are not main focus of this article, but we will touch them, because…
In Part 2 of this serie, we will try to prove our tech stack in a demo app. We are gonna build and deploy hello world SPA… In Part 3 we will turn our dummy app into real weather forecast SPA.
See you next time;)
Top comments (0)