Performance Optimization & Core Web Vitals for SSGs
Static site generators give you a head start on Core Web Vitals — pre-rendered HTML means fast Largest Contentful Paint (LCP) and stable layout by default. But production performance isn't automatic. It comes from disciplined asset processing, correct cache headers at the edge, and keeping JavaScript off pages that don't need it. This guide is for engineers and documentation teams who already ship a static site and want to turn "fast by default" into "fast under real-world load."
We cover the four levers in order of impact: build-time assets, edge delivery, hydration, and continuous measurement. Each one ties back to a specific metric and a specific tool you can use to measure it.
What You Will Learn
This guide is organized around four pillars of static-site performance, each with its own deep-dive section:
- Build-time asset optimization — compression, responsive images, and chunk splitting that set your LCP ceiling. The full image pipeline lives in Image Optimization Pipelines in Astro, and font handling in Font Loading Strategies for Static Sites.
- Edge delivery and caching — the two-tier cache policy that makes repeat visits nearly free, detailed in CDN Caching Rules for SSGs.
- Hydration and interactivity — shipping zero JavaScript by default and hydrating only real islands, covered in JavaScript Hydration & Partial Rendering.
- Continuous measurement — gating deploys on a performance budget so regressions fail the build instead of shipping.
Choosing an SSG for Production Performance
Every major generator pre-renders HTML, but they differ in how much JavaScript they ship and how they handle assets. Astro ships zero JS by default and hydrates islands on demand, which gives it the best out-of-the-box INP story. Eleventy is template-only — there is no client runtime at all unless you add one — so its baseline is even leaner, at the cost of doing interactivity yourself. Hugo is the fastest builder and emits plain HTML/CSS, ideal for large content sites. A Next.js static export gives you React's component model with pre-rendered output, but you pay for the framework runtime on every interactive page.
For Core Web Vitals specifically, the ranking on a content-heavy site is roughly: Eleventy and Hugo (no runtime) ≈ Astro (islands) < Next export (full React hydration). The framework comparison in Choosing the Right Static Site Generator for Production weighs these trade-offs against authoring experience and ecosystem.
Build-Time Asset Optimization & Compression
What you ship at build time sets your LCP ceiling. Compress HTML, CSS, and JavaScript — Brotli is well supported by every major CDN and beats gzip by 15-20% on text assets — and split bundles so a visitor to one page doesn't download the whole site's JavaScript.
Generate responsive images at build time rather than resizing in the browser. Each framework has a native path: Hugo's image Resize/Fill methods, Eleventy's eleventy-img, Jekyll's jekyll_picture_tag, and Astro's built-in <Image /> from astro:assets (no separate plugin — image handling moved into Astro core in v3). All emit optimized formats like WebP/AVIF at build time, which typically cut image bytes by 25-50% versus JPEG at equivalent quality.
For JavaScript, the highest-leverage build setting is chunk splitting — isolate dependencies into a long-cacheable vendor chunk so app changes don't bust the whole bundle:
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
build: {
rollupOptions: {
output: {
// Put everything from node_modules in a stable "vendor" chunk.
manualChunks(id) {
if (id.includes('node_modules')) return 'vendor';
},
},
},
},
});
Core Web Vitals: LCP, CLS and INP for Static Sites
The three metrics fail for different reasons, so they need different fixes:
- LCP is usually a hero image or a web font blocking the largest text block. Preload the LCP image, serve it as AVIF/WebP, and give it
fetchpriority="high". On a typical marketing hero this moves LCP from ~3.2s to ~1.8s on a mid-tier mobile connection. - CLS comes from images and ads without reserved dimensions, and from web fonts swapping in late. Always set
width/height(oraspect-ratio) on media, and usefont-display: optionalor a metric-matched fallback to avoid reflow. - INP is driven by main-thread JavaScript during interaction. The fix is structural: ship less JS, hydrate fewer components, and move heavy work off the main thread.
Delivery Architecture & Edge Caching
Serving pre-built HTML well is mostly about cache headers. Distribute through a CDN's points of presence to cut Time to First Byte (TTFB), and align your purge strategy with the CDN Caching Rules for SSGs so a deploy doesn't stampede your origin.
The rule that matters most: fingerprinted (hashed) assets never change, so cache them for a year as immutable — the browser then skips revalidation entirely. HTML is the opposite and needs a short TTL. A typical host config for hashed assets:
{
"headers": [
{
"source": "/static/:path*",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
}
]
}
Image, Font and Asset Optimization Pipelines
Images and fonts are the two assets most likely to wreck LCP and CLS. The discipline is the same for both: process them at build time, serve modern formats, and reserve their space in the layout before they load. For images, that means a build step that emits multiple widths and formats and a <picture> element that lets the browser pick. For fonts, it means subsetting to the characters you use, self-hosting to avoid a third-party connection, and preloading the one weight above the fold. The dedicated guides — Image Optimization Pipelines in Astro and Font Loading Strategies for Static Sites — walk through both end to end with before/after numbers.
Client-Side Hydration & Interaction Metrics
JavaScript on the main thread is what drives INP up. The static-site advantage is that you can ship zero JS by default and hydrate only the components that are actually interactive — islands architecture. Astro's client:* directives make the trade-off explicit: load eagerly, on visibility, or on idle.
<!-- Hydrate only when needed, not on every page load -->
<SearchBox client:visible />
<CartWidget client:idle />
The JavaScript Hydration & Partial Rendering approach covers when each directive is appropriate. Audit third-party scripts just as hard — analytics and chat widgets routinely wreck INP on otherwise-fast static pages, because they run on the same main thread your interactions need.
CI/CD, Performance Budgets and Continuous Measurement
Lab scores (Lighthouse) and field data (Real User Monitoring) measure different things; track both. Lab catches regressions before deploy; RUM tells you what users actually experience across devices and networks.
Gate deploys on a Lighthouse budget so a regression fails the build instead of shipping:
lhci autorun \
--collect.url=https://deploy-preview.example.com/ \
--assert.preset=lighthouse:recommended
When a field metric drops, line it up against your deploy timeline — a sudden INP regression usually maps to a specific release or a new third-party tag, not gradual drift. Wiring this into the deploy flow is covered in Production-Ready Deployment & CI/CD Workflows.
Common Pitfalls
- Over-hydrating static content: attaching a client framework to purely presentational markup adds bundle weight and delays First Contentful Paint with no benefit.
- Long TTLs on HTML: caching HTML aggressively serves stale content and breaks rollbacks. Keep HTML short-lived; reserve
immutablefor hashed assets. - Ignoring third-party scripts: analytics, chat, and ad tags execute on the main thread and degrade INP and LCP regardless of how optimized your own output is.
- Unsized media: images, embeds, and ads without explicit dimensions are the most common CLS source on otherwise-static pages.
Key Takeaways
- SSGs hand you good LCP and CLS for free; the real work is protecting INP and TTFB.
- Optimize assets at build time — that step sets the ceiling everything else operates under.
- Use the two-tier cache policy: immutable hashed assets, short-lived HTML.
- Hydrate only genuine interactive islands, and budget third-party scripts as strictly as your own.
- Gate every deploy on a performance budget so regressions never reach production silently.
FAQ
How do Core Web Vitals differ for SSGs compared to SPAs?
SSGs serve pre-rendered HTML, so LCP and CLS are strong by default. The remaining work is INP — you still have to control hydration so interactivity matches what a single-page app provides without shipping its full JavaScript cost.
Can static sites achieve top Lighthouse scores in production?
Yes. With disciplined asset optimization, deferred third-party scripts, and edge caching, static sites routinely score 95-100 in the lab. Just remember lab scores can diverge from real-user data, so monitor both Lighthouse and field RUM.
What is the optimal caching strategy for SSG HTML files?
Use a short max-age (0-300s) with stale-while-revalidate for HTML, paired with one-year immutable caching for fingerprinted CSS, JS, and images. This two-tier policy keeps content fresh while making repeat visits nearly free.
How does partial hydration impact SEO and performance?
Search engines receive full crawlable HTML while JavaScript execution is deferred, which improves First Contentful Paint and INP and lowers the main-thread cost. Content is indexable regardless of whether an island has hydrated.
Which Core Web Vital is hardest to fix on a static site?
INP. LCP and CLS are largely solved by pre-rendering and reserved space, but INP depends on how much JavaScript runs on the main thread during interaction — which is entirely under your control through hydration discipline and third-party script budgets.
Related
- Parent: Static Site Generators in Production — the home guide tying performance, framework choice, and deployment together.
- Image Optimization Pipelines in Astro — build-time responsive images.
- Font Loading Strategies for Static Sites — eliminate font-driven CLS.
- CDN Caching Rules for SSGs — the two-tier cache policy in depth.
- JavaScript Hydration & Partial Rendering — control INP with islands.