Discuss your task

Search

Start typing to search articles, cases, and services.

navigate Esc close

What Actually Speeds Up a Static Site - CDN or Browser Cache

A CDN and the browser cache speed up a site in different ways and solve different problems: a CDN shortens the distance a request has to travel to reach the server, while the browser cache removes the network request entirely. On a static site built with Astro or Next.js, both mechanisms take almost no effort to configure, as long as you know which headers to set for which files. Below are concrete Cache-Control values for different file types, plus a breakdown of what your host handles for you and what you still have to configure by hand.

According to the CDN chapter of the 2025 Web Almanac from HTTP Archive, 35% of HTML content on the web is now served through a CDN, up from 33% a year earlier, and the growth is driven mainly by the free or built-in CDN options bundled with modern hosting. In practice a site almost always already has a CDN in front of it once it’s deployed on Vercel, Netlify, or Cloudflare Pages - the real question is whether the caching behind that CDN is configured to actually speed up repeat visits.

In audits of Astro and Next.js sites, we consistently see the same mistake: the team moves to a modern host with a CDN out of the box, PageSpeed jumps once at launch, and attention to caching stops right there. HTML gets served with the same headers as hashed JS bundles, or the other way around - hashed files get cached for five minutes as if they were dynamic content. Below is a breakdown of what in this configuration actually saves seconds, and what’s just the same mistake copied across different domains.

The cost of a wrong configuration isn’t abstract. Every repeat visit without proper caching means extra hundreds of milliseconds before first render and extra load on the origin server, and after a deploy a user can keep seeing an old version of the homepage for weeks because the HTML got cached for a month by oversight.

CDN (Content Delivery Network): a network of geographically distributed servers that stores copies of a site’s static files closer to the user and serves them without contacting the origin server.

Browser cache (HTTP cache): local storage on the user’s device that the browser uses instead of making another network request, as long as the response headers allow it.

How CDN caching differs from browser caching

A CDN and the browser cache both cache the same file, but at different stages of the request path, and confusing the two causes half of all caching misconfigurations.

Edge caching is a copy of a file stored on a CDN server physically closer to the user than the origin server. The first request from a region travels to the origin and populates the edge cache; every subsequent request from that region gets the file from the nearest server - the savings come from round-trip time (RTT), not from eliminating the request itself: the user’s browser still makes a network request, just a shorter one.

The browser cache removes the network request entirely. Response headers - Cache-Control, ETag, Last-Modified - tell the browser whether it can use the local copy of a file without contacting the network at all, and for how long. Those headers are what decide the behavior: a CDN delivers a file faster, but it’s the headers that decide whether the browser needs to ask for it again in the first place.

stale-while-revalidate: a Cache-Control directive that lets the browser or CDN serve a stale copy of a file instantly while revalidating it in the background for the next request - the user never waits on cache validation, but also never gets stuck on an old version for long. The mechanics are covered in detail in Jake Archibald’s article on web.dev.

What you can cache for a long time on a static site, and what you can’t

Static site generation (SSG) produces two fundamentally different types of files, and the caching rule for each is the opposite of the other.

An Astro or Next.js build adds a content hash to every JS and CSS filename - app.4f2c9f1.js, styles.7890ab.css. As long as the file’s content doesn’t change, the hash - and therefore the URL - stays the same; any code change generates a new hash and a new URL. That means a given URL physically cannot change, so it can be cached for as long as possible with no need to ever revalidate it.

HTML documents work differently. The page at /blog/post/ always has the same URL, but its content changes with every deploy - new text, new links to rebuilt JS bundles with new hashes. If you cache HTML as aggressively as hashed files, users keep getting the old HTML page after a deploy, one that links to build files already removed from the CDN - and the site either shows stale content or, worse, returns a 404 for a bundle that no longer exists.

Images sit somewhere in between: if the build pipeline hashes their filenames too, the same rule applies as for JS/CSS. If images live in public/ without a hash - as is typically the case with hero images uploaded through a CMS - they need a moderate cache with revalidation, not a permanent immutable one. For how to choose the format and size of the images themselves, so they aren’t a bottleneck before caching even enters the picture, see the article on image optimization for LCP.

Which Cache-Control headers to actually set

Concrete values matter more than general advice like “cache your static assets longer” - here’s a table you can apply directly on a real project.

File typeHeaderWhy
Hashed JS/CSS from the buildCache-Control: public, max-age=31536000, immutableThe URL changes whenever content changes, so a year-long cache is safe, and immutable removes unnecessary revalidation requests
HTML documentsCache-Control: public, max-age=0, must-revalidate or max-age=60, stale-while-revalidate=86400Content changes with every deploy; a short TTL or SWR prevents a stale page from sticking around too long
Images without a hash in the filenameCache-Control: public, max-age=2592000, stale-while-revalidate=604800These change rarely, but not never - 30 days with background revalidation balances speed and freshness
FontsCache-Control: public, max-age=31536000, immutableA font file is never edited in place; an update always produces a new file
JSON/API responses fetched client-sideCache-Control: no-store or a short max-age scoped to the specific caseDynamic data needs its own strategy - there’s no single universal rule
How to check your site's current cache headers
curl -I https://example.com/assets/app.4f2c9f1.js
curl -I https://example.com/blog/post/

In the response, look at the cache-control, etag, and age lines - the last one shows how many seconds the file has already been served from the CDN cache without reaching the origin.

ETag and Last-Modified work as a fallback mechanism for files with a shorter max-age: when the local copy is stale, the browser sends a conditional request with that value, and the server responds 304 Not Modified without resending the file if the content hasn’t changed - saving bandwidth even under frequent revalidation.

What modern platforms handle for you, and what they don’t

Deploy platforms for Astro and Next.js automate most of this configuration, but not all of it.

Vercel, Netlify, and Cloudflare Pages serve build output through their own CDN by default and automatically set an immutable cache on hashed JS/CSS/font files - that part of the table above almost never needs manual configuration. A comparison of how each of the three platforms is built at the edge-infrastructure and deployment level is covered in the article on Vercel vs Netlify vs Cloudflare Pages.

What platforms don’t solve for you: caching for images loaded dynamically through a CMS or API rather than generated statically during the build - these need headers set manually through host configuration (vercel.json, netlify.toml, Cloudflare Page Rules) or through the platform’s image optimizer. Cache invalidation on partial content updates is also a manual zone: if a site uses incremental static regeneration (ISR) or dynamic API routes on top of a static build, you have to explicitly decide what happens to old edge copies when the underlying data updates - the platform can’t guess that for you.

Common caching mistakes that noticeably slow things down

Most caching problems on static sites come down to a handful of recurring configurations.

The most common one is caching HTML for too long. A team sets max-age=31536000 once for the whole site through a blanket rule in the host configuration, without separating file types - and after the next deploy, some users keep seeing the old version of the page for weeks, because both the browser and the CDN still consider it valid.

The opposite mistake is not using immutable on hashed files, leaving the host’s default short cache of a couple of hours in place. This doesn’t technically break the site, but every repeat visit forces the browser to re-download files whose content physically could not have changed - pure loss of speed for no reason.

A separate category is missing compression. Caching speeds up delivery of a file that’s already been transferred, but it doesn’t shrink the file itself: without Brotli or Gzip on the origin server, the CDN caches and serves the uncompressed version, and part of the gain from edge caching gets eaten by the extra weight of every response. Most modern deploy platforms enable compression by default, but on self-hosted setups behind a separate CDN, this needs to be checked explicitly.

And last - a site’s own caching strategy can’t do anything about third-party domains. Chat widgets, analytics, and pixels load from servers you don’t control, on their own caching schedule, and even a perfectly configured Cache-Control for your own files won’t speed up a domain that isn’t yours. We covered the mechanics of this problem and ways around it in the article on third-party scripts and site speed.

A real case - what changes after configuration

On a typical project migrating a B2B marketing site from WordPress to Astro, the host was initially left on Cloudflare Pages defaults, with no manual configuration for content images loaded through the headless CMS.

After a cache audit, three things changed: hashed JS/CSS was explicitly set to immutable instead of the default one-hour TTL, HTML was set to stale-while-revalidate=86400 instead of the permanent cache inherited from an old WordPress-era rule, and CMS images without a hash in the filename got max-age=2592000 with revalidation instead of the default no-cache.

Based on a month of CrUX data, repeat visits sped up noticeably: TTFB for returning users dropped from 380 to 40 milliseconds thanks to the browser cache on static assets, and LCP on repeat visits went from 1.8 to 0.9 seconds. First visits from new users barely changed - by definition, the browser cache doesn’t help someone landing on the site for the first time - the entire gain came from repeat visits and navigation between pages. For what else gets lost, and what to double-check, during the migration itself, see the article on migrating from WordPress to Astro.

Who this matters most for

Proper cache configuration matters most on sites with a high share of repeat visits - blogs, documentation, marketing sites for SaaS products, where users come back for new content or to compare pricing before buying. On single-page landing pages driving cold paid traffic, the effect from caching is smaller, because every visit is a first visit, and optimizing the first request itself matters more there. Beyond caching, diagnosing what else is slowing down LCP, CLS, and INP is exactly what our website speed optimization and Core Web Vitals audit covers.

This also matters for teams that recently migrated from WordPress or a site builder to a static stack: old caching rules inherited from the previous host often carry over as-is and don’t match the logic of hashed Astro or Next.js builds.

Frequently asked questions

Do you need a separate CDN if the site is already hosted on Vercel or Netlify

No, you don’t need to add a separate CDN - both platforms already serve build output through their own edge network. A separate external CDN only makes sense for specific needs: custom edge logic outside the platform’s ecosystem, or multi-cloud redundancy, which is overkill for most marketing sites.

What happens if you set immutable on a file that can change

The browser and CDN will keep serving the cached version for the entire max-age period, even if a new file already exists on the server at the same URL - the user won’t see the change until the cache expires naturally. That’s exactly why immutable is only safe for files with a hash in the filename, where a content change is guaranteed to produce a new URL.

How to avoid users seeing an old version of the site after a deploy

HTML shouldn’t be cached for long periods - a short max-age or stale-while-revalidate with a window of a few hours guarantees that even a cached version of the page refreshes within a reasonable time after a deploy. Hashed JS/CSS never creates this problem in the first place, because old versions keep existing at their old URLs until the next build.

Does a CDN speed up a site with few visitors

Yes, although the effect from geographic distribution will be less noticeable if the audience is concentrated in one region. But modern deploy platforms bundle a CDN for free with hosting, so the question usually isn’t whether to enable it, only whether the cache headers on top of it are configured correctly.

How ETag differs from Cache-Control in purpose

Cache-Control determines whether the browser needs to contact the server at all before using the local copy. ETag comes into play after that contact has already happened: it’s a version identifier that the server compares against the client’s version and responds with 304 Not Modified, without resending the content, if the file hasn’t changed. For files with a long max-age and immutable, ETag barely gets used at all - no request ever reaches the server that could check it.

What to do first:

  • Check the Cache-Control headers via curl -I for three file types - a hashed JS file, an HTML page, and an image without a hash
  • Confirm that hashed build files are served with immutable, not the host’s default short cache
  • Shorten the HTML cache to stale-while-revalidate instead of a permanent value, if that was set by oversight
  • Confirm that Brotli or Gzip compression is actually enabled at the server level, not just theoretically available

If your site already has a CDN in place and repeat visits still feel slow, describe the problem to the Exceltic.dev team. We’ll check your current cache headers for each file type and configure them for your specific stack. This is part of the site development and optimization work Exceltic.dev does.

More articles

All
Get in touch your way
WhatsApp Telegram

Before you go: a free estimate

Describe your task in a few words and we propose a solution, a stack and a quote with timelines.

Three areas with one team: software, web development and CRM implementation. Over 100 projects, and the project is run directly by an engineer, in Russian and English.