Skip to main content
Back to Research
Field ReportDeep Readfrontend-engineering

Performance Patterns for Static Exports

Static sites are fast by default and slow by accident. The accidents follow predictable patterns: wrong JavaScript loading order, unoptimized images…

Abstract

A systematic collection of performance patterns for Next.js applications using output: export — covering resource loading order, image optimization, animation budgeting, font strategies, and the specific optimizations that transform a 44 Lighthouse score into a 70+ score without architectural changes.

June 15, 2026
8 min read

Static sites have no server rendering latency. They serve pre-built HTML from a CDN edge node, typically within 20ms globally. The performance ceiling is high. Most teams hit the floor instead, because the JavaScript they ship on top of the static HTML introduces exactly the latency they avoided by eliminating the server.

This handbook documents the specific patterns that recur across Next.js static export performance audits — why they occur, how to identify them, and how to fix them. Every pattern here is derived from measuring real build outputs, not from first-principles reasoning.

Pattern 1 — The Preload Nothing Mistake

Symptom: High LCP (Largest Contentful Paint) despite the LCP element being visible on initial load. The element is there in the HTML, but its resources arrive late.

Cause: The LCP element references an image or font that is not preloaded. The browser discovers it only when it parses the element's CSS or HTML attribute — too late in the loading pipeline.

Fix: Add <link rel="preload"> for the LCP resource. In Next.js, this goes in the root layout's <head>:

<link
  rel="preload"
  href="/hero-image.webp"
  as="image"
  type="image/webp"
/>

For fonts, use next/font which handles preloading automatically. For images, the next/image component with priority prop injects the preload hint.

Pattern 2 — The Synchronous Third-Party

Symptom: High TBT, high FID (or INP in modern audits). The performance timeline shows a long task from a domain that is not your domain — analytics.example.com, static.intercomcdn.com.

Cause: Third-party scripts loaded synchronously before the page is interactive. Common culprits: Google Tag Manager, Hotjar, Intercom, Drift, Segment.

Fix: Use Next.js's Script component with strategy="afterInteractive" or strategy="lazyOnload":

import Script from 'next/script'

// Loads after hydration — safe for analytics
<Script
  src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"
  strategy="afterInteractive"
/>

// Loads in idle time — for non-critical enrichment
<Script src="https://cdn.drift.com/drift.js" strategy="lazyOnload" />

Resist vendor pressure to load their scripts "before the closing body tag." That is not a meaningful timing constraint for modern browsers with resource scheduling. afterInteractive is safe for everything except scripts that modify the DOM before first paint — which is a legitimate use case only for anti-flash-of-unstyled-content scripts.

Pattern 3 — The Unnecessary Layout Animation

Symptom: High CLS (Cumulative Layout Shift). The page looks stable but Lighthouse reports CLS above 0.1.

Cause: Framer Motion or CSS animations that animate layout properties — top, left, width, height, margin, padding — trigger layout recalculation on every frame. These also shift the visual position of elements, causing CLS.

Fix: Animate only transform and opacity. These run on the GPU compositor thread and do not trigger layout:

// ❌ Causes layout shift + CLS
<motion.div animate={{ top: isVisible ? 0 : -20 }} />

// ✅ GPU-accelerated, no CLS
<motion.div animate={{ y: isVisible ? 0 : -20 }} />

For elements that genuinely need to change size (accordion reveals, expanding cards), use Framer Motion's layout prop to let the library handle the layout animation efficiently rather than directly animating layout properties.

Pattern 4 — The Unoptimized Image

Symptom: Lighthouse reports "Serve images in next-gen formats" or "Properly size images."

Cause: Raw PNG or JPEG images served without compression or format conversion. Common in portfolio sites where project screenshots are added as-is.

Fix for static exports: Next.js's built-in image optimization (the next/image component) does not work with output: 'export' unless you configure an external image provider. The pragmatic options are:

  • Pre-convert images to WebP at commit time using a Makefile target or pre-commit hook
  • Use Cloudflare Images as the delivery layer (automatic WebP/AVIF conversion at edge)
  • Use next/image with unoptimized: true and rely on Cloudflare Pages for optimization

For the hero image specifically — the most viewed, most impactful image on any portfolio — convert to WebP manually and add the preload hint from Pattern 1. This is worth the manual step.

Pattern 5 — The Font Flash

Symptom: Lighthouse reports CLS from font loading. Users see a flash of system font before the custom font loads.

Cause: Google Fonts loaded via <link> tag without font-display: swap, or font files that arrive too late to be included in the first paint.

Fix: Use next/font/google, which automatically applies display: 'swap' and preloads the font subset for the current locale. The CLS impact drops to near-zero because the browser knows immediately which fallback to use and how much space the loaded font will need.

import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',   // default in next/font — explicit for clarity
  preload: true,     // default — generates preload link in <head>
})

If you need a font that is not on Google Fonts, use next/font/local with the TTF file committed to your repository. This eliminates the network dependency entirely and is the best performance option available.

Pattern 6 — The Premature Three.js

Symptom: Three.js (600KB+ minified) is included in the main chunk, blocking interactivity before the page is visible.

Cause: R3F or Three.js imported statically at module level in a component that renders on the homepage, even if the 3D scene is below the fold.

Fix: Dynamic import the entire 3D section with ssr: false, wrapped in a LazySection that only triggers the import when the section approaches the viewport:

const HeroScene = dynamic(
  () => import('./HeroScene').then(m => ({ default: m.HeroScene })),
  { ssr: false }
)

// In your page component:
<LazySection rootMargin="400px">
  <HeroScene />
</LazySection>

The rootMargin="400px" starts loading the Three.js chunk when the section is 400px below the viewport, giving it time to load before the user scrolls to it. Adjust based on your measured scroll speed distribution.

Pattern 7 — The Missing Service Worker

Symptom: Lighthouse PWA audit fails. Repeat visits do not benefit from cache.

Cause: No service worker registered, or the service worker does not cache the right assets.

Fix: A minimal service worker that caches the HTML, JS chunks, and images on first visit dramatically improves repeat-visit performance. The key is cache versioning — a new deploy should update the cache, not serve stale assets:

const CACHE_VERSION = 'v1' // increment on every deploy
const STATIC_CACHE  = `static-${CACHE_VERSION}`

self.addEventListener('install', (e) => {
  e.waitUntil(
    caches.open(STATIC_CACHE).then(cache =>
      cache.addAll(['/', '/en/', '/es/', '/offline.html'])
    )
  )
})

Connect the cache version to your build process — ideally, the version is a hash of the build output or a timestamp. Stale service workers serving outdated JS bundles cause mysterious bugs that are maddening to debug.

Measuring Progress

Run Lighthouse from CI on every deploy, not just manually when you remember. The metrics that matter most for static exports:

  • TBT under 200ms — acceptable; under 50ms — excellent
  • LCP under 2.5s — good; under 1.5s — excellent
  • CLS under 0.1 — good; under 0.05 — excellent

These targets are achievable on a Next.js static export with Three.js and Framer Motion. The key is the combination of lazy loading heavy dependencies (Pattern 6), deferring third parties (Pattern 2), and animating only transform/opacity (Pattern 3). Applied together, they convert a 44 Lighthouse score to a 70+ score without removing any functionality.

Continue Reading
JCJOOTACEE / OPS

Operational laboratory for AI systems, automation infrastructures, and modular digital ecosystems.

Systems

  • AURA Orchestration
  • MCP Ecosystem
  • Graph Memory
  • AI Agents
  • Docker Infrastructure
  • Industrial Intelligence

System Status

PlatformOperational
APIHealthy
3D EngineActive
MCP Nodes8 Online

Try the Konami code...

Stay in the loop

Occasional updates on AI systems, autonomous infrastructure, and new releases.

© 2026 JootaCee. All systems operational.

RSSChangelogNext.js 16 + React 19 + R3F + GSAP