Why do we need to do cache busting ?
Static file gets cached and can be stored for a long period of time before it ends up expiring. So in order to get the latest features and fixes we need to remove the cache so that browsers can get the latest updates.
Additionally, have you ever felt web application like soundcloud
, facebook
etc..., asking to reload or upgrade after deployment, no right ? how the hell are they doing the updates ?
Making sure we have cache invalidated in CDN
Cache busting solves the CDN caching issue by using a unique file version identifier.
As discussed in the previous article we used no-cache
policies on some files to bust the cache using unique files names. Hence we are sure that cloudfront
will always keep the updated files..
Lets bust the browser cache
So today will cover what is the one of the best approach to seamlessly bust the cache of the frontend application in the browsers when a deployment is done, without the user feeling the app was upgraded...
The trick
The trick is we keep updating the version of the application in meta
file, which never gets cached... and to seamlessly upgrade, we perform a reload on the route change so that user would feel as if they are redirecting to a different view, but in our case we are actually cache busting our application to get the new update from the build
we deployed.
Let's dig in to see how its possible.
How to check if a new build is generated
To know when we have a new build in browsers we keep two version of the application.
-
package.json
version -
meta.json
version
What are these version and how we manage it
Here is the command prebuild
that runs before every build
to manage both versions as shown below:
"release": "HUSKY=0 standard-version",
"prebuild": "npm run release && node ./build-version",
package.json
version is kept and maintain using tools like Semantic versioning or Standard release which upgrades the package version after every deployment. Here we are usingstandard-version
to always get ourpackage.json
upgraded.meta.json
version is created inpublic
folder using a scriptbuild-version.js
we wrote to make sure we always get a latest version after deployment.
build-version.js
:
const fs = require('fs');
const { version } = require('./package.json');
fs.writeFile('./public/meta.json', JSON.stringify({ version }), 'utf8', (error) => {
if (error) {
console.error('Error occurred on generating meta.json:', error);
return;
}
// eslint-disable-next-line no-console
console.info(`meta.json updated with latest version: ${version}`);
});
The above scripts takes the latest version
from package.json
which was upgraded using npm run release
and save it to meta.json
using fs.writeFile.
Here is how the output of the above script will look like:
meta.json
:
{ "version": "108.0.0" }
Before we proceed to the next step, let me inform you that we are using the following frameworks in our app:
- Reactjs: react
- Routing: react-router-dom
Code to check application is upgraded
We created a hook that could be placed in a suitable position in your application, preferably on layouts/routes:
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { version } from '../../../package.json';
const useCacheBuster = () => {
const location = useLocation();
const parseVersion = (str) => +str.replace(/\D/g, '');
useEffect(() => {
fetch(`/meta.json?v=${+new Date()}`, { cache: 'no cache' })
.then((response) => response.json())
.then((meta) => {
if (meta?.version) {
const metaVersion = parseVersion(meta.version);
const packageVersion = parseVersion(version);
if (packageVersion < metaVersion) {
if (window?.location?.reload) {
window.location.reload();
}
}
}
})
.catch((error) => {
console.error('something went wrong fetching meta.json', error);
});
}, [location]);
return null;
};
export default useCacheBuster;
The hooks above is doing the following:
-
useEffect having a
deps
oflocation
, which runs on every change of the route. -
parseVersion
is a pure function that can format the version like"1.0.5"
into a number105
, so we can compare the versions. - On changing the app route, the hook fires and fetches
/meta.json
files from the root of the app, important thing to note here is we are passing a date param: and cache, to make sure this file never returns the cached content on fetching. - Using the response of
meta.json
we are checking ifpackageVersion
is less thanmetaVersion
, which means the new build has deployed and the browser is using the old cached build, so the app needs to reload. - If the above condition is
true
then reload it!.
NOTE: if you are using a CDN then you need to cache bust in CDN by adding the meta.json
to behaviours as shown here
P.S: we can optimize the fetching of meta, by conditionalizing it on certain routes rather then all.
That's it folks..., all you need to perform cache bust in browser programmatically.
Conclusion
The solution above is useful for the scenarios when you have a frequent deployment to production.
Moreover, in my understanding for apps used in webview or apps saved on homepage could also be similarly busted with different reload methods...
Please feel free to comment on the approach, would love to hear your feedback on this.
Regards.
Top comments (16)
What about running a script when you publish that takes all referred files (js, CSS, etc.) And renames them to be
filename.hashOfContents.extension
? Make a single change and the cache is automatically invalidated.That will work to invalidate cache in your CDN, but what about the users who are using the app and have cached the files in browser.. You can read more about it in the first series of this blog :)
They won't get an update until they manually refresh or close the browser. (which most of users don't do, as they prefer to suspend the system and continue from where they left in the browser)
My bad! I left out a final piece: don't cache your HTML for longer than maybe a day. Either it is an SPA and the bulk of the data is in JavaScript/APIs or it's a SSR page and you probably want them to see the latest data. I'd personally use a ServiceWorker to show the old cached page if the new page takes more than 1-2s to load.
The method I mentioned above has the benefit of serving the correct JS/CSS for each version of the page. Say I end up using a cached page: my JS and CSS is for that version of the page not the latest version of it. Obviously you need to keep the last n versions for it to work.
Agreed, there's always more than one ways to do it.
Service workers are great, but they are not supported widely in all browser..
But it's great do control your cache.. would love to see your implementation if you can share, so that we can learn.
interesting approach. The disadvantage is of course the loading of meta.json. Here you consume a request.
Yeah agreed, that's a drawback..
In my live app I have optimized to check meta file only on certain routes to avoid hitting meta file on every route change..
Moreover, in my understanding hitting in on the same domain file, which would hardly few bytes is no harm. Considering if you have at least one deployment to production in a week..
The bytes are not necessarily the bottleneck, but the connection. In the worst case you have a complete roundtrip. Of course it depends on the protocol.
RFC contains a few details:
w3.org/Protocols/rfc2616/rfc2616-s...
But that is already complaining on a high level.
For many, your solution should be a viable way.
Hey thanks for sharing the link..
Yeah the complain is on the spot.
A GitHub repo with all the code used in the series will be great
That's a good idea, will post the repo for it
Thanks
I thought webpack has functionality to do this for you?
Umm, I believe what you referring to is unique file names that webpack generates ?
Would love to learn how webpack breaks browser cache too ?
From my high level understanding, you use webpack to generate unique files names for all css and js dependencies, but rely on http cache headers to force browser to reload index.html periodically. (I could be wrong in my understanding). It's not as instant as your solution, since it relies on browser rather than interactively triggering reload directly inside of the react app. I would use your solution for thousands of user. I would use something simpler for hundreds of users.
It consist of two things:
This solution causes the app to be stuck in an infinite loop as it keeps on refreshing because
packageVersion < metaVersion
would be true always.No it won't be true always if you have set header's of meta.json file to have not cache using CloudFront policies.
Or manually settings headers for this to for
no-store
.In this case it will always be a new version from next deployment.
Let me know if I was able to help ?