Every time you visit a website, your browser downloads stuff. HTML, CSS, JavaScript, images, fonts. A typical modern webpage downloads 2-3 MB of resources across 50-80 separate files.
Now imagine you visit that same site again five minutes later. Does your browser re-download all 2-3 MB? If it did, the web would be unbearably slow. Every page revisit would feel like a first visit. Your mobile data plan would evaporate. And web servers would be crushed under traffic that's entirely redundant.
Browser is the reason the web feels fast. When you load a page, your browser stores copies of downloaded resources in a local cache on your disk. Next time you need that same CSS file or logo image, the browser just grabs it from the local copy. No network request. No waiting. Instant.
This isn't an obscure optimization. It's fundamental to how the web works. And if you're a web developer who doesn't understand browser caching, you're either making your users re-download things they already have, or, worse, serving them stale files that cause bizarre bugs.
Browser caching is controlled by headers. When your server sends a response, it can include headers that tell the browser: "Hey, you can keep this file for a while. Don't ask me for it again until it expires."
There are two main mechanisms:
Cache-Control (the modern way)
The Cache-Control header is the primary mechanism. It gives the browser explicit instructions:
| Directive | What It Does |
|---|---|
max-age=3600 | Cache this for 3600 seconds (1 hour) |
no-cache | Cache it, but revalidate with the server before using it |
no-store | Don't cache this at all. Ever. Not even temporarily. |
public | Any cache (browser, , proxy) can store this |
private | Only the user's browser can cache this, no shared caches |
immutable | This file will never change. Don't even check. |
So Cache-Control: public, max-age=31536000, immutable says: "This file can be cached anywhere, keep it for a year, and don't bother checking if it changed." You'd use this for versioned static assets like app.a3f8c2.js, the hash in the filename changes when the content changes, so the old file literally never needs to be re-fetched.
ETag + If-None-Match (conditional validation)
Sometimes you want the browser to cache a file but check with the server before using it. The server sends an ETag header, a fingerprint of the file's content. On the next request, the browser sends If-None-Match: "abc123". If the file hasn't changed, the server responds with 304 Not Modified, no body, just headers. The browser uses its cached copy. If the file changed, the server sends the new version.
This saves (304 responses are tiny) but still requires a network round-trip.
When you open DevTools and look at the Network tab, you'll see a (disk cache) or (memory cache) label on some requests. Here's what those mean:
Memory cache. Stored in RAM. Blazing fast. Gets cleared when you close the tab. Used for resources the browser expects you'll need again very soon (scripts on the current page, inline images).
Disk cache. Stored on your hard drive or SSD. Persists across tab closures and browser restarts. Slower than memory but has way more capacity. Used for larger files and files with long max-age values.
The browser decides where to store what based on size, frequency of use, and available resources. You don't control this directly as a developer, you just control whether something gets cached and for how long via headers.
How big is the browser cache?
It varies, but most browsers allocate around 50-250 MB for cache. Chrome dynamically adjusts based on available disk space. When the cache fills up, the browser evicts the least recently used entries, yes, that's the LRU algorithm we'll cover later in this category.
Browser has a dark side, and every web developer has been bitten by it at least once.
You deploy a bug fix. You tell your users "just refresh the page." They refresh. Nothing changes. The browser is serving the old JavaScript file from cache, and max-age still has 23 hours left.
This is why modern build tools add content hashes to filenames. Instead of app.js, you serve app.a3f8c2.js. When you deploy a fix, the filename changes to app.b7d1e4.js. The browser has never seen this filename before, so it downloads it fresh. The old cached file just quietly expires and gets evicted.
The golden rule of browser caching:
Cache-Control: no-cache (always revalidate, this is how users discover your new JS/CSS filenames)Cache-Control: public, max-age=31536000, immutable (cache for a year, never revalidate)Cache-Control: public, max-age=86400 (cache for a day, or longer if versioned)Cache-Control: private, max-age=0 or no-store (usually too dynamic to cache in the browser)When your browser needs a resource, it does not just blindly fetch it from the server. There is a whole decision tree happening behind the scenes: checking Cache-Control headers, comparing ETags, deciding whether to use the cached copy or revalidate. This diagram traces that decision path step by step so you can see exactly when a request hits the network and when it does not.
The step-by-step walkthrough above shows you the common scenarios. But the complete decision tree has more branches, and understanding all of them helps you set the right headers for every resource type. This flowchart shows every path the browser can take, from "is it even cached?" all the way to "download the new version."
3 questions - Score 80% to pass
You deploy a critical JavaScript bug fix, but some users still see the broken behavior. What's most likely happening?
What does Cache-Control: no-cache actually mean?
Your CSS file is served with Cache-Control: public, max-age=31536000, immutable. A user visits your site today and comes back in 6 months. What happens?
If you don't set any Cache-Control headers, the browser will use "heuristic caching", it guesses based on the Last-Modified date. That guess is often wrong. Always set explicit caching headers. Don't leave it to chance.