Angular and SPAs - Caching for the REAL need for speed?

Jose I Santa Cruz G
ITNEXT
Published in
8 min readDec 20, 2022

--

Performance requires understanding performance

👍 yeah! faster…

WARNING: This article is highly opinionated. You may disagree with many of my appreciations, and that’s absolutely OK.

Most frontend framework tutorials teach you how to use them, and usually skip some things that have to be considered when facing a real world challenge. Performance is one of these topics, and it’s usually understood how fast our web app work.
When a real world application reaches an important size and complexity, performance is a MUST, clients MUST have a good and performant experience while navigating our web app.

In this article, I’ll write about a couple of things that may affect your web application performance/speed, most of them highly oriented towards an Angular SPA, but you may extrapolate the article and try to apply the concepts to your favorite SPA framework.

What makes a SPA performant?

First, let's think about the non-app related things:

  • Fast user computer (lots of RAM, and processor)
  • Fast server (lots of RAM, and processor)
  • High network speed
  • "My tests on localhost:4200 ran super fast" 🤌 c 'mmon…

OK, , see the problem here? If your performance measures only consider those elements, you’re missing some points.

Let's add some app-related things:

  1. Lazy loaded modules
  2. Any UX tricks such as infinite pagination, or intersection observer over images or non-visible content
  3. Small CSS bundle (eg using Tailwind CSS)
  4. Small API operations (on the backend)
  5. Cache cacheable API requests on the SPA

And how to ruin your app speed & performance improvements:

  1. Your modules are too big (or have bad internals, such as import * from library;). No matter how lazy you load them, they’re still big or terribly developed.
  2. Intersection observer over a 5MB image (have seen that…), or over 40 simultaneous API requests is not good.
  3. You may have bought that Tailwind leads to really small CSS bundles. Will not work properly if you use @applyall over your SCSS files. (Tailwind can become extremely verbose in your HTML templates but believe me that once compiled, the final styles.css file will be really small)
  4. API operations can be small, but should also respond fast.
  5. Cache all the things!
Not so fast, Fast

I’m purposely skipping some other good development practices that usually help in the performance quest.

Briefly understanding the Angular build

Most typical Angular builds lead to:

  • a single index.html file
  • a single hashed styles.css file (something like styles.1234abcdrandomhash.css)
  • many “hashed” .js files
  • an asset folder
Example Angular build from a real pipeline

Hashes in a filename are meant to work as cache-busters, meaning that a particular file can be replaced with another one with another hash. Perhaps the file will be cached at the browser or get cache-control headers from the server, but it may be replaced with another file, with another hash. Eg:

  • 1st compilation: user.modules.ts-> compiles to -> 105.123bcdsomeuglyhash.js
  • 2nd compilation: user.modules.ts-> compiles to -> 207.345qweanotheruglyhash.js
  • nTH compilation: user.modules.ts-> compiles to -> 654.978poisomeotheruglyhash.js

There are some other options that allow Angular to compile without hashed or keeping a user friendly name. For that, you’ll have to read some Angular documentation and screw up your angular.json file more than a couple of times (I usually do so almost on a daily basis 😥).

To cache or to not cache, that is the question…

The index.html doesn’t have a hash, and that’s key knowledge when deploying an Angular app to a server. The index file keeps it’s filename, so if this file is cached in any way, it will retrieve the cached contents.

  • The good thing: The file is retrieved really fast from any cache
  • The REALLY bad thing: The file contents may be outdated, so many app scripts won't exist, and will certainly fail to load due to the different hashes that result when compiling the app.

All your speed gains may end up in a no working web app. Not good.

The usual advice is that the index.html file doesn’t have to be cached in any way from the server. That means serving theindex.html file from the server with these headers:

Cache-Control: no-cache, no-store, must-revalidate
Expires: 0
Pragma: no-cache
A example on how headers are set up on an Express app

Caching the index.html file will only lead to headaches, script loading errors (HTTP status 404 file not found), and a terrible UX.

But the index.html file is a static resource and static resources have to be cached. Isn't it?

Static resources such as images or some other 3rd party scripts that may be included in the site (and are not hashed in the compilation), yes, agreed. Static HTML files that have contents that are not changing (eg a Contact form page), agreed.
But in this case (or any compiled SPA with some file hashing logic), we can argue if it’s good or bad.

How do we solve the index.html caching issue? (I still want to cache it as a static resource)

The only way is to physically store every old hashed file (both javascript and css) in order to make sure that even an old and outdated index.html file loaded from somewhere, loads every old and outdated javascript and css files it requires.

The big problem

Every time the app is deployed, its compilation will add a bunch of new files, as hashes are never the same between compilations. Sure, the index.html file and any other static non-hashed file will be replaced, but your app will certainly repeat some modules, with different filenames, but still the same module.
After a few deployments, you’ll be dragging a huge amount of cosmic waste, and if you don´t have a reset strategy, in order to delete unused and/or ourdated files, you’ll be filling up you HDD quite fast.

OK, HDD is not an issue, as “storage is cheap”. Let’s throw some numbers:

  • 20 MB or so sized app (real world app, remember?)
  • 1 deployment per day (+- 20 deploys per month, considering only work days)
Missing the 80’s Peavey T-40 bass

Considering the node_modules for your backend (real world app, remember?), in a couple of months your app can eat all the space in a starting tier deploy instance. Cosmic waste IS an issue.

So…, caching is good, but you have to know what to cache, and how to cache it.

There was a time when all developers ran loose on the web, just to show who was the fastest one. There was an unpopular library called Basket.js (https://github.com/addyosmani/basket.js) which in the eternal quest for performance, aimed to improve it storing all scripts in localstorage. Nice idea until you tried to navigate in incognito mode.

What about service workers?

Same problem as caching our index.html files. You can make a pretty good configuration to define a performant criteria. But if your web application is not a former PWA, you’ll just end up trying to load old scripts and worst of all, as your app doesn’t update itself it will be a real PITA trying to keep your app error free due to script not found errors.
Terrible for your users’ experience. Been there, done that, suffered…

Does this particular app has to be cached for speed?

Before answering this question let's answer another one:

What type of application are we developing?

  • Is it a website?
  • Is it a public web application? (by public meaning no user registration required)
  • Is it a private application? (requires user registration, or main app entrance is a login screen)

Side question: Is this an app that takes some benefit on SEO or high Web Page Speed/GT Metrix/Lighthouse rankings?

A few facts:

  • User input (typing, scrolling, clicking) takes as an average ~300ms (100ms is understood as instantaneous https://www.nngroup.com/articles/response-times-3-important-limits/ )
  • Average users read about 230 word per minute
  • Angular (and most SPA frameworks) use idle time to download any deferred script
  • Only QA users (or people on your own development team) are prone to do extremely fast navigation, scrolls, or clicks before all the page has fully loaded. So this kind of behavior can certainly happen, but not with the frequency you expect.

Mixing both lists:

  • Websites? Yeah!, absolutely, cache it all, or most of it. SEO, performance, UI-UX, speed rankings, everything will mean improvement.
  • Public web applications? Yeah! mostly because it’s like a website but with “app like features”.
  • Private application? We can argue on this one. I would say cache doesn’t matter, as there’s no SEO or page speed penalties. What really matters here is that the end user gets the latest, most updated version of the deployed app.
20% gotta go faster, always…

So…, will a 200ms improvement in loading time, due to caching, is really a performance gain? I mean besides your own self saying “Wooohooooo! the site loads 200ms faster”? It depends on the app, and your audience; 80% won’t notice, 20% will always ask for more speed.

Best cacheless user experience for private apps

Thanks for reading so far.
Don’t know if this will suit your requirements, but perhaps you should cache the app (I know, not cacheless…) , at least your scripts. The thing is that as a private app is not under the Lighthouse (or similar) pressure, you can skip lazy loading and preload all modules. In that way, users will feel almost no transition while navigating the app, all modules will be already loaded.

How to do this?

import { RouterModule, Routes, PreloadAllModules } from ‘@angular/router’; 
@NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
],
exports: [RouterModule]
})
export class AppRoutingModule {}

The truth is that this last one is not a gem of advice, but it will supposedly ease your cache=speed soul fires.

Caching by itself won’t make your web app fast enough for a 100 Lighthouse score, and there’re so many considerations that you’ll probably end up asking yourself if it’s worth banging your head against the keyboard or all the effort.

--

--

Polyglot senior software engineer, amateur guitar & bass player, geek, husband and dog father. Not precisely in that order.