Beim Erstellen statischer Sites mit Astro stehen Entwickler oft vor einer Herausforderung: Datenbankaufrufe während der Static Site Generation (SSG) zu managen. Wenn mehrere Seiten dieselben Daten aus Ihrer Datenbank benötigen, stellt sich die Frage: Cached Astro die Abfrageergebnisse, oder führt es für jede Seite einen separaten Datenbankaufruf aus?
Hochwertige Starter-Kits mit integriertem Authentifizierungsfluss (Auth.js), Objekt-Uploads (AWS, Clouflare R2, Firebase Storage, Supabase Storage), integrierten Zahlungen (Stripe, LemonSqueezy), E-Mail-Verifizierungsablauf (Resend, Postmark, Sendgrid) und viel mehr . Kompatibel mit jeder Datenbank (Redis, Postgres, MongoDB, SQLite, Firestore).
Get all 3 kits Bundle ↗- Next.js Starter Kit
- SvelteKit Starter Kit
- Astro Starter Kit
One-time license · Lifetime updates
Die kurze Antwort: Astro cached Datenbankabfragen nicht automatisch über Seiten hinweg. Wenn drei verschiedene Seiten (pages/foo, pages/bar, pages/ipsum) alle db.select().from(tbl) ausführen, macht Astro während des Build-Prozesses drei separate Aufrufe an Ihre Datenbank.
In dieser Anleitung lernen Sie, wie Sie einen Cache-Mechanismus implementieren, damit Ihre Datenbank während SSG nur einmal abgefragt wird — unabhängig davon, wie viele Seiten diese Daten benötigen. Dieser Ansatz kann Ihre Build-Zeiten deutlich verbessern und die Datenbanklast reduzieren.
Voraussetzungen
Sie benötigen Folgendes:
- Node.j 20 oder höher
- Ein bestehendes Astro-Projekt
- Eine Datenbankverbindung (MySQL, PostgreSQL oder eine beliebige Datenbank)
Inhaltsverzeichnis
- Das Problem verstehen
- Die Lösung: Ein Cache auf Modulebene
- Schritt-für-Schritt-Implementierung
- Fortgeschritten: Cache mit TTL für die Entwicklung
- So funktioniert es
- Performance-Vorteile
Das Problem verstehen
Während der Static Site Generation baut Astro jede Seite unabhängig. Betrachten Sie dieses Szenario:
---import { db } from '@/db'const products = await db.select().from(productsTable)---<div>{products.length} products</div>---import { db } from '@/db'const products = await db.select().from(productsTable)---<div>Products: {products.map(p => p.name).join(', ')}</div>---import { db } from '@/db'const products = await db.select().from(productsTable)---<div>Total: {products.length}</div>Wenn Sie npm run build ausführen, führt Astro die Datenbankabfrage dreimal aus — einmal pro Seite. Bei großen Datensätzen oder langsamen Datenbanken kann das die Build-Zeit erheblich erhöhen.
Die Lösung: Ein Cache auf Modulebene
Die eleganteste Lösung ist ein Cache auf Modulebene, der über Seiten-Builds hinweg bestehen bleibt. Da Astros Build-Prozess in einem einzigen Node.js-Prozess läuft, können wir JavaScripts Modul-Caching nutzen, um Abfrageergebnisse zu teilen.
Schritt-für-Schritt-Implementierung
Schritt 1: Ein Datenbankverbindungsmodul erstellen
Richten Sie zuerst Ihre Datenbankverbindung ein. In diesem Beispiel verwenden wir Drizzle ORM mit MySQL, aber das Muster funktioniert mit jeder Datenbankbibliothek.
import { drizzle } from 'drizzle-orm/mysql2'import mysql from 'mysql2/promise'
// Create a connection poolconst pool = mysql.createPool({ host: import.meta.env.DATABASE_HOST, user: import.meta.env.DATABASE_USER, password: import.meta.env.DATABASE_PASSWORD, database: import.meta.env.DATABASE_NAME,})
// Initialize Drizzleexport const db = drizzle(pool)Schritt 2: Einen Query-Cache-Helper implementieren
Erstellen Sie nun eine Utility-Funktion, die Abfrageergebnisse auf Modulebene cached:
type CacheEntry<T> = { data: T timestamp: number}
// Module-level cache that persists across page buildsconst queryCache = new Map<string, CacheEntry<any>>()
/** * Execute a database query with caching * @param key - Unique identifier for this query * @param queryFn - Function that executes the database query * @returns Cached or fresh query results */export async function cachedQuery<T>( key: string, queryFn: () => Promise<T>): Promise<T> { // Check if we have a cached result if (queryCache.has(key)) { const cached = queryCache.get(key)! console.log(`[Cache HIT] Using cached data for: ${key}`) return cached.data as T }
// Execute the query if no cache exists console.log(`[Cache MISS] Executing query for: ${key}`) const data = await queryFn()
// Store in cache queryCache.set(key, { data, timestamp: Date.now(), })
return data}
/** * Clear the entire cache (useful for development) */export function clearCache(): void { queryCache.clear() console.log('[Cache] Cleared all cached queries')}
/** * Clear a specific cache entry */export function clearCacheKey(key: string): void { queryCache.delete(key) console.log(`[Cache] Cleared cache for: ${key}`)}Schritt 3: Die gecachte Abfrage in Ihren Seiten verwenden
Aktualisieren Sie nun Ihre Seiten, um die gecachte Abfragefunktion zu verwenden:
---import { db } from '@/lib/db'import { cachedQuery } from '@/lib/db/cache'import { productsTable } from '@/lib/db/schema'
const products = await cachedQuery('all-products', async () => { return await db.select().from(productsTable)})---
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Products - Foo</title></head><body> <h1>All Products</h1> <p>{products.length} products available</p></body></html>---import { db } from '@/lib/db'import { cachedQuery } from '@/lib/db/cache'import { productsTable } from '@/lib/db/schema'
const products = await cachedQuery('all-products', async () => { return await db.select().from(productsTable)})---
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Products - Bar</title></head><body> <h1>Product List</h1> <ul> {products.map(product => ( <li>{product.name}</li> ))} </ul></body></html>---import { db } from '@/lib/db'import { cachedQuery } from '@/lib/db/cache'import { productsTable } from '@/lib/db/schema'
const products = await cachedQuery('all-products', async () => { return await db.select().from(productsTable)})---
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Products - Ipsum</title></head><body> <h1>Product Summary</h1> <p>Total products: {products.length}</p> <p>Average price: ${(products.reduce((sum, p) => sum + p.price, 0) / products.length).toFixed(2)}</p></body></html>Schritt 4: Die Optimierung verifizieren
Wenn Sie npm run build ausführen, sehen Sie Konsolen-Logs, die zeigen, dass die Abfrage nur einmal ausgeführt wird:
[Cache MISS] Executing query for: all-products[Cache HIT] Using cached data for: all-products[Cache HIT] Using cached data for: all-productsDie erste Seite löst die Datenbankabfrage aus; alle folgenden Seiten nutzen das gecachte Ergebnis wieder.
Fortgeschritten: Cache mit TTL für die Entwicklung
Während der Entwicklung möchten Sie den Cache möglicherweise periodisch aktualisieren. Hier ist eine erweiterte Version mit Time-To-Live (TTL)-Unterstützung:
type CacheEntry<T> = { data: T timestamp: number}
const queryCache = new Map<string, CacheEntry<any>>()
// Default TTL: 5 minutes (only used in dev mode)const DEFAULT_TTL = 5 * 60 * 1000
export async function cachedQuery<T>( key: string, queryFn: () => Promise<T>, options?: { ttl?: number forceRefresh?: boolean }): Promise<T> { const { ttl = DEFAULT_TTL, forceRefresh = false } = options || {}
// Force refresh if requested if (forceRefresh) { queryCache.delete(key) }
// Check cache if (queryCache.has(key)) { const cached = queryCache.get(key)! const isExpired = import.meta.env.DEV && Date.now() - cached.timestamp > ttl
if (!isExpired) { console.log(`[Cache HIT] Using cached data for: ${key}`) return cached.data as T }
console.log(`[Cache EXPIRED] Refreshing data for: ${key}`) queryCache.delete(key) }
// Execute query console.log(`[Cache MISS] Executing query for: ${key}`) const data = await queryFn()
queryCache.set(key, { data, timestamp: Date.now(), })
return data}
/** * Get cache statistics */export function getCacheStats() { return { size: queryCache.size, keys: Array.from(queryCache.keys()), }}Sie können es nun mit benutzerdefinierter TTL verwenden:
---// Cache for 10 minutes in dev, forever during buildconst products = await cachedQuery( 'all-products', async () => db.select().from(productsTable), { ttl: 10 * 60 * 1000 })---So funktioniert es
Der Cache-Mechanismus funktioniert aufgrund der Art, wie Node.js-Module geladen werden:
-
Module Singleton: Die
queryCache-Map ist auf Modulebene definiert und damit ein Singleton, der für den gesamten Build-Prozess bestehen bleibt. -
Build-Prozess: Während
npm run buildläuft Astro in einem einzigen Node.js-Prozess. Wenn mehrere Seiten dasselbe Modul importieren, liefert Node.js dieselbe Modulinstanz zurück. -
Cache Key: Ein eindeutiger Cache-Key (z. B.
'all-products') stellt sicher, dass verschiedene Abfragen nicht kollidieren. -
Erste Abfrage: Die erste Seite, die die Daten benötigt, führt die Abfrage aus und speichert das Ergebnis im Cache.
-
Folgende Abfragen: Alle weiteren Seiten prüfen zuerst den Cache und verwenden das vorhandene Ergebnis wieder.
Performance-Vorteile
Vergleichen wir die Performance-Auswirkungen:
Ohne Caching:
- 100 Seiten × 200 ms pro Abfrage = 20 Sekunden Datenbankabfrage-Zeit
- Erhöhte Datenbanklast
- Höhere Cloud-Datenbankkosten
Mit Caching:
- 1 Abfrage × 200 ms = 200 ms Datenbankabfrage-Zeit
- Minimale Datenbanklast
- Deutliche Kostensenkung
Ein Praxisbeispiel: Sie haben einen Produktkatalog mit 1.000 Produkten und 50 Seiten, die diese Daten benötigen:
- Ohne Cache: 50 Datenbankabfragen während des Builds
- Mit Cache: 1 Datenbankabfrage während des Builds
- Eingesparte Zeit: ca. 10–30 Sekunden pro Build (abhängig von der Datenbanklatenz)
Best Practices
- Aussagekräftige Cache-Keys verwenden: Cache-Keys sollten beschreibend und eindeutig sein:
// Goodawait cachedQuery('products-active', () => db.select().from(products).where(eq(products.active, true)))await cachedQuery('products-all', () => db.select().from(products))
// Badawait cachedQuery('data', () => db.select().from(products))- Auf der richtigen Ebene cachen: Cachen Sie Daten, die wirklich seitenübergreifend geteilt werden. Cachen Sie keine seiten-spezifischen Abfragen:
// Good: Shared dataawait cachedQuery('site-settings', () => db.select().from(settings))
// Bad: Page-specific dataawait cachedQuery(`user-${userId}`, () => db.select().from(users).where(eq(users.id, userId)))- Cache-Nutzung überwachen: Logging hinzufügen, um die Cache-Effektivität zu verstehen:
import { getCacheStats } from '@/lib/db/cache'
// After build, log cache statisticsconsole.log('Cache statistics:', getCacheStats())- Speicherverbrauch berücksichtigen: Bei sehr großen Datensätzen Cache-Größenlimits implementieren:
const MAX_CACHE_SIZE = 100 // Maximum number of cached queries
if (queryCache.size >= MAX_CACHE_SIZE) { // Remove oldest entry const oldestKey = queryCache.keys().next().value queryCache.delete(oldestKey)}Fazit
In dieser Anleitung haben Sie gelernt, wie Sie Datenbankaufrufe während der Static Site Generation in Astro optimieren, indem Sie einen Query-Cache auf Modulebene implementieren. Dieser Ansatz stellt sicher, dass gemeinsame Daten während des Build-Prozesses nur einmal abgerufen werden — das verbessert die Build-Performance deutlich und reduziert die Datenbanklast. Dieses Muster ist besonders wertvoll, wenn Sie content-lastige Sites mit Hunderten oder Tausenden von Seiten bauen, die gemeinsame Datenquellen nutzen.
Bei Fragen oder Anmerkungen erreichen Sie mich gerne auf Twitter.