Caching for the second visit
When someone returns to your site, their browser will attempt to reuse what it already has in its HTTP cache. How well that works depends on caching rules that date back to 1999 and remain loosely specified—whether a stylesheet or image comes from the network or the cache can feel unpredictable without explicit configuration.
A sensible modern default is to do no caching at all and rely on a CDN for low-latency validation. But that's a starting point, not the whole strategy. Here's what to consider.
The two goals of repeat visits
For any second load, you're balancing two objectives:
- Deliver the latest version of your content—if something changed, users should see it promptly.
- Achieve that with as few network requests as possible.
In practice, that means shipping the smallest possible change on each return visit. The way your site is structured determines how efficiently those changes propagate—see the companion video for a deeper look at code splitting and release coordination.
You have other options beyond the network-vs-cache spectrum. For an app-like experience, a service worker can serve content entirely offline and check for updates later. Cache-only and network-only extremes are both valid in specific contexts, but most sites don't need to live at either end.
Why "stale cache" happens
Developers instinctively reach for a hard refresh, an incognito window, or DevTools to escape a stale cache. Regular users can't do any of that, so it's your responsibility to make sure they never get stuck with outdated assets.
The root cause is often the original HTTP caching default, which relies on the Last-Modified header:
That rule was well-intentioned, but it interacts badly with modern releases. If your JavaScript deploys on Tuesday and your CSS on Friday, users can end up with files from different releases mixed together—simply because the two files were cached under different lifetimes.
The no-cache default
A straightforward modern baseline is to skip caching entirely and let a CDN serve requests from somewhere geographically close to each user. Each visit triggers a network round trip to confirm freshness, but that request is cheap when nothing changed—just a small 304 response instead of the full payload.
The cost is latency: users still wait for a network response before anything can be reused. That works well on fast connections with good CDN coverage but can degrade the experience for users on slower mobile networks.
Configure your host to return:
Cache-Control: max-age=0,must-revalidate,public
That tells the browser the file is immediately stale and must be revalidated before reuse—anything more is only a suggestion. Netlify adopts this as its default; on Firebase Hosting, add the header in the hosting section of firebase.json:
"headers": [
// Be sure to put this last, to not override other headers
{
"source": "**",
"headers": [ {
"key": "Cache-Control",
"value": "max-age=0,must-revalidate,public"
}
}
]
Files that can be cached forever
Assets whose filenames include a content hash—like sitecode.af12de.js—are guaranteed to change only when their contents change. When users request those files, it's safe to tell their browsers to keep them for a year:
Cache-Control: max-age=31536000,immutable
Per the spec, that's effectively forever. Don't generate these hashes by hand; Webpack, Rollup and similar tools handle it for you.
This applies well beyond JavaScript: icons, CSS, and any immutable data file can carry a content hash in its URL.
Human-facing pages like index.html obviously can't be renamed for every release. That's where a middle ground comes in.
Time-limited caching for the rest
Some resources warrant a limited cache window—say, an hour (3600 seconds):
Cache-Control: max-age=3600,immutable,public
Before applying that to HTML, consider its dependencies. A page including a lazily loaded image:
<img src="https://web.dev/images/foo.jpeg" />
If that image changes or is removed, users with cached HTML will still reference the original /images/foo.jpeg and may get a missing asset. When your content is cached in pieces across users' browsers, your site no longer exists only on your server.
For resources that are safe to cache temporarily, look for ones that don't affect how other files are interpreted:
- Large images used in timely articles—readers rarely revisit a single post, so holding onto a hero image forever just wastes storage.
- Assets with their own natural lifetime, like hourly weather JSON or a build-status image that can only change on a published schedule.
Behavioral CSS is a poor candidate, since it alters how your HTML renders.
And for pages users typically visit once—news articles, for example—stick with the no-cache default. The value of caching rarely outweighs a user's expectation to see the latest update on a developing story.
Pick a default and opt in deliberately
A second visit is a sign of user investment—don't waste it. The right caching strategy keeps that load fast without serving outdated content. Start with the no-cache baseline, apply immutability to fingerprinted assets, and reach for time-limited caching only where you've thought through the dependencies. Whether that means an extra network request or a year-long cache, make the choice explicit rather than leaving it to 1999-era defaults.



