Reutilizar consultas de base de datos en Astro SSG
LaunchFast Logo LaunchFast
Blog
2605 palabras 14 min de lectura

Reutilizar consultas de base de datos en Astro SSG

Aprenda cómo reutilizar los resultados de consultas de base de datos en varias páginas durante la Static Site Generation en Astro para minimizar llamadas redundantes a la base de datos y mejorar el rendimiento del build.

Rishi Raj Jain
Rishi Raj Jain Autor
Optimizar llamadas a la base de datos durante SSG en Astro

Al construir sitios estáticos con Astro, un desafío habitual para los desarrolladores es gestionar las llamadas a la base de datos durante la Static Site Generation (SSG). Si tiene varias páginas que necesitan los mismos datos de su base de datos, puede preguntarse: ¿Astro almacena en caché los resultados de las consultas o realiza una llamada separada a la base de datos por cada página?

Sponsored

Kits de inicio de alta calidad con flujo de autenticación integrada (Auth.js), carga de objetos (AWS, Clouflare R2, Firebase Storage, Supabase Storage), pagos integrados (Stripe, LemonSqueezy), flujo de verificación de correo electrónico (Resend, Postmark, Sendgrid) y mucho más . Compatible con cualquier base de datos (Redis, Postgres, MongoDB, SQLite, Firestore).

Get all 3 kits Bundle ↗

One-time license · Lifetime updates

La respuesta corta: Astro NO almacena en caché automáticamente las consultas de base de datos entre páginas. Si tres páginas distintas (pages/foo, pages/bar, pages/ipsum) ejecutan todas db.select().from(tbl), Astro hará tres llamadas separadas a su base de datos durante el proceso de build.

En esta guía, aprenderá a implementar un mecanismo de caché para asegurarse de que su base de datos se consulte solo una vez durante SSG, sin importar cuántas páginas necesiten esos datos. Este enfoque puede mejorar drásticamente los tiempos de build y reducir la carga sobre la base de datos.

Requisitos previos

Necesitará lo siguiente:

  • Node.js 20 o posterior
  • Un proyecto Astro existente
  • Una conexión a base de datos (MySQL, PostgreSQL o cualquier otra)

Tabla de contenidos

Entender el problema

Durante la Static Site Generation, Astro construye cada página de forma independiente. Considere este escenario:

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>

Cuando ejecute npm run build, Astro ejecutará la consulta a la base de datos tres veces: una por cada página. Para conjuntos de datos grandes o bases de datos lentas, esto puede aumentar significativamente los tiempos de build.

La solución: una caché a nivel de módulo

La solución más elegante es crear una caché a nivel de módulo que persista entre los builds de las páginas. Como el proceso de build de Astro se ejecuta en un único proceso de Node.js, podemos aprovechar el almacenamiento en caché de módulos de JavaScript para compartir los resultados de las consultas.

Implementación paso a paso

Paso 1: Crear un módulo de conexión a la base de datos

Primero, configure su conexión a la base de datos. En este ejemplo usaremos Drizzle ORM con MySQL, pero el patrón funciona con cualquier biblioteca de base de datos.

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)

Paso 2: Implementar un helper de caché de consultas

Ahora, cree una función de utilidad que almacene en caché los resultados de las consultas a nivel de módulo:

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}`)
}

Paso 3: Usar la consulta en caché en sus páginas

Ahora, actualice sus páginas para usar la función de consulta en caché:

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>

Paso 4: Verificar la optimización

Cuando ejecute npm run build, verá logs en consola que indican que la consulta solo se ejecuta una vez:

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

La primera página dispara la consulta a la base de datos y las páginas posteriores reutilizan el resultado en caché.

Avanzado: caché con TTL para desarrollo

Durante el desarrollo, puede querer refrescar la caché periódicamente. Aquí tiene una versión mejorada con soporte de Time-To-Live (TTL):

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()),
}
}

Ahora puede usarlo con un TTL personalizado:

---
// Cache for 10 minutes in dev, forever during build
const products = await cachedQuery(
'all-products',
async () => db.select().from(productsTable),
{ ttl: 10 * 60 * 1000 }
)
---

Cómo funciona

El mecanismo de caché funciona gracias a cómo Node.js carga los módulos:

  1. Singleton de módulo: el Map queryCache se define a nivel de módulo, lo que lo convierte en un singleton que persiste durante todo el proceso de build.

  2. Proceso de build: durante npm run build, Astro se ejecuta en un único proceso de Node.js. Cuando varias páginas importan el mismo módulo, Node.js devuelve la misma instancia del módulo.

  3. Clave de caché: usar una clave de caché única (por ejemplo, 'all-products') garantiza que distintas consultas no colisionen.

  4. Primera consulta: la primera página que necesita los datos ejecuta la consulta y almacena el resultado en la caché.

  5. Consultas posteriores: todas las páginas siguientes comprueban primero la caché y reutilizan el resultado existente.

Beneficios de rendimiento

Comparemos el impacto en el rendimiento:

Sin caché:

  • 100 páginas × 200 ms por consulta = 20 segundos de tiempo de consulta a la base de datos
  • Mayor carga sobre la base de datos
  • Costos más altos en bases de datos en la nube

Con caché:

  • 1 consulta × 200 ms = 200 ms de tiempo de consulta a la base de datos
  • Carga mínima sobre la base de datos
  • Reducción significativa de costos

En un ejemplo del mundo real, si tiene un catálogo de productos con 1.000 productos y 50 páginas que necesitan esos datos:

  • Sin caché: 50 consultas a la base de datos durante el build
  • Con caché: 1 consulta a la base de datos durante el build
  • Tiempo ahorrado: ~10–30 segundos por build (según la latencia de la base de datos)

Buenas prácticas

  1. Use claves de caché descriptivas: las claves de caché deben ser descriptivas y únicas:
// 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. Almacene en caché al nivel correcto: almacene en caché datos que realmente se comparten entre páginas. No almacene en caché consultas específicas de una página:
// 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. Supervise el uso de la caché: añada logging para entender la efectividad de la caché:
import { getCacheStats } from '@/lib/db/cache'
// After build, log cache statistics
console.log('Cache statistics:', getCacheStats())
  1. Considere el uso de memoria: para conjuntos de datos muy grandes, considere implementar límites de tamaño de caché:
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)
}

Conclusión

En esta guía, aprendió a optimizar las llamadas a la base de datos durante la Static Site Generation en Astro implementando una caché de consultas a nivel de módulo. Este enfoque garantiza que los datos compartidos se obtengan solo una vez durante el proceso de build, mejorando drásticamente el rendimiento del build y reduciendo la carga sobre la base de datos. Este patrón es especialmente valioso al construir sitios con mucho contenido y cientos o miles de páginas que comparten fuentes de datos comunes.

Si tiene preguntas o comentarios, no dude en contactarme en Twitter.

Sigue leyendo