
Cache and Performance
Performance optimisation is one of those topics where the advice is simultaneously everywhere and mostly wrong. Generic recommendations to "minify your CSS" and "compress your images" are the equivalent of telling someone to "eat healthy" — technically correct and practically useless without specifics. This guide covers the caching and performance decisions that actually matter for static sites served through CDNs: asset fingerprinting, cache header configuration, image delivery strategy, critical rendering path management, and the measurement practices that tell you whether your optimisations are working. It is written for developers deploying to platforms like Cloudflare Pages, Netlify, or Vercel who need their sites to load fast on real devices over real networks.
We have optimised site performance across dozens of production deployments, measured with real-user monitoring data and lab tools alike. The recommendations here are the ones that consistently produce measurable improvements — not the ones that sound impressive in conference talks but do not move the needle in practice.
Caching Fundamentals
A cache stores a copy of a resource so subsequent requests can be served without regenerating or re-downloading it. For static sites, three cache layers matter:
- Browser cache — stores resources on the user's device based on response headers
- CDN cache — stores resources at edge locations close to users
- Build cache — stores intermediate build artifacts to speed up deployments
The most impactful layer for user-facing performance is the CDN cache. When a resource is cached at the CDN edge, the response comes from a server geographically close to the user, with zero origin server processing. The difference between a CDN cache hit and a cache miss can be 200ms+ on intercontinental requests.
The Cloudflare caching documentation provides authoritative detail on how CDN caching works, including cache key construction, purge mechanisms, and the interaction between origin headers and CDN behavior. Understanding these mechanics prevents the cache configuration mistakes that result in either serving stale content or failing to cache at all.
Asset Fingerprinting
Asset fingerprinting — appending a content hash to filenames — is the foundation of efficient caching. A file named main.a7b3c9d2.css can be cached indefinitely because the filename changes whenever the content changes. The browser never serves a stale version because the new filename triggers a fresh request.
Gatsby and most modern static site generators handle this automatically for JavaScript and CSS bundles. The key is configuring headers that take advantage of it:
# Hashed assets — cache for one year
/static/*
Cache-Control: public, max-age=31536000, immutable
# HTML pages — revalidate every time
/*.html
Cache-Control: public, max-age=0, must-revalidate
The immutable directive tells the browser not to even check if the resource has changed — it trusts the cache completely. This eliminates conditional requests (304 responses) that still cost a round trip even when the resource has not changed.
Image Optimisation
Images are typically 50–80% of a page's total weight. The optimisation priority order:
-
Do not load images that are not visible. Lazy loading (
loading="lazy") on below-the-fold images is the single highest-impact image optimisation. It costs nothing to implement and eliminates potentially megabytes of unnecessary initial payload. -
Serve correctly sized images. A 1600px-wide hero image loaded on a 375px phone screen wastes 75%+ of the downloaded bytes. Use
srcsetandsizesattributes to let the browser choose the appropriate size. -
Compress aggressively. JPEG quality of 75–80 is visually indistinguishable from 100 in most photographic content at web resolution. The file size difference is typically 60–70%.
-
Declare dimensions. Always include
widthandheightattributes on images. This lets the browser reserve space before the image loads, preventing Cumulative Layout Shift (CLS).
Critical Rendering Path
The critical rendering path is the sequence of steps the browser takes to render the first frame of a page. For static sites, the main concern is blocking resources — CSS and JavaScript that must be downloaded and processed before anything appears on screen.
Strategies that consistently work:
- Inline critical CSS. The CSS needed for above-the-fold content can be inlined in the
<head>, eliminating a render-blocking request. Gatsby plugins can automate this. - Defer non-critical JavaScript. Use
deferorasyncon script tags that do not affect initial rendering. Gatsby handles this well by default. - Preload key resources. If the browser needs a font or hero image to render the first frame,
<link rel="preload">tells it to start downloading immediately rather than waiting to discover the resource through CSS parsing.
Checklist
- [ ] Hashed assets have
Cache-Control: public, max-age=31536000, immutable - [ ] HTML pages have
Cache-Control: public, max-age=0, must-revalidate - [ ] Images have
Cache-Control: public, max-age=86400(or longer if fingerprinted) - [ ] All below-the-fold images have
loading="lazy" - [ ] All images have
widthandheightattributes - [ ] Hero images use
srcsetwith multiple resolutions - [ ] JPEG compression is 75–80 quality
- [ ] No render-blocking JavaScript in the document head
- [ ] Fonts use
font-display: swaporfont-display: optional - [ ] Lighthouse performance score measured on simulated mobile (not desktop)
Field-Tested Observations
The biggest performance gains come from not loading things. Every optimisation technique for making resources smaller or faster is dwarfed by the impact of simply not loading unnecessary resources. Audit what loads on each page. Remove what is not needed. This consistently produces larger improvements than any compression or caching configuration.
Real-user performance varies more than lab tests suggest. Lighthouse on your development machine produces scores that bear little resemblance to what a user on a three-year-old Android phone on a 3G connection experiences. We use both lab tools (for consistent benchmarking) and real-user monitoring (for understanding actual user experience). The gap between them is always surprising.
Cache invalidation problems are deployment problems. The classic "hard problem" of cache invalidation is largely solved by asset fingerprinting. Where it resurfaces is in deployment processes that do not properly purge CDN caches or that deploy assets before HTML. Ensure your deployment process is atomic — assets first, then HTML that references them.
Third-party scripts destroy performance budgets. A single analytics or chat widget can add 200KB+ of JavaScript and dozens of network requests. We have seen sites where removing one third-party script improved Time to Interactive by 3 seconds. Audit third-party impact separately from first-party optimisation.
Common Pitfalls
-
Setting long cache times on non-fingerprinted assets. If your CSS file is
styles.css(no hash), a year-long cache means users get stale styles until the cache expires. Only use long cache times with fingerprinted filenames. -
Lazy loading above-the-fold images. The hero image should load immediately.
loading="lazy"on above-the-fold images delays the Largest Contentful Paint (LCP) because the browser waits for layout before initiating the download. -
Optimising for Lighthouse instead of users. A perfect Lighthouse score on desktop does not mean the site is fast. Test on real mobile devices with throttled connections.
-
Ignoring Time to First Byte (TTFB). For static sites on CDNs, TTFB should be under 200ms for cached responses. If it is not, the problem is CDN configuration, not your code.
FAQ
How long should I cache HTML pages?
For static sites that deploy frequently, max-age=0, must-revalidate ensures users always get the latest version while still allowing CDN caching with proper purge on deploy. If your content changes rarely, max-age=3600 (one hour) is a reasonable middle ground.
Is WebP better than JPEG?
WebP produces smaller files at equivalent quality — typically 25–30% smaller. However, JPEG has universal browser support and excellent tooling. If your build pipeline supports WebP generation with JPEG fallback, use it. If adding WebP complicates your workflow significantly, well-compressed JPEG is fine.
Should I use a service worker for caching?
For static content sites, a service worker provides offline access and can improve repeat visit performance. However, it adds complexity and creates cache invalidation challenges. We use service workers for application-like experiences and skip them for content-focused sites.
What is a good LCP target?
Under 2.5 seconds for the 75th percentile of page loads, measured with real-user data. This is Google's "Good" threshold for Core Web Vitals. Under 1.5 seconds is excellent and achievable for well-optimised static sites on CDNs.
How do I measure cache hit rates?
Most CDN providers offer analytics dashboards showing cache hit ratios. Cloudflare's analytics, for example, show the percentage of requests served from cache versus origin. A well-configured static site should achieve 90%+ cache hit rates after warm-up.
Related Reading
- Typography and Readability — font loading performance and web font strategies
- SVG and UI Icons — lightweight icon delivery
- WordPress Theme Structure — performance considerations in theme architecture