Next.js Static Export Deep Dive: Building a 200+ Page High-Performance Tool Site

技术架构(Updated May 17, 2026)

How Static Export Works

Next.js output: "export" pre-renders the entire app to pure static HTML with no Node.js runtime:

next build (SSG)
     ↓
Traverse all generateStaticParams()
     ↓
Pre-render HTML for each [locale] × [slug] combination
     ↓
Output to out/
     ↓
Upload to OSS/CDN

SSG vs SSR vs ISR

Mode Server required Build-time render Updates Best for
SSG (export) No Rebuild & redeploy Fixed-content tool sites
SSR Yes Render per request Personalized content
ISR Yes Background incremental revalidation Frequently updated content

Why ToolsKu chose SSG:

  • 200+ tool pages are fully static—no dynamic data
  • Deploy to Alibaba Cloud OSS + CDN with zero ops overhead
  • Extremely low first-byte TTFB (CDN serves HTML directly)
  • Natural fit for offline caching

generateStaticParams Design Patterns

Locale × tool page Cartesian product

// src/app/[locale]/layout.tsx
export function generateStaticParams() {
  return [
    { locale: 'zh-CN' },
    { locale: 'zh-TW' },
    { locale: 'en' },
    { locale: 'ja' },
  ];
}
// src/app/[locale]/pdf/merge/page.tsx
export function generateStaticParams() {
  return [
    { locale: 'zh-CN' },
    { locale: 'zh-TW' },
    { locale: 'en' },
    { locale: 'ja' },
  ];
}

Next.js automatically computes the Cartesian product of layout.generateStaticParams × page.generateStaticParams, generating HTML for every (locale, path) pair.

Dynamic routes: enumerating blog posts

// src/app/[locale]/blog/[slug]/page.tsx
export function generateStaticParams() {
  return getStaticParamsForAllPosts().map(({ slug, locale }) => ({
    locale,
    slug,
  }));
}

getStaticParamsForAllPosts() scans src/content/blog/, enumerating every slug × available locale so each article version is pre-rendered.


Build Performance Optimization

The build challenge at scale

  • 292 tool pages × 4 locales = 1,168 HTML pages
  • Plus blog, category pages, etc.—1,300+ static pages total
  • Each page calls getTranslations() to load i18n messages

Concurrency control

// next.config.ts
experimental: {
  staticGenerationRetryCount: 2,
  ...(isCi
    ? {
        staticGenerationMaxConcurrency: 4,     // CI: 4 concurrent
        staticGenerationMinPagesPerWorker: 32,  // 32 pages per worker
      }
    : {
        staticGenerationMaxConcurrency: 16,     // Local: 16 concurrent
        staticGenerationMinPagesPerWorker: 8,
      }),
}
  • CI: GitHub Actions has limited memory—lower concurrency to avoid OOM
  • Local dev: Raise concurrency to speed up builds

Timeout settings

staticPageGenerationTimeout: 180, // 3 minutes

Some tool pages (e.g. video tools) load heavy WASM dependencies; pre-render can exceed the default 60s.


Multilingual Routing Architecture

next-intl + App Router integration

src/i18n/routing-config.ts
  → locales: ['zh-CN', 'zh-TW', 'en', 'ja']
  → localePrefix: 'always'  ← All URLs include locale prefix
  → localeDetection: true   ← Accept-Language negotiation

URL structure

https://www.toolsku.com/zh-CN/pdf/merge/     ← Simplified Chinese
https://www.toolsku.com/en/pdf/merge/         ← English
https://www.toolsku.com/zh-CN/blog/pdf-merge-guide/  ← Blog

hreflang implementation

Every page injects complete hreflang tags:

<link rel="alternate" hreflang="zh-CN" href="https://www.toolsku.com/zh-CN/pdf/merge/" />
<link rel="alternate" hreflang="zh-TW" href="https://www.toolsku.com/zh-TW/pdf/merge/" />
<link rel="alternate" hreflang="en" href="https://www.toolsku.com/en/pdf/merge/" />
<link rel="alternate" hreflang="ja" href="https://www.toolsku.com/ja/pdf/merge/" />
<link rel="alternate" hreflang="x-default" href="https://www.toolsku.com/zh-CN/pdf/merge/" />

Automatic Sitemap Discovery

ToolsKu's sitemap.ts uses filesystem scanning to discover all pages:

const pathEntries = discoverLocaleInnerPathsWithFilePath(appDir);
// Auto-discover all page.tsx under src/app/[locale]/

Skipping dynamic segments

discoverLocaleInnerPaths intentionally skips dynamic segments (e.g. [slug]) because they need parameters to render.

For blog posts, sitemap.ts handles them separately:

const blogSlugs = getAllBlogSlugs();
for (const slug of blogSlugs) {
  entries.push({
    url: absoluteLocalizedUrl(locale, `/blog/${slug}`),
    lastModified: meta.updatedAt,
    priority: 0.7,
    alternates: { languages },
  });
}

CDN Deployment Architecture

Asset prefix

// next.config.ts
assetPrefix: getAssetPrefix(),
// → Production points to https://toolsku.oss-cn-beijing.aliyuncs.com/public

Built assets under _next/static/ reference the CDN; HTML is served directly from OSS.

Deployment flow

next build → out/
     ↓
ossutil cp out/ oss://toolsku/ --recursive
     ↓
CDN invalidation (or wait for TTL)
     ↓
New version live

Caching strategy

Resource type Cache policy Rationale
_next/static/**/*.js Cache-Control: max-age=31536000, immutable Filenames include hash
*.html Cache-Control: max-age=3600, s-maxage=86400 Pages may update
*.wasm Cache-Control: max-age=31536000 WASM versions are stable
i18n/*.json Cache-Control: max-age=86400 Search index updates periodically

Build Pipeline

yarn build
  → generate-tools-search-index.mjs    ← Generate tool search index
  → generate-tool-clients-public.mjs   ← Generate tool client data
  → next build                          ← SSG pre-render all pages
     → Traverse generateStaticParams()
     → Render each (locale, path) pair
     → Output to out/

The incremental build gap

Next.js static export does not support incremental builds yet—every next build re-renders all pages. For 1,300+ pages, a full build takes 3–5 minutes.

Mitigations:

  • Lower CI concurrency to reduce memory peaks
  • Use dev mode locally (on-demand render, no pre-generation)
  • Turbopack speeds up development mode

Lessons Learned

Decision Choice Rationale
Rendering SSG (export) Zero ops, minimal TTFB
Routing App Router Server Components, better code splitting
i18n next-intl Native App Router support, mature message system
Content Repo (JSON/TS) No CMS dependency, Git versioning
Hosting OSS + CDN Fast in China, zero server maintenance

Pure static export is the best fit for tool sites—fixed content, high traffic, and a need for blazing first paint. Next.js App Router + SSG lets ToolsKu ship high performance with no server to operate.

Try these browser-local tools — no sign-up required →

#Next.js#SSG#静态导出#CDN#性能优化