Reduje mi tiempo de rebuild incremental en aproximadamente un 39%, de unos 16.5 segundos a unos 10, al habilitar los builds estáticos incrementales en Astro 7.2.
Este sitio prerenderiza 661 páginas en cada build de producción: 70 posts de blog en tres idiomas, documentación, páginas de características y páginas de casos de uso. La mayoría de esas páginas no cambian entre despliegues. Antes de este cambio, editar un solo post seguía re-renderizando las 661.
Astro 7.2 permite que una ruta prerenderizada devuelva un cacheKey desde getStaticPaths. Astro restaura la página del build anterior cuando su grafo de módulos y esa clave permanecen sin cambios. Después de editar un solo post, 637 de 661 páginas se restauran desde la caché. Este post explica cómo lo configuré.
Requisitos previos
- Astro 7.2 o posterior
- Rutas prerenderizadas (
export const prerender = true) - Bun para los pasos del patch de Starlight más abajo (npm/yarn/pnpm tienen herramientas de patch equivalentes)
Paso 1: Habilita el flag
Para poder usar los builds incrementales, el primer paso es actualizar a 7.2 con el siguiente comando:
bun add astro@latestTen en cuenta que el modo es experimental. Actívalo en astro.config.mjs con el siguiente cambio:
export default defineConfig({ experimental: { incrementalBuild: true, },})Después de habilitar el flag, los dos primeros builds tardaron lo mismo. No se restauró ninguna página desde la caché. ¡Aquí entra la concurrencia!
Paso 2: Corrige la configuración de concurrencia
Astro desactiva la caché incremental cuando las páginas se renderizan en paralelo:
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 }}Yo tenía build.concurrency configurado al número de CPUs. El renderizado en paralelo y la restauración de caché no funcionan juntos, así que eliminé mi override:
// build: {// concurrency: Math.min(os.cpus().length, 12), // removed// }Con la concurrencia de vuelta en el valor por defecto de 1, pierdes el renderizado en paralelo pero ganas la restauración de caché. Ten en cuenta que esto solo ayuda si la mayor parte de tu sitio no cambia entre builds.
Paso 3: Añade una cache key al blog
Una ruta se une a la caché devolviendo un cacheKey desde getStaticPaths. Astro reutiliza una página solo cuando su grafo de módulos y esta clave coinciden con el build anterior.
Las entradas de content collections tienen un digest que cambia cuando cambia el origen de la entrada:
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 por sí solo no es suficiente para la plantilla de mi blog.
Paso 4: Incluye todo lo que renderiza la página
Una página de blog aquí renderiza:
- Tres posts relacionados (vistas previas de título y descripción)
- El anuncio del patrocinador actual en la barra lateral
Ninguno forma parte de post.digest. Con solo el digest del post como clave, la caché sirve una página desactualizada cuando cambia un post relacionado o cambia la campaña del patrocinador.
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('|')}El blog en los tres idiomas y las páginas de características usan esta clave. La caché se invalida cuando cambia el cuerpo del post, una vista previa relacionada o el patrocinador activo.
Paso 5: Páginas de casos de uso
Las páginas de casos de uso son el grupo más grande del sitio: más de 300 entre todos los idiomas. No son entradas de content collections. Se generan a partir de TypeScript, una página por cada framework e integración.
Los datos son TypeScript importado, así que ya están en el grafo de módulos de la ruta. El hash de dependencias de Astro invalida cada página de caso de uso cuando cambian los datos, con o sin cacheKey. El grafo de módulos no cubre el anuncio del patrocinador, así que la clave solo necesita el slug y el patrocinador activo:
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('|')}Paso 6: Páginas de documentación (patch de Starlight)
La documentación funciona con Starlight, que inyecta una ruta catch-all [...slug]. No hay ningún getStaticPaths en mi código al que añadir una clave.
Primero intenté sombrear la ruta con src/pages/[...slug].astro. La documentación se restauró desde la caché, pero Astro registró un conflicto de rutas para cada ruta de documentación. Dos rutas reclamaban el mismo patrón.
La solución es aplicar un patch a la ruta inyectada por Starlight y hacerle seguimiento con bun patch:
bun patch @astrojs/starlight# edit node_modules/@astrojs/starlight/routes/static/index.astrobun patch --commit 'node_modules/@astrojs/starlight'Esto escribe un patch en patches/ y una entrada patchedDependencies en package.json. Bun lo reaplica en cada instalación:
---// 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 />Cada página de documentación renderiza una barra lateral compartida y enlaces anterior/siguiente de todo el conjunto de documentación. La clave usa dos partes:
- El digest de la entrada (el contenido de esta página)
- Un hash sobre cada documento (barra lateral y paginación)
Si la documentación no cambió, las 96 páginas de documentación se restauran desde la caché.
Resultados
Un build en frío completo frente a un rebuild incremental después de editar un solo post, con concurrencia 1:
| Build | Páginas restauradas | Tiempo total |
|---|---|---|
| En frío (caché limpia) | 0 / 661 | ~16.5s |
| Incremental (un post editado) | 637 / 661 | ~10s |
Alrededor de un 39% más rápido una vez que la caché está caliente. Editar un post re-renderiza 24 páginas, ese post más las páginas índice siempre dinámicas, y restaura las otras 637 🤯