Inkrementelle statische Builds in Astro 7.2:… | LaunchFast
LaunchFast LogoLaunchFast
Blog
1.137 Wörter6 Min. Lesezeit

Inkrementelle statische Builds in Astro 7.2: So habe ich meine Rebuild-Zeit um 39% reduziert

Wie inkrementelle statische Builds in Astro 7.2 den Rebuild eines einzelnen Beitrags von ~16,5s auf ~10s reduziert haben, indem 637 von 661 Seiten mit der richtigen cacheKey-Einrichtung aus dem Cache wiederhergestellt wurden.

Rishi Raj Jain
Rishi Raj JainAutor

Ich habe meine inkrementelle Rebuild-Zeit um etwa 39% reduziert, von rund 16,5 Sekunden auf rund 10, indem ich inkrementelle statische Builds in Astro 7.2 aktiviert habe.

Diese Seite prerendert bei jedem Produktions-Build 661 Seiten: 70 Blogbeiträge in drei Sprachen, Dokumentation, Feature-Seiten und Use-Case-Seiten. Die meisten dieser Seiten ändern sich zwischen Deployments nicht. Vor dieser Änderung wurden beim Bearbeiten eines einzelnen Beitrags trotzdem alle 661 neu gerendert.

Astro 7.2 erlaubt es einer prerenderten Route, aus getStaticPaths einen cacheKey zurückzugeben. Astro stellt die Seite aus dem vorherigen Build wieder her, wenn ihr Modulgraph und dieser Schlüssel beide unverändert sind. Nach dem Bearbeiten eines einzelnen Beitrags werden 637 von 661 Seiten aus dem Cache wiederhergestellt. Dieser Beitrag beschreibt, wie ich das eingerichtet habe.

Voraussetzungen

  • Astro 7.2 oder neuer
  • Prerenderte Routen (export const prerender = true)
  • Bun für die Starlight-Patch-Schritte weiter unten (npm/yarn/pnpm haben gleichwertige Patch-Tools)

Schritt 1: Aktiviere das Flag

Um die inkrementellen Builds nutzen zu können, ist der erste Schritt das Upgrade auf 7.2 mit dem folgenden Befehl:

Terminal window
bun add astro@latest

Beachte, dass der Modus experimentell ist. Aktiviere ihn in astro.config.mjs mit der folgenden Änderung:

export default defineConfig({
experimental: {
incrementalBuild: true,
},
})

Nach dem Aktivieren des Flags haben die ersten beiden Builds gleich lange gedauert. Es wurden keine Seiten aus dem Cache wiederhergestellt. Kommen wir zur Concurrency!

Schritt 2: Korrigiere die Concurrency-Einstellung

Astro deaktiviert den inkrementellen Cache, wenn Seiten parallel gerendert werden:

node_modules/astro/dist/core/build/generate.js
if (options.settings.config.experimental.incrementalBuild) {
if (options.settings.config.build.concurrency > 1) {
logger.warn(
'build',
'The incremental build cache is disabled because `build.concurrency` is greater than 1.',
)
} else {
// load the cache and restore pages
}
}

Ich hatte build.concurrency auf die Anzahl der CPUs gesetzt. Paralleles Rendern und das Wiederherstellen aus dem Cache funktionieren nicht zusammen, deshalb habe ich mein Override entfernt:

astro.config.mjs
// build: {
// concurrency: Math.min(os.cpus().length, 12), // removed
// }

Mit der Concurrency zurück auf dem Standardwert 1 verlierst du das parallele Rendern, gewinnst aber die Cache-Wiederherstellung. Beachte, dass dies nur hilft, wenn der Großteil deiner Seite zwischen den Builds unverändert bleibt.

Schritt 3: Füge dem Blog einen Cache Key hinzu

Eine Route tritt dem Cache bei, indem sie aus getStaticPaths einen cacheKey zurückgibt. Astro verwendet eine Seite nur dann wieder, wenn ihr Modulgraph und dieser Schlüssel beide mit dem vorherigen Build übereinstimmen.

Content-Collection-Einträge haben einen digest, der sich ändert, wenn sich die Quelle des Eintrags ändert:

export async function getStaticPaths() {
const posts = await getCollection('blog')
return posts.map((post) => ({
params: { slug: post.data.slug },
props: { post },
cacheKey: post.digest,
}))
}

post.digest allein reicht für meine Blog-Vorlage nicht aus.

Schritt 4: Beziehe alles ein, was die Seite rendert

Eine Blog-Seite rendert hier:

  • Drei verwandte Beiträge (Vorschau von Titel und Beschreibung)
  • Die aktuelle Sponsor-Anzeige in der Seitenleiste

Keins von beiden ist Teil von post.digest. Mit nur dem Post-Digest als Schlüssel liefert der Cache eine veraltete Seite aus, wenn sich ein verwandter Beitrag oder die Sponsor-Kampagne ändert.

src/lib/cache-key.ts
import { getActiveSponsor } from '@/lib/sponsor'
type WithDigest = { digest?: string | number }
export function buildContentCacheKey(post: WithDigest, related: WithDigest[] = []): string {
const sponsor = getActiveSponsor()?.id ?? 'none'
const relatedDigests = related.map((entry) => entry.digest ?? '').join('.')
return [post.digest ?? '', relatedDigests, sponsor].map(String).join('|')
}

Der Blog in allen drei Sprachen und die Feature-Seiten verwenden diesen Schlüssel. Der Cache wird ungültig, wenn sich der Post-Text, eine verwandte Vorschau oder der aktive Sponsor ändert.

Schritt 5: Use-Case-Seiten

Die Use-Case-Seiten sind die größte Gruppe auf der Seite: über 300 über alle Sprachen hinweg. Sie sind keine Content-Collection-Einträge. Sie werden aus TypeScript generiert, eine Seite pro Framework und Integration.

Die Daten sind importiertes TypeScript, also bereits im Modulgraph der Route. Astros Dependency-Hash invalidiert jede Use-Case-Seite, wenn sich die Daten ändern, mit oder ohne cacheKey. Der Modulgraph deckt die Sponsor-Anzeige nicht ab, deshalb braucht der Schlüssel nur den Slug und den aktiven Sponsor:

export function buildUseCaseCacheKey(useCase: { slug: string }, related: { slug: string }[] = []): string {
const sponsor = getActiveSponsor()?.id ?? 'none'
const relatedSlugs = related.map((entry) => entry.slug).join('.')
return [useCase.slug, relatedSlugs, sponsor].join('|')
}

Schritt 6: Docs-Seiten (Starlight-Patch)

Die Dokumentation läuft mit Starlight, das eine Catch-all-Route [...slug] einfügt. Es gibt in meinem Code kein getStaticPaths, dem ich einen Schlüssel hinzufügen könnte.

Zuerst habe ich versucht, die Route mit src/pages/[...slug].astro zu überschatten. Die Docs wurden aus dem Cache wiederhergestellt, aber Astro protokollierte für jeden Docs-Pfad einen Routenkonflikt. Zwei Routen beanspruchten dasselbe Muster.

Die Lösung ist, Starlights eingefügte Route zu patchen und sie mit bun patch zu verfolgen:

Terminal window
bun patch @astrojs/starlight
# edit node_modules/@astrojs/starlight/routes/static/index.astro
bun patch --commit 'node_modules/@astrojs/starlight'

Das schreibt einen Patch unter patches/ und einen patchedDependencies-Eintrag in package.json. Bun wendet ihn bei jeder Installation erneut an:

---
// node_modules/@astrojs/starlight/routes/static/index.astro (patched)
import { paths } from '../../utils/routing';
import CommonPage from '../common.astro';
import { createHash } from 'node:crypto';
export const prerender = true;
export async function getStaticPaths() {
const docsSetHash = createHash('sha1')
.update(paths.map((p) => `${p.props?.entry?.id ?? ''}:${p.props?.entry?.digest ?? ''}`).sort().join('|'))
.digest('hex');
return paths.map((p) => ({ ...p, cacheKey: `${p.props?.entry?.digest ?? ''}|${docsSetHash}` }));
}
---
<CommonPage />

Jede Docs-Seite rendert eine gemeinsame Seitenleiste und Vor-/Zurück-Links aus dem gesamten Docs-Set. Der Schlüssel verwendet zwei Teile:

  1. Den Eintrags-Digest (den Inhalt dieser Seite)
  2. Einen Hash über jedes Dokument (Seitenleiste und Paginierung)

Wenn sich die Docs nicht geändert haben, werden alle 96 Docs-Seiten aus dem Cache wiederhergestellt.

Ergebnisse

Ein vollständiger Cold-Build gegenüber einem inkrementellen Rebuild nach dem Bearbeiten eines einzelnen Beitrags, bei Concurrency 1:

Build Wiederhergestellte Seiten Gesamtzeit
Cold (Cache geleert) 0 / 661 ~16,5s
Inkrementell (ein Beitrag bearbeitet) 637 / 661 ~10s

Rund 39% schneller, sobald der Cache warm ist. Das Bearbeiten eines Beitrags rendert 24 Seiten neu, diesen Beitrag plus die stets dynamischen Index-Seiten, und stellt die übrigen 637 wieder her 🤯

Weiterlesen