DEV Community

Cover image for SPAs: Have Your Cache And Eat It Too
James Thomson
James Thomson

Posted on

SPAs: Have Your Cache And Eat It Too

So you've developed an awesome single page application and now you're ready to launch it. Sweet, good job! But hold on there buddy, before you go pulling the trigger and Tweeting your pun-tastic URL take a second to make sure you have your caching strategy in place. Otherwise you may find yourself in a world of hurt the next time you push an update.

I speak from personal experience, and here's what I learned...

Before I dive in, let me precursor this with: I'm not going to get into the nitty gritty on how to set everything up. Each dev environment is different and requires different steps. What I want to get across is the importance of setting things up properly before you go live because if you don't, it can be very difficult to dig your users out of a stale cache hell hole.

TL;DR

  • NEVER cache your index. Set headers to no-store with expires 0.
  • Use a bundler (e.g. Webpack) to implement hashed versioned file names.
  • Use no-cache and max-age headers on your JS, CSS, and any other files that change frequently.

Never Cache The Index

Set your index headers to no-store with expires 0. This ensures your users get a fresh index every time they launch the app. This is VERY important as your index references all your other files (js, css, etc.) and we want to make sure the correct hashed files are served.

Use Hashed (Fingerprinted) Filenames

When done correctly, this step should eliminate pretty much all your caching woes. The strategy being when you change something in your file the bundler will also fingerprint the filename by renaming it with a hash (e.g. app.jgm315la0.js). Because the filename is different and because the index is never cached (remember what I said in the paragraph above?) the latest file will be used.

Set Your Headers

Last, but certainly not least, set your headers for the rest if your files. Using the Cache-Control response headers you can define how you want a users browser to handle caching. This step is important because if the headers aren't set correctly then your users could potentially end up with stale files...forever! Or at least until they clear their cache, which not a lot of users will know to do. By setting the correct headers you ensure that your users browser will behave as you instruct it to.

Now, there is no hard and fast rule here. Each SPA is different so it's up to you how you handle your Cache-Control headers. This may be overkill, especially on hashed files because they should be cache-busted when the filename changes, but to start I'd recommended setting no-cache and max-age on all your files (apart from index). I say this because you can set them and then change/remove them later when you confirm your hashed files work as you expect, but you can't go the other way if it isn't working as expected and the browser hangs on to that cached version for dear life.

Contrary to its name, no-cache (which sounds like it should never cache), instructs the browser to always ask the server if the file has changed. If it hasn't, it will use the browsers cached version. This has the unfortunate effect of sending a request, albeit a very very small one, but ensures the browser always checks to see if it should use a cached version rather than just assuming it should always use the cached version.

max-age gives the file an "expiry" date. When the file is downloaded for the first time it is given this maximum age. Once that age comes about, the file is considered stale and will be downloaded again.

When in doubt about how to approach headers for a certain filetype, reference Google's Cache-Control policy decision tree.

Conclusion

So there you have it. This is by no means a definitive guide and there's a lot to be learned about caching, but hopefully it helps you avoid a painful production experience I just went through. Most importantly, if you follow these initial steps you should be able to easily tweak your headers later and your users won't know any better, but if you don't, they will know all too well.

Happy coding!

Discussion (10)

Collapse
avxkim profile image
Alexander Kim

Thanks for the article, i missed these headers in my nginx config, in case someone wonders, on where to put them, here's an example in http block:

location / {
    root /path/to/your/built/files;
    try_files $uri $uri/ /index.html;
    add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate';
    expires 0;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
jamesthomson profile image
James Thomson Author

Happy the article helped you. The only thing I'd note here is if you're using the no-store flag then no-cache, must-revalidate, proxy-revalidate is redundant as no-store will tell the browser to flat out never cache the file.

By contrast, "no-store" is much simpler. It simply disallows the browser and all intermediate caches from storing any version of the returned response—for example, one containing private personal or banking data. Every time the user requests this asset, a request is sent to the server and a full response is downloaded.

Source: developers.google.com/web/fundamen...

Collapse
sherlock1982 profile image
Nikolai Orekhov • Edited on

I don't really get why to use "no-store" even. You can use "no-cache" and it will have forced revalidation on each request returning 304 most of the time if your index.html was not changed.
It also applies to any static files that doesn't have a "hashed" names for various reasons.

Thread Thread
chipit24 profile image
Robert Komaromi • Edited on

I agree, no need to use no-store here as long as your server implements validation via etag or last-modified. Dev.to, for example, sets the following headers:

cache-control: public, no-cache
etag: W/"495a374b731c7d2a6d8758814988907f"
Enter fullscreen mode Exit fullscreen mode
Collapse
anduser96 profile image
Andrei Gatej

Thank you for sharing!

One thing I didn’t fully understand is why do the users need to get a fresh index every time they launch the app. Could you please elaborate a little bit on that?

Collapse
jamesthomson profile image
James Thomson Author

Hey Andrei, the reason for a fresh index is because it contains the references to your files. E.g. app.ahdj57nl.js, styles.afni47hh.css, etc.

If the index is cached you would end up with references to old files that potentially don’t exist anymore.

Collapse
anduser96 profile image
Andrei Gatej

It makes sense now. So to make sure that I understood, the server is who sets the cache headers for other files(*.js, *.css etc). Whereas the meta tags are used to set the headers on the index file.
Did I get it right?

Thread Thread
jamesthomson profile image
James Thomson Author

Correct on the first part, but meta isn’t needed for the index. You set control-headers for the index as well.

Thread Thread
anduser96 profile image
Andrei Gatej

Ok. Thanks!

Collapse
mecampbellsoup profile image
Matt Campbell

Otherwise you may find yourself in a world of hurt the next time you push an update.

Could you please elaborate?