Caching SVG files with HTTP headers
How to cache SVG icon files with Cache-Control, immutable and stale-while-revalidate, content-hashed file names, the right content type and a CDN.
4 min read
Icons are close to the ideal case for HTTP caching: small files, requested again and again, that rarely change. With the right headers, a returning visitor loads your token list without a single icon request, and a CDN serves everyone else without touching your server. The main decision isn't which header to send, though. It's how your URLs behave when a file changes.
First question: does the URL change when the file does?
Every caching strategy starts here. Icon URLs come in two kinds.
Content-hashed URLs include a fingerprint of the file's contents, like /assets/eth.3f9a1c2b.svg. When the logo changes, the hash changes, so the new file has a new URL. Build tools generate these automatically for assets you import in code.
Stable URLs stay the same for the life of the file, like /icons/token/ETH.svg. You want these for icons people link to directly, for public icon libraries, and for logo URLs stored in a database or a token list.
With hashed URLs, you can tell browsers to keep a file forever, because any change will arrive under a new name. With stable URLs, a long lifetime means people keep seeing the old logo after a rebrand until their cached copy expires. So stable URLs need a shorter lifetime, revalidation, or both.
The directives that matter
| Directive | What it does |
|---|---|
max-age=N |
The response is fresh for N seconds; no request is needed during that time |
s-maxage=N |
Like max-age, but for shared caches such as CDNs, where it takes precedence |
public |
Any cache may store the response, including shared ones |
immutable |
The file won't change while fresh, so the browser shouldn't revalidate it, even on reload |
stale-while-revalidate=N |
After expiry, a cache may serve the stale copy for up to N more seconds while it fetches a fresh one in the background |
no-cache |
Store it, but check with the server before each use |
no-store |
Don't store it at all |
When a cached copy expires, the browser can revalidate it with a conditional request, using the ETag (If-None-Match) or the modification date (If-Modified-Since). If nothing changed, the server answers 304 Not Modified with no body. For a small icon that's nearly as cheap as a cache hit, but it still costs a round trip.
Support for immutable and stale-while-revalidate differs between browsers. Caches ignore directives they don't recognize, so including them is safe. Many CDNs also honor stale-while-revalidate; check your provider's documentation.
Header recipes
Hashed file names. Cache for a year and mark the file immutable. 31,536,000 seconds is 365 days.
Cache-Control: public, max-age=31536000, immutable
Stable icon URLs. Pick a lifetime that matches how quickly a logo change has to show up. For example: a day in the browser, a week at the CDN, and a background refresh after expiry.
Cache-Control: public, max-age=86400, s-maxage=604800, stale-while-revalidate=86400
When you ship a corrected logo, purge it from the CDN rather than waiting out the week.
Versioned stable URLs. A middle path is adding a version to the query string, such as /icons/token/ETH.svg?v=2026-11, and changing it whenever the icon set updates. That lets you use long lifetimes on readable paths. Whether a CDN treats the query string as part of the cache key depends on its configuration, so check before you rely on it.
The HTML that references the icons. Give pages a short lifetime or no-cache, so a new deploy that points at new hashed files is picked up right away. If the whole site is pre-rendered, this split between short-lived pages and long-lived assets is the main thing to get right; see static generation for icon libraries.
Content type and compression
Serve SVG with Content-Type: image/svg+xml. Browsers don't infer SVG from a file's contents: served as text/plain or application/octet-stream, an SVG typically won't display in an <img>, and opened directly it may download or show as text. Web servers usually map the .svg extension correctly by default, but files uploaded to object storage sometimes get a generic type. Check the header rather than assuming.
SVG is text, so it compresses well. Make sure gzip or Brotli is enabled for image/svg+xml, since some servers only compress a short default list of types. The gain is largest for detailed logos of several kilobytes; for files under 1 KB it's small. Making the files themselves smaller usually helps more; see optimizing SVG files.
Setting the headers
In Next.js, you can set headers for files in public/ from next.config.ts:
import type { NextConfig } from 'next'
const config: NextConfig = {
async headers() {
return [
{
source: '/icons/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=86400, s-maxage=604800, stale-while-revalidate=86400',
},
],
},
]
},
}
export default config
In nginx, whose default mime.types already maps .svg to image/svg+xml:
location /icons/ {
add_header Cache-Control "public, max-age=86400, s-maxage=604800, stale-while-revalidate=86400";
gzip on;
gzip_types image/svg+xml;
}
Hosting platforms and CDNs offer the same settings through their own config files or dashboards.
Then check what's actually served. Send an Accept-Encoding header, or the response won't show compression:
curl -sI -H 'Accept-Encoding: br, gzip' https://example.com/icons/token/ETH.svg \
| grep -iE '^(content-type|content-encoding|cache-control|etag|age):'
An Age header usually means the response came from a shared cache. Many CDNs also add their own header saying whether the request was a hit or a miss; run the command twice and compare.
A note on SVGs opened directly
An SVG opened directly in a browser tab, rather than through <img>, is a full document that can run scripts. That's not a concern for icons you drew yourself. If your library accepts uploads or proxies third-party logos, though, sanitize the files and send a restrictive Content-Security-Policy header with SVG responses. The guide on SVG security explains the risks and the headers.