Do you have a vague idea of what a cache is, but want to really understand it? Want to learn how you can use caching to make your apps faster, more resilient, and even less resource-intensive for your clients? Then this article is for you.
In this article, weβre going to go through what a cache is, and what kinds of caching is relevant for most frontend developers. Weβll touch on how we can cache data in JavaScript, via service workers, the browser itself, and external caches, such as CDNs and backends. Finally, weβll look at cache invalidation, and try to get a basic understanding of both what it is and why itβs so hard to get right.
Before we dive into the many ways we can implement caching, we should look at some sort of technical definition of what a cache is. Put simply, a cache is a way to save data that you received earlier so that itβs easier to retrieve again later. Iβll explain this through an example.
Like most internet users, youβve probably downloaded a file to your computer at some point in time. Perhaps itβs a document youβre working on with a few friends from school. Since itβs now on your computer, you can access it whenever you want, without fetching a new copy every time you want to work on it. This feature β of having access to some resources in an easier (or cheaper) way is the main idea of a cache.
We see this kind of technique used in most parts of a modern tech stack. We cache photos in our browser so they show up right away on subsequent visits. We cache the user JSON object in some sort of state management library, so we donβt have to ask the server for what the userβs name is every time we want to change whatβs displayed. We even cache entire web apps in the browser so that they work without an internet connection (so-called progressive web apps or PWAs).
With all of these upsides, you might ask yourself why we donβt cache everything forever! Why even bother fetching new data if we already have it locally? Well, as it turns out, the world isnβt static, and the data we download has the potential to change in the future. Therefore, we run the risk of dealing with out of date information whenever we cache it.
Knowing what to cache, and for how long, is one of those problems that requires you to really consider the use case of each piece of information, and how important it is to reflect changes right away. Thatβs why Iβve always thought of it as an art to get right. With all that said, weβll go through some examples and give you some practical hints later on in this article.
As a frontend developer, youβll see quite a few different caching types as you progress through the stack. Hereβs a description of each βlayerβ of cache, and when it shines.
The very first cache your code will encounter is the cache you typically make yourself. That is, some sort of way to keep the data from your API in memory.
A very simple implementation of a simple cache with no invalidation (relax, weβll come back to what that means later) could be this:
let cache = {}; async function getCachedValue(key, callback) { if (cache.hasOwnProperty(key)) { return cache[key]; } const result = await callback(); cache[key] = result; return result; }
Here, we have a βglobalβ cache object, which is persisted between calls to this caching function. We check if the cache contains the cache key, and if it does, we simply return the cached value. If it doesnβt, we call the provided callback function to somehow get a value, place it in the cache and return it to the user.
You would then call this function with a key, and a callback that would asynchronously fetch the data in question:
const user = getCachedValue("user", async () => { const res = await fetch("/api/user"); return res.json(); });
Here, we would fetch the user the first time this code was called. The second time, we would have found the user in the cache, and avoided the extra call to the server.
There are tons of libraries that help with this. I write mostly React code myself, and in that ecosystem, SWR and react-query are two great arguments that implement such a cache for you (in addition to a lot of other nice-to-have features you need).
Caching is one of the most fundamental features in web browsers, and has been for decades. Thatβs why itβs built into the very protocol that transfers data from servers to users β the HTTP. Via special header fields prepended to each response, the server can instruct the browser to cache certain files for certain periods of time. In particular, itβs the Cache-Control
header you want to read into.
This caching mechanism is the one most users think about when they hear caching. Youβve probably at some point heard the term βclearing your cacheβ as a way to fix some weird bug on a website, and this is the cache they referred to.
Caching resources via HTTP is an incredible tool for improving your site. By adding the correct cache headers, and perhaps creating unique file names for all static resources, you can cache all resources indefinitely on the client-side (well, until somebody tells your user to clear their cache, that is). Even dynamic content can be cached if done carefully.
I would love to dive deeply into the HTTP caching techniques, but MDNβs resource on the matter is too comprehensive to not recommend instead. Check it out here.
Sometimes, you need the power of an HTTP cache, with the programmability of JavaScript. Thatβs where you can reach for so-called service workers. Service workers enable you (among other things) to cache all resources locally, but with full programmatic control over what gets cached when, and for how long.
Service workers act as an intermediary for all network requests. Whenever your web application requests a resource (letβs say, an image), you can intercept it, look up a cached version (or a fallback) and return it, all while youβre fetching an updated version in the background.
Combined with a simple manifest file, service workers even lets you create complete offline experiences for web sites after the original visit. This is an immensely valuable feature in a world where data coverage isnβt as universal as you might think!
Let me add a final word of caution. Since service workers are so incredibly powerful, they also come with the possibility of ruining your web site for the foreseeable future. Since they run as a separate process from the rest of your site, it will persist between one version and the next. In other words, you need to take special care to make sure you donβt screw anything up π .
Luckily, there are tools that help you create ready-made service worker caches. You can plug tools like Googleβs workbox into your build pipeline, and have one generated for you. Job done!
The last piece of the caching puzzle for frontend devs has nothing to do with the frontend at all. Instead, itβs the caching that happens on the server-side of your application.
But why do we need caching on the backend as well? The servers typically have much more resources and network stability than even the most powerful clients, so why is there a need to cache stuff? Well, as it turns out, the server also asks other services for data.
Take a database query, for instance. Scanning through a database of millions of records to find the ones relevant for a particular query might take seconds. Instead of doing this work over and over again, a backend engineer might choose to cache those queries for a bit of time. Other external services outside of our control might also be great caching opportunities.
Caching on the server-side often includes a concept called distributed caches, which complicates things quite a bit. Since youβre probably running more than one server, and a request can be directed to any one of those servers, you need to have a shared cache between them. This has become easier to set up with tools like hazelcast, but is still a stumbling block for many.
I wonβt dive into too much detail about this kind of caching, as I find it a bit out of scope for this article. But know there is a lot to learn here as well!
Sometimes, you donβt want something to be cached anymore. There are typically three good reasons for this. It might have changed, it might be too old, or it might not be used often enough.
Letβs start with removing entries that arenβt used often enough. Why would you want to be stingy about caching data thatβs seldomly used? Well, because of space. Put simply, caching is just a way to save data, and some of that data might be pretty large in terms of megabytes. At some point, depending on your systemβs configuration, you will run out of space to do this duplicate saving of data. Then, we need to somehow rank our cache entries by usefulness, and how often a cached resource is used is definitely a nice metric for usefulness. So if weβre trying to add a new entry to our cache, we need to remove the least used ones first.
There are several techniques to deciding whatβs the least useful entry though β it could be the one that has been looked up the fewest times in a given time interval or the least recently used entry. Which technique you choose is up to you and your specific requirements.
Another approach to keep cache sizes in check, while also ensuring that your data is fresh enough, is removing cache entries based on how long theyβve been in the cache. You might want to cache images longer than your user data, since images rarely change, but at some point, you probably want to fetch a new version of the image as well β just in case.
If a cached resource is requested, and the cached item is expired, a new version will be fetched instead, and the old entry will be switched out, leaving the cache fresh again.
I told you weβd get back to cache invalidation. So what is it, exactly?
Cache invalidation is the art of removing a subset of the cached data from the cache. You typically want to do this if youβre updating the data in the cache, and want your application to go fetch a new version.
Depending on where youβre doing your caching, youβll have different ways to do this as well. If youβre doing something programmatically (like in JavaScript), you can simply remove the cache entry, and request a new one in the background.
Caching is hard because caching is a lot of different things. You can cache stuff in your application, via HTTP, via service workers, and even in the backend itself. What to do when isnβt readily apparent to many, but hopefully this gave you some sort of idea of how it all works. Finally, we looked at why you would ever want to remove something from the cache, and different ways to do that, too.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocketβs Content Advisory Board. Youβll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowDiscover how the Chakra UI MCP server integrates AI into your editor, reducing context switching and accelerating development by fetching real-time documentation, component data, and code insights directly in-app.
fetch
callSkip the LangChain.js overhead: How to build a Retrieval-Augmented Generation (RAG) AI agent from scratch using just the native `fetch()` API.
Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 8th issue.
Walk through building a data enrichment workflow that moves beyond simple lead gen to become a powerful internal tool for enterprises.