Static assets — CSS, JavaScript, fonts, and images — account for 80–90% of HTTP requests on modern web properties. Unlike dynamic HTML, these files are ideal candidates for edge caching, yet misconfigured headers, uncompressed payloads, and poor versioning leave 40–60% of potential performance gains untapped.
This guide provides a practical, asset-by-asset framework for optimizing static delivery through a CDN. You’ll learn how to configure cache control, compression, and protocols for each content type to reduce Time to First Byte (TTFB), Largest Contentful Paint (LCP), and Cumulative Layout Shift (CLS) without sacrificing cache hit ratio or deploy safety.
1. Why Static Assets Need a Dedicated CDN Strategy
Static assets are deterministic, highly cacheable, and bandwidth-intensive. A single unoptimized 280 KB render-blocking CSS file or a 1.2 MB hero image can dominate LCP. A CDN moves these bytes within 20–50 ms of users globally, offloads 85–95% of origin traffic, and enables protocol optimizations that origin servers rarely handle efficiently.
However, treating all static files identically creates problems. CSS and JS benefit from aggressive compression and HTTP/2 multiplexing. Fonts require CORS and careful display strategies. Images need content negotiation and responsive variants. A unified “cache everything for 1 hour” policy under-caches immutable files and over-caches mutable ones.
An effective strategy aligns with your CDN architecture: origin sets authoritative Cache-Control, the edge enforces tiered caching and compression, and the client leverages long-lived browser cache plus preload hints. When combined with proper SSL/TLS configuration, this reduces TLS negotiation overhead for every static request.
2. CSS Delivery Optimization
CSS is render-blocking by default. Every kilobyte and millisecond counts toward First Contentful Paint (FCP).
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
Minify and compress aggressively
Minify with cssnano or Lightning CSS, then pre-compress. Brotli at level 11 shrinks CSS 18–25% more than Gzip. Serve both encodings and let the CDN negotiate via Vary: Accept-Encoding. Cache compressed and uncompressed variants separately to avoid recompression at the edge.
Separate critical from non-critical CSS
Inline critical above-the-fold CSS (≤14 KB) and load the remainder asynchronously. Use media attributes or rel="preload" as="style" with an onload switch to prevent blocking:
<link rel="stylesheet" href="/assets/app.a3f8c9.css" media="print" onload="this.media='all'">
Cache strategy
Fingerprinted CSS (e.g., app.a3f8c9.css) should be immutable: Cache-Control: public, max-age=31536000, immutable. Non-fingerprinted files (e.g., /css/theme.css) should use max-age=3600, stale-while-revalidate=86400 with ETag validation. Purge via cache invalidation only for emergency fixes; prefer new filenames.
3. JavaScript Delivery Optimization
JavaScript impacts interactivity, Total Blocking Time (TBT), and bandwidth. CDN optimization focuses on delivery, not execution, but delivery choices directly affect parsing time.
- Code splitting and hashing: Split vendor, app, and route chunks. Each chunk gets a content hash. Long-term caching of vendor chunks (which change rarely) yields the highest hit ratio.
- Compression: JS compresses exceptionally well with Brotli. A 350 KB bundle often drops to ~95 KB with Brotli vs ~115 KB with Gzip. Pre-compress at build time and set
Content-Encoding: brat origin or edge. - Delivery attributes: Use
deferfor scripts that need DOM order,asyncfor independent scripts (analytics). Avoid blocking<script>in <head> without defer. - HTTP/2 multiplexing: Splitting into 4–6 chunks is optimal. Too many small files increase header overhead; too few large files reduce cache granularity and increase re-download cost after deploys.
location ~* \.(?:css|js)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Vary "Accept-Encoding";
etag on;
# Pre-compressed files: app.abc123.js.br / .gz
gzip_static on;
gunzip on;
}
Enable edge support for load balancing and failover so JS chunks are served from the nearest healthy PoP even during origin issues, using stale-while-revalidate to mask origin latency.
4. Font Delivery Optimization
Fonts are small but high-impact: a late font swap causes layout shift, a blocked font causes invisible text. CDN tuning must address CORS, caching, and display behavior.
Use WOFF2 only
WOFF2 is 30% smaller than WOFF and supported by >97% of browsers. Serve only WOFF2; let legacy browsers fall back to system fonts.
Subset aggressively
Subset to latin + required glyphs. A 180 KB full font often drops to 35–55 KB after subsetting. Use unicode-range to load language-specific subsets on demand.
Preload critical fonts
Preload 1–2 primary fonts: <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>. Do not preload more than two.
font-display: swap
Prevents FOIT (Flash of Invisible Text). Pair with size-adjust and fallback metrics to minimize CLS during swap.
Fonts require CORS. Set at origin or edge:
Cache-Control: public, max-age=31536000, immutable
Timing-Allow-Origin: *
/* Required for cross-origin font requests from CDN hostname */
Cache fonts immutably for one year. Because fonts rarely change without a filename change, a long TTL is safe and dramatically improves repeat-visit performance. Combine with DDoS protection rate-limiting to prevent font-scraping abuse from inflating bandwidth.
5. Image Delivery Optimization
Images typically represent 50–65% of page weight. CDN image optimization delivers the largest byte savings.
Modern formats and content negotiation
Serve AVIF and WebP with fallback to JPEG/PNG. AVIF reduces size 30–50% vs JPEG at equivalent quality; WebP reduces 25–35%. Use CDN content negotiation (Accept: image/avif,image/webp) or <picture> elements to serve the best format without URL changes.
Responsive variants
Generate 4–6 widths (320, 640, 960, 1280, 1920, 2560) and use srcset + sizes. The CDN should cache each variant separately and strip unnecessary metadata. Enable automatic quality selection (e.g., q=75–80 for photos) and chroma subsampling at the edge.
Cache and origin shielding
Images should be cached for 30–90 days with stale-while-revalidate. Use origin shielding to collapse requests for the same image variant to a single origin fetch. For user-generated images, normalize query parameters (e.g., ?w=800&h=600&fit=crop) to avoid cache fragmentation.
For performance budgeting, see our overview of CDN benefits for performance and security to correlate image weight reductions with Core Web Vitals improvements.
6. Cache Control and Versioning: The TTL Matrix
Versioning determines TTL safety. Content hashing (e.g., Webpack [contenthash], Vite hash) ties the filename to the file’s bytes. When the hash changes, the URL changes — making immutable caching safe. Without hashing, you must rely on validation and short TTLs.
| Asset Type | Versioning | Cache-Control (Edge + Browser) | Purge Strategy |
|---|---|---|---|
| CSS / JS (hashed) | contenthash in filename | public, max-age=31536000, immutable | None — new filename on change |
| CSS / JS (unhashed) | query string or none | public, max-age=3600, stale-while-revalidate=86400 | Surrogate-key purge on deploy |
| Fonts (WOFF2) | hashed | public, max-age=31536000, immutable | None |
| Images (static) | hashed or versioned path | public, max-age=2592000, stale-while-revalidate=604800 | Path purge if needed |
| Images (UGC / dynamic) | query params (w, h, format) | public, max-age=86400, stale-while-revalidate=86400 | Normalize params; purge by tag |
Always set Vary: Accept-Encoding for text assets and Vary: Accept for negotiated images. Avoid Vary: * and unnecessary Vary: User-Agent, which fragment cache. Strip Set-Cookie and Cookie from static asset responses at the edge — cookies prevent caching and inflate request headers.
7. Protocols, Compression, and Edge Tuning
HTTP/2 and HTTP/3
Enable HTTP/2 and HTTP/3 (QUIC) at the edge. Multiplexing eliminates head-of-line blocking for many small assets; QUIC improves lossy-network performance by 10–30% for mobile users. Ensure connection coalescing by serving all static assets from a single hostname (e.g., cdn.example.com) rather than sharding across cdn1, cdn2.
Compression
Pre-compress text assets at build time. Serve Brotli to capable clients, Gzip as fallback. Do not compress already-compressed formats (WOFF2, AVIF, WebP, JPEG, PNG) — it wastes CPU and can increase size.
TLS and early hints
Use TLS 1.3, OCSP stapling, and 0-RTT where safe. Send 103 Early Hints or Link: </assets/app.css>; rel=preload; as=style headers from the edge to initiate CSS/font fetches before HTML fully arrives. Pair with preconnect hints for third-party font providers if you cannot self-host.
8. Common Pitfalls to Avoid
- Cache busting via query string only: Some proxies ignore query strings. Prefer filename hashing.
- Missing immutable: Without
immutable, browsers revalidate on reload, adding 50–150 ms per asset. - Over-preloading: Preloading more than 3 assets contends with critical resources. Audit with Coverage and Priority hints.
- No origin shield: Without shielding, a cache miss storm after a purge can overwhelm origin. Enable shield in the nearest region to origin.
- Serving unhashed assets with long TTL: Users receive stale CSS/JS after deploys. Use short TTL or hashed filenames.
Static asset optimization is iterative. Measure cache hit ratio (target >92% for static), edge TTFB (target <40 ms p50), and LCP before and after each change. Small, consistent wins — 15% smaller CSS, one fewer font, AVIF images — compound into sub-second loads globally.
Frequently Asked Questions
What cache TTL should I use for versioned static assets?
For fingerprinted or content-hashed assets (e.g., app.a3f8c9.css), use immutable caching: Cache-Control: public, max-age=31536000, immutable. This allows 1-year edge and browser caching. Never use immutable without a content hash, as updates will not propagate without a filename change.
Should fonts be served from the same CDN as CSS and JS?
Yes, serving fonts from the same CDN hostname avoids extra DNS and TLS handshakes and improves HTTP/2 connection coalescing. Ensure CORS headers (Access-Control-Allow-Origin: *) are set for cross-origin font requests and use font-display: swap to prevent invisible text.
How does Brotli compare to Gzip for static assets?
Brotli (br) typically reduces text asset size by 15-25% over Gzip at similar decompression speeds. Pre-compress CSS and JS at Brotli level 11 and Gzip level 9 at build time, then serve with content negotiation. Enable CDN support for both encodings and cache variants separately via Vary: Accept-Encoding.
Why are my images not hitting the CDN cache?
Common causes are missing Cache-Control headers from origin, query-string cache busting without normalization, Vary headers that fragment cache keys, or Set-Cookie on static responses. Strip cookies at the edge for static paths, normalize query strings, and set explicit Cache-Control from origin or via edge rules.