Avoid cache trap when serving Angular app
What is the weirdest bug you ever got? One of my top ten came after a major design change we made:
I opened the app, and I’m getting a blue design. After I’m doing a refresh, I’m getting a dark design. And it is happening every morning.
Every morning? WT@!*@!@#???
So since you have the title of this article, you already know this one is related to cache, but why is this happening, and what can you do to prevent it?
We all love cache, I think. It helps to load our apps faster, lower some load from the server, and let our users a great user experience in our app.
But sometimes, this cache is working against us and causing our users not to get the last version of the app, which is probably because of a wrong cache configuration in our server.
To avoid cache issues in our Angular application and manage app versioning, when we are building our app to production using ng build --prod
Angular adds (by default) a hash to our js files and updates our index.html
file to refer to the hash files. When we deploy a new version, the hash keys are changed, and when the user asks the site again, the index.html
will ask to load the new files from the server. Since the browser doesn't have those files in the cache, it will get them from the server.
So it looks like Angular guys cover us? Well, not entirely. The problems are starting when the index.html
file is cached. Let’s say we just deployed a new version of our app, and the static files, and index.html
file with them, are cached. In this scenario, when the user is starting to use our app from the main URL, he is getting the old version of our app because the cached index.html
asks to load the old js files, and they will probably load from the browser cache.
But let’s move on with the scenario and complicate it a little. The user uses our app (remember — with the old version), moving between screens, and doing some actions. He decided to refresh at some point — and now he is getting the new version of the app.
Wait, the new version? But why? index.html
is in the cache; why are we getting the new version? So this is related to the redirect we are doing when serving SPA from a server.
SPA’s handling the app navigation on the client-side, every navigation to a new route Angular router changed the URL in the address bar dynamically. When a user types in the browser address bar a route of our app, say https://some-domain.com/home,
and clicks enter, we actually don’t have this route in our server, but instead of returning 404, we are configuring the server to return our index.html
, Angular router is doing the job and directing the user to the right screen.
Now we can understand what is going on in our scenario. If we enter our app from the main URL, we are getting the old version — since we have this endpoint on the server, the browser can cache it. But if we are asking for a route, we will always get the index.html
from the server and not from the cache — so we will see the new version after refresh.
Nice, isn't it? and it’s all because of a one index.html
in cache.
How to check if index.html file
is cached
It’s pretty easy to find out if the index.html
file is configured to be cached or not.
- Open the browser Dev-tools.
- Go to the Network tab.
- Ensure the Disable cache checkbox is not marked.
- Filter to Doc.
- Refresh the screen.
- Click on the first document.
- Check the
cache-control
header.
Let's take, for example, the angular.io/docs site:
We can see that the cache-control
header is no-cache
. Which values in the cache-control
header are good for us and which are not?
no-cache
— This will cache ourindex.html
file, but tell any cache system to check if there is a newer version in the server. We are Ok with that.no-store
— This will tell any cache system not to cache theindex.html
file — also good.max-age=0
— This is also won't cache theindex.html
.max-age=31536000
— This isn't good. The value in the max-age represents seconds — ourindex.html
will be cached for a year. It’s really up to you which values are ok by you, but I think we can agree we don't want theindex.html
in the cache for a full year.
Those are the popular values for the cache-control
header; if you see something else in your response, you can check it out here.
What if we didn’t supply the cache-control header?
Well, now you are in a “no man’s land” territory.
This is a quote from w3.org section 13.2.2:
Since origin servers do not always provide explicit expiration times, HTTP caches typically assign heuristic expiration times, employing algorithms that use other header values (such as the Last-Modified time) to estimate a plausible expiration time. The HTTP/1.1 specification does not provide specific algorithms, but does impose worst-case constraints on their results. Since heuristic expiration times might compromise semantic transparency, they ought to used cautiously, and we encourage origin servers to provide explicit expiration times as much as possible.
At this point, it will be nice to mention that there are few types of cache. The cache in our browser is our private cache because it serves only us in our browser. But there is a public cache shared between users with many possibilities: proxy caches, gateway caches, CDN, reverse proxy caches, and load balancers.
So, we understood that there is no specification for caches how to handle a situation where we are not supplying specific orders how to cache our indx.html
. Basically, every cache or browser can apply its own algorithm based on some other headers we may or may not supply and still cache our index.html
. More than that, you can’t test those kinds of things. Your users may enter your app from private networks or even ISP’s that apply some cache algorithm, and you won't even know about it. This is the reason W3 recommended we provide explicit expiration times.
So make sure your index.html
comes with the right cache-control
header.
Cache trap
The real problem with cached index.html
is that users who already got the file in the cache are trapped with the cached file until one of the two will happen:
- They clear the cache manually (assuming the file is cached in the browser cache).
- The cache expiration date is reached.
You can relax; those index.html
issues are quite rare. Most of the hosting services won’t serve the static files with long cache configuration as default. But you may have the issue if you serve your Angular app from a server that already exists and serves dynamic files.
How to prevent the index.html form from being cached?
You will probably find some StackOverflow answers that suggest you try to control the cache with an HTML meta tag. You may even test it, and it may even work on your browser. But if you research a little more, you find this is not an effective way to prevent cache. Those tags may be honored by some browsers but not honored by other types of caches.
The best way is to set the cache-control
header of your index.html
from your server. Doing this required some changes to your server configuration — but this is the straightforward and most effective way to prevent cache.
I have Service Worker; I’m all good!
Sorry, but no, you don’t.
You may have noticed in the example from angular.io that the file served from the service worker — but Angular guys still send this file from the server with a cache-control=no-cache
header.
Service Workers are awesome— they give us Front-End developers full control while caching our files. But as we mentioned before, there are few cache types — Service Worker is just one of them. Service Worker is one layer your request is going through — but it’s not replacing the HTTP cache (browser cache) and, of course, not the public cache. If your Service Worker decides to get a file from the server, the browser will still check the relevant headers to decide whether to serve it from cache or not.
Conclusion
Some cache issues are pretty hard to debug and reproduce and easily mistaken as code issues. They usually come from the end-users of our app and depend on how frequently we are releasing a new version. Some users won’t even report that they have a problem and will just describe it as a “weird behavior that disappears when I refresh”.
We all want the best experience for our users — if you are not sure, then check your cache today! 😏