Datenbankabfragen in Astro SSG wiederverwenden
LaunchFast Logo LaunchFast
Blog
2.345 Wörter 12 Min. Lesezeit

Datenbankabfragen in Astro SSG wiederverwenden

Erfahren Sie, wie Sie Ergebnisse von Datenbankabfragen während der Static Site Generation in Astro über mehrere Seiten hinweg wiederverwenden, um redundante Datenbankaufrufe zu vermeiden und die Build-Performance zu verbessern.

Rishi Raj Jain
Rishi Raj Jain Autor
Optimize Database Calls During SSG in Astro

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?

Sponsored

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 ↗

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

Während der Static Site Generation baut Astro jede Seite unabhängig. Betrachten Sie dieses Szenario:

src/pages/foo.astro
---
import { db } from '@/db'
const products = await db.select().from(productsTable)
---
<div>{products.length} products</div>
src/pages/bar.astro
---
import { db } from '@/db'
const products = await db.select().from(productsTable)
---
<div>Products: {products.map(p => p.name).join(', ')}</div>
src/pages/ipsum.astro
---
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.

src/lib/db/index.ts
import { drizzle } from 'drizzle-orm/mysql2'
import mysql from 'mysql2/promise'
// Create a connection pool
const 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 Drizzle
export const db = drizzle(pool)

Schritt 2: Einen Query-Cache-Helper implementieren

Erstellen Sie nun eine Utility-Funktion, die Abfrageergebnisse auf Modulebene cached:

src/lib/db/cache.ts
type CacheEntry<T> = {
data: T
timestamp: number
}
// Module-level cache that persists across page builds
const 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:

src/pages/foo.astro
---
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>
src/pages/bar.astro
---
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>
src/pages/ipsum.astro
---
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:

Terminal window
[Cache MISS] Executing query for: all-products
[Cache HIT] Using cached data for: all-products
[Cache HIT] Using cached data for: all-products

Die 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:

src/lib/db/cache.ts
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 build
const 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:

  1. Module Singleton: Die queryCache-Map ist auf Modulebene definiert und damit ein Singleton, der für den gesamten Build-Prozess bestehen bleibt.

  2. Build-Prozess: Während npm run build läuft Astro in einem einzigen Node.js-Prozess. Wenn mehrere Seiten dasselbe Modul importieren, liefert Node.js dieselbe Modulinstanz zurück.

  3. Cache Key: Ein eindeutiger Cache-Key (z. B. 'all-products') stellt sicher, dass verschiedene Abfragen nicht kollidieren.

  4. Erste Abfrage: Die erste Seite, die die Daten benötigt, führt die Abfrage aus und speichert das Ergebnis im Cache.

  5. 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

  1. Aussagekräftige Cache-Keys verwenden: Cache-Keys sollten beschreibend und eindeutig sein:
// Good
await cachedQuery('products-active', () => db.select().from(products).where(eq(products.active, true)))
await cachedQuery('products-all', () => db.select().from(products))
// Bad
await cachedQuery('data', () => db.select().from(products))
  1. Auf der richtigen Ebene cachen: Cachen Sie Daten, die wirklich seitenübergreifend geteilt werden. Cachen Sie keine seiten-spezifischen Abfragen:
// Good: Shared data
await cachedQuery('site-settings', () => db.select().from(settings))
// Bad: Page-specific data
await cachedQuery(`user-${userId}`, () => db.select().from(users).where(eq(users.id, userId)))
  1. Cache-Nutzung überwachen: Logging hinzufügen, um die Cache-Effektivität zu verstehen:
import { getCacheStats } from '@/lib/db/cache'
// After build, log cache statistics
console.log('Cache statistics:', getCacheStats())
  1. 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.

Weiterlesen