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, fast incremental builds, correct cache headers at the edge, and keeping JavaScript off pages that don't need it. This guide is for engineers, indie hackers, 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 levers in order of impact: how the generator you pick sets the baseline, what the build produces, how the edge delivers it, how much JavaScript runs on the client, and how you keep all of it from regressing. 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 the stages of static-site performance, each with its own deep-dive companion:
- Choosing the right generator — the framework decision sets your JavaScript baseline before you write a line of app code. The full framework comparison lives in Choosing the Right Static Site Generator for Production.
- Build performance at scale — incremental builds and caching that keep the measure-and-fix loop fast, detailed in Incremental Builds & Build Caching for SSGs.
- The three Core Web Vitals — why LCP, CLS, and INP fail for different reasons, with the deepest metric broken out in Largest Contentful Paint Optimization for Static Sites.
- Asset pipelines — compression, responsive images, and fonts that set your LCP ceiling. The 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, 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.
- Layout stability — the four causes of movement after first paint and the build-time fix for each, in Cumulative Layout Shift Fixes for Static Sites.
- Third-party cost — budgeting, tiering and replacing the scripts that undo a fast build, in Third-Party Script Performance on Static Sites.
- 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 two dimensions that decide your Core Web Vitals baseline: how much JavaScript reaches the browser, and how much asset processing happens for you at build time.
Astro ships zero JS by default and hydrates islands on demand, which gives it the best out-of-the-box INP story for content sites. 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 repositories. 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, which is the single biggest lever on INP.
| Generator | Default client JS | Build speed | Best fit |
|---|---|---|---|
| Eleventy | None (opt-in) | Fast | Lean content, full control of JS |
| Hugo | None | Fastest | Very large content repositories |
| Astro | Zero, islands on demand | Moderate | Content sites needing selective interactivity |
| Next.js export | Full React hydration | Slower | Teams committed to the React ecosystem |
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 trade-off is authoring experience and ecosystem, not raw output speed — that comparison is weighed page by page in Choosing the Right Static Site Generator for Production, and framework build times specifically in Hugo Build Times for Large Repositories. If you are still narrowing the field, the SSG framework selection matrix scores each option against production criteria.
Build Performance & Incremental Builds at Scale
Build speed is not a Core Web Vital, but it decides whether you actually measure them. When a full rebuild takes ten minutes, teams stop previewing changes and skip the Lighthouse pass that catches regressions. Keeping builds fast is what keeps the measure-and-fix loop tight enough to protect the metrics that ship.
The problem scales with content. A 200-page site rebuilds in seconds; a 20,000-page documentation set can take many minutes if every page is regenerated on every commit. Three techniques keep that in check:
- Incremental builds — only regenerate pages whose source (or a shared dependency) changed. Hugo and Eleventy both support incremental modes; Next.js offers Incremental Static Regeneration for hybrid hosts.
- Persistent build caching — cache the expensive steps (image transforms, MDX compilation, data fetches) between runs so a warm build skips work a cold build repeats. In CI this means restoring a cache key keyed on lockfile and content hashes.
- Parallelism — shard the build across cores or across CI runners for very large sites, then stitch the output.
The payoff is concrete. A documentation site that took 8 minutes for a clean rebuild in CI can drop to under 45 seconds on a warm cache when only a handful of pages changed — the difference between previewing every pull request and previewing none. The mechanics of cache keys, safe invalidation, and CI wiring are covered end to end in Incremental Builds & Build Caching for SSGs. Because the build step also produces every optimized asset, it is where your LCP ceiling is set — which is the next section.
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';
},
},
},
},
});
A stable vendor chunk pairs directly with the caching strategy below: once it is fingerprinted, a returning visitor never downloads it again until a dependency actually changes.
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. Because it is the metric with the most moving parts on a static page, it gets its own walkthrough in Largest Contentful Paint Optimization for Static Sites. - 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.
The thresholds Google uses for a "good" experience at the 75th percentile are worth committing to memory, because they are what field data is graded against:
| Metric | Good | Needs work | Poor | Primary driver on an SSG |
|---|---|---|---|---|
| LCP | ≤ 2.5s | ≤ 4.0s | > 4.0s | Hero image / font, TTFB |
| CLS | ≤ 0.1 | ≤ 0.25 | > 0.25 | Unsized media, late fonts |
| INP | ≤ 200ms | ≤ 500ms | > 500ms | Main-thread JS during interaction |
The static-site advantage is that pre-rendering hands you good LCP and CLS almost for free — the largest paint is real HTML the CDN already holds, and there is no client-side layout thrash if you reserve space. That leaves INP and TTFB as the metrics that take real engineering, and they are governed by the two stages that follow: edge delivery and hydration.
Edge/CDN Delivery, Caching and Headers
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 is the two-tier policy: fingerprinted (hashed) assets never change, so cache them for a year as immutable — the browser then skips revalidation entirely. HTML is the opposite. It needs a short TTL so a new deploy is visible quickly and rollbacks actually take effect, ideally paired with stale-while-revalidate so users get an instant response while the edge refreshes in the background.
A typical host config for hashed assets:
{
"headers": [
{
"source": "/static/:path*",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
}
]
}
And the matching short-lived policy for HTML documents:
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400
On platforms with a global edge — the setup in Cloudflare Pages Edge Caching Setup is a worked example — this two-tier split is what turns a fast build into a fast first byte for a visitor on the other side of the world. Get the split wrong in the other direction (long TTL on HTML) and you serve stale content and break rollbacks; that failure mode is in the pitfalls below.
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 the smallest file that fits the layout slot. A single 1.4 MB JPEG hero can become a set of AVIF sources totaling under 200 KB at the sizes actually rendered — the entire technique, with before/after numbers, is in Image Optimization Pipelines in Astro.
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. Late-swapping fonts are a leading cause of CLS on otherwise-static pages; Font Loading Strategies for Static Sites walks through font-display, metric-matched fallbacks, and the size-adjust descriptor that removes the reflow entirely. In both cases the work happens in the build step above and is delivered by the caching layer above that — the pipeline is one continuous chain, not three isolated optimizations.
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 right directive depends on where the component sits and how soon a user is likely to touch it — client:idle for below-the-fold widgets, client:visible for anything that enters on scroll, and eager loading reserved for controls above the fold. The full decision tree is in JavaScript Hydration & Partial Rendering.
Audit third-party scripts just as hard. Analytics, chat widgets, and ad tags routinely wreck INP on otherwise-fast static pages, because they run on the same main thread your interactions need. Give them a byte budget, load them with async/defer or from a partytown-style worker, and treat a new tag the same way you would treat adding a heavy dependency to your own bundle.
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. The web-vitals library reports LCP, CLS, and INP straight from real sessions, which is the only place INP can be measured honestly — it depends on real interactions the lab cannot simulate.
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
Running that against a per-branch preview URL — the pattern in Preview Environments for Pull Requests — means every change is measured on production-like infrastructure before it merges. Wire the collection step itself into your pipeline with GitHub Actions for Automated SSG Builds, and the wider deploy, rollback, and content workflow is covered in Production-Ready Deployment & CI/CD Workflows.
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. That single habit, correlating RUM against releases, catches the majority of production regressions before users complain.
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.
- Rebuilding everything on every commit: slow full rebuilds push teams to stop previewing changes, which is how performance regressions slip through unmeasured.
- Trusting lab scores alone: a 100 in Lighthouse says nothing about the INP a real user hits on a mid-range Android — always confirm against field RUM.
Where the Time Actually Goes on a Static Page
It is worth being concrete about the budget, because "make it fast" is not actionable and the components are not equally expensive.
On a typical content page loaded on a mid-tier phone over a slow connection, the largest single cost is almost always the hero image: it is the LCP element, it is measured in hundreds of kilobytes, and it is downloaded before anything below the fold. The second is render-blocking CSS, which delays every paint by a full round trip regardless of how small the page is. The third is fonts, which delay readable text and, without metric-matched fallbacks, move it once it appears. The fourth is third-party JavaScript, which rarely affects the paint at all and dominates interaction latency.
That ordering is stable across content sites, and it is the reason the sections in this guide are ordered as they are. A team that optimises in this order sees most of the available improvement in the first two changes; a team that starts with bundle splitting on a page that ships 12 KB of its own JavaScript will spend a week for a result nobody can measure.
The corollary is that a static site's performance work is mostly asset work rather than code work. The generator already removed the server, the hydration and the runtime rendering. What remains is bytes, their order, and whether the browser can discover them early — which is exactly what the following sections are about.
Measure in Both Directions, Always
Every section above ends in a number, and those numbers come from two different places that answer different questions.
Lab measurement — Lighthouse, a scripted trace, a build-time budget — is reproducible, runs on every pull request and tells you whether a change helped. Its weakness is that it models one device on one connection loading one page once, which is not what readers do.
Field measurement — the web-vitals library reporting from real page views — tells you what readers actually experienced, across the devices and networks they actually have, over the whole lifetime of the page. Its weakness is latency: you learn about a regression days after it shipped, and only if enough people hit the affected template.
Use the lab to gate changes and the field to choose what to work on. A team that only has lab numbers optimises pages that were already fast for the people visiting them; a team that only has field numbers cannot tell whether last week's change was the cause. The pipeline for collecting the field half is small — a few kilobytes of script and an endpoint — and it is the single highest-leverage addition to a performance practice that already has lab gates in place.
Keep both series per template rather than per URL. A content site has a handful of templates and thousands of pages, and a regression is nearly always a property of the template rather than of any individual page.
Key Takeaways
- SSGs hand you good LCP and CLS for free; the real work is protecting INP and TTFB.
- The generator you choose sets the JavaScript baseline before you write any app code — pick for your interactivity needs, not just build speed.
- Fast incremental builds keep the measure-and-fix loop tight, so build speed protects Core Web Vitals indirectly.
- 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.
Do slow builds affect Core Web Vitals?
Not directly, but they change your workflow. When a full rebuild takes ten minutes, teams stop previewing changes and skip the performance checks that catch regressions. Fast incremental builds keep the measure-and-fix loop tight, so build speed protects Core Web Vitals indirectly.
Related
- Parent: Static Site Generators in Production — the home guide tying performance, framework choice, and deployment together.
- Largest Contentful Paint Optimization for Static Sites — the deepest of the three metrics, start to finish.
- 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.
- Cumulative Layout Shift Fixes for Static Sites — stop the page moving after it paints.
- Third-Party Script Performance on Static Sites — budget and tier everything a vendor ships.
- Choosing the Right Static Site Generator for Production — how the framework decision sets your performance baseline.
- Production-Ready Deployment & CI/CD Workflows — preview deploys, rollbacks, and build automation.