Skip to main content
Cloudflare

Cloudflare Performance Optimization

Cache headers, Core Web Vitals fixes, and CDN configuration for Arcturus-Prime.com on Cloudflare Pages

February 25, 2026

Cloudflare Performance Optimization

Arcturus-Prime runs on Cloudflare Pages with a mix of static pre-rendered pages and SSR worker routes. This doc covers the caching strategy, CWV optimizations, and known Cloudflare Pages behaviors.

Cache Strategy

public/_headers File

Cloudflare Pages reads public/_headers to set response headers on static assets. The file uses path patterns with most-specific-match-wins ordering.

Key rules:

Path PatternBrowser CacheEdge CachePurpose
/_astro/*immutable, 1 yearFingerprinted JS/CSS/images
/images/*, /fonts/*, /assets/*immutable, 1 yearStatic assets
/blog/*, /journal/*, /posts/*5 min1 hourContent pages
/1 min5 minHomepage
/status, /command/*, /telemetry1 min1 minDynamic-ish pages
/api/*, /auth/*no-storeNever cache

CDN-Cache-Control

CDN-Cache-Control is a Cloudflare-specific header that sets the edge cache TTL independently from the browser Cache-Control. This lets us have short browser TTLs (users see fresh content) with longer edge TTLs (reduce origin requests).

Cache-Control: public, max-age=300, must-revalidate
CDN-Cache-Control: max-age=3600

Browser caches for 5 minutes, edge caches for 1 hour.

cf-cache-status: DYNAMIC

On Cloudflare Pages, HTML responses always show cf-cache-status: DYNAMIC. This is normal — Pages has its own edge distribution separate from the traditional CDN cache layer. The pages are still served fast from the edge.

What NOT to do

  • /*.html patterns don’t match Cloudflare Pages clean URLs (/blog/slug/)
  • You can’t override Strict-Transport-Security via _headers — it’s set in the Cloudflare dashboard
  • The /* catch-all should only contain security headers, never Cache-Control (it would override specific rules)

Core Web Vitals

CLS (Cumulative Layout Shift)

All images need explicit width and height attributes. Without them, the browser doesn’t know how much space to reserve, causing layout shifts when images load.

Critical files:

  • src/pages/blog/[slug].astro — hero image
  • src/pages/posts/[slug].astro — hero image
  • src/pages/configurations/[slug].astro — sidebar image
  • src/pages/projects/[slug].astro — sidebar image
  • src/layouts/BlogPost.astro — hero image + avatar

LCP (Largest Contentful Paint)

Hero images must use loading="eager" and fetchpriority="high". The default loading="lazy" defers the download, which is wrong for above-fold content.

<!-- WRONG -->
<img src={heroImage} loading="lazy" />

<!-- CORRECT -->
<img src={heroImage} width="1024" height="512"
     loading="eager" decoding="async" fetchpriority="high" />

Font Optimization

Self-hosted WOFF2 fonts with preload hints in both layouts:

<link rel="preload" href="/fonts/SpaceGrotesk-latin.woff2"
      as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/fonts/Inter-latin.woff2"
      as="font" type="font/woff2" crossorigin />

All @font-face declarations use font-display: swap (in src/styles/fonts.css).

Preconnect Hints

External origins get preconnect hints to save DNS+TLS roundtrips:

<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin />
<link rel="dns-prefetch" href="https://cdnjs.cloudflare.com" />

Added to both BaseLayout.astro and CosmicLayout.astro.

Security Headers

Applied globally via /* in _headers:

HeaderValuePurpose
X-Content-Type-OptionsnosniffPrevent MIME sniffing
X-Frame-OptionsDENYPrevent clickjacking
Referrer-Policystrict-origin-when-cross-originLimit referrer leaks
Permissions-Policycamera=(), microphone=(), geolocation=()Disable device APIs
Cross-Origin-Opener-Policysame-originIsolate browsing context
Cross-Origin-Resource-Policysame-originPrevent cross-origin reads
Content-Security-PolicyFull policyXSS prevention

HSTS is configured in the Cloudflare dashboard (not _headers).

Proxy Route Error Handling

All API proxy routes (/api/gateway/*, /api/proxy/*, /api/orchestrator/*, etc.) must wrap fetch calls in try/catch and return graceful 502 JSON responses when backends are unreachable. Without this, the CF Worker crashes and returns a raw 500 to the client.

Pattern:

try {
  const result = await cachedFetch(key, () => fetchOrigin(url));
  return new Response(result.body, { status: result.status, headers });
} catch (error) {
  return new Response(
    JSON.stringify({ error: 'Service unavailable', detail: error.message }),
    { status: 502, headers: { 'Content-Type': 'application/json' } }
  );
}

4xx/5xx Error Sources

  • 4xx (~22k): Mostly bot/scanner noise — WordPress paths, .env, xmlrpc.php. All 536 sitemap URLs return 200. Consider a Cloudflare WAF rule to block common scanner paths.
  • 5xx (~1.7k): Intentional 502/503 from proxy routes when homelab services are offline. After wrapping all routes in try/catch, unhandled 500s should be near zero.
cloudflareperformancecachingcwv