En monorrepositorios empresariales modernos y aplicaciones a gran escala basadas en Next.js 15+, la duración de los pipelines de Integración Continua (Continuous Integration, CI) dicta de manera directa la velocidad de ingeniería y el Time-to-Market. Un pipeline de CI convencional e ingenuo, que realiza una instalación limpia con npm install y compila el proyecto completo desde cero, toma entre 8 y 15 minutos en promedio. Los cuellos de botella fundamentales de este modelo derivan de la arquitectura de los ejecutores efímeros (Ephemeral Runners) de GitHub Actions:
ubuntu-latest con 2 vCPUs y 7 GB de RAM). Descargar gigabytes de dependencias en node_modules y descomprimir repetidamente archivos tar satura los límites de I/O del subsistema de disco virtual de Azure asignado al runner..next/cache, la CPU desperdicia cientos de segundos recalculando archivos idénticos en cada ejecución.@repo/ui no ha sufrido modificaciones, volver a compilarlo y probarlo constituye un consumo irracional de minutos de cómputo del runner.La optimización de un pipeline de CI de alto rendimiento se estructura en tres niveles de almacenamiento en caché:
npm o yarn classic, que duplican dependencias físicamente en cada directorio, pnpm utiliza un almacenamiento direccionable por contenido global (Content-Addressable Storage, CAS). En el runner se debe almacenar en caché el directorio del almacén virtual ~/.local/share/pnpm/store/v3, y no la carpeta local node_modules. Ante un Cache HIT, la resolución del árbol de dependencias toma de 8 a 15 segundos mediante enlaces duros (hardlinks), en lugar de varios minutos de descarga..next/cache). La caché interna de Next.js conserva binarios de SWC, fragmentos de webpack/turbopack y rutas estáticas precalculadas (SSG). Restaurar este directorio permite al compilador reutilizar transformaciones AST y emitir únicamente deltas de cambio.>>> FULL TURBO).El flujo de ejecución del pipeline se visualiza como un filtro multicapa que valida las entradas de caché antes de delegar trabajo a los núcleos de cómputo del runner:
[ Evento GitHub: Pull Request / Push ]
|
v
+------------------------------+
| Init Runner GitHub Actions |
| (VM Efímera: ubuntu-latest) |
+------------------------------+
|
[ Git Checkout v4 ]
|
v
+-------------------------------------------------------------------+
| 1. Restauración de caché de dependencias pnpm |
| Clave: setup-pnpm-store-${{ runner.os }}-${{ hash(pnpm-lock) }}|
| Match? ---> [ HIT: Enlaces desde CAS en 10s ] |
| ---> [ MISS: Descarga de registry + escritura en CAS ] |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| 2. Restauración de caché incremental de Next.js |
| Clave: nextjs-${{ runner.os }}-${{ hash(lock) }}-${{ commit }} |
| Restore-keys: nextjs-${{ runner.os }}-${{ hash(lock) }}- |
| Match? ---> [ HIT / PARCIAL: Restaurar .next/cache ] |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| 3. Ejecución de Turborepo con Remote Caching |
| Cálculo del hash del grafo de tareas (Entradas -> SHA-256) |
| ¿Coincidencia en Remote Cache? |
| ├── [ HIT: Descarga de artefactos desde S3 / Vercel -> 0s ] |
| └── [ MISS: Compilación local -> Envío a Remote Cache ] |
+-------------------------------------------------------------------+
|
v
+---------------------------+
| Éxito / Estado de Pipeline|
+---------------------------+.next/cache preserva prerrenderizados estáticos y caché de SWC, evitando agotar los minutos mensuales incluidos de GitHub Actions.@repo/ui, Turborepo solo recompila las aplicaciones consumidoras afectadas. Otras 5 aplicaciones no alteradas obtienen inmediatamente un resultado [FULL TURBO] desde la caché.Lograr el máximo rendimiento exige claves de caché deterministas, aislamiento estricto y una configuración precisa del entorno de ejecución.
Un antipatrón recurrente es almacenar directamente la carpeta node_modules en actions/cache. En pnpm, node_modules está constituido por enlaces simbólicos y duros vinculados al almacén virtual centralizado. Comprimir este directorio infla los archivos tar o rompe los enlaces relativos al desempaquetarse en otro runner.
La estrategia adecuada es almacenar en caché el CAS global de pnpm (pnpm store path) empleando pnpm/action-setup o actions/cache@v4.
- name: Instalar pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
run_install: false
- name: Obtener ruta de pnpm store
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Restaurar caché de pnpm Store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Instalar dependencias con lockfile estricto
run: pnpm install --frozen-lockfile --prefer-offlineEl flag --frozen-lockfile garantiza que el CI falle si pnpm-lock.yaml difiere de package.json. El flag --prefer-offline ordena a pnpm consultar el CAS local antes de realizar peticiones de red al registro de paquetes.
Next.js almacena el estado de compilación dentro de <app-dir>/.next/cache. Esto incluye:
La clave de caché debe combinar el hash de pnpm-lock.yaml (ya que cambios en versiones mayores invalidan los binarios) y el SHA del commit:
- name: Restaurar caché incremental de Next.js
uses: actions/cache@v4
with:
path: |
${{ github.workspace }}/apps/web/.next/cache
# Clave compuesta con lockfile y commit para determinismo
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
# Fallback para recuperar el estado de compilación previo en la rama
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-Al ejecutar turbo run build, Turborepo evalúa una firma compuesta:
Hash = SHA256(DefiniciónTarea + ContenidoArchivos + VariablesEntorno + SalidasDependencias)
Para activar Remote Cache en CI, se proporcionan las variables de entorno TURBO_TOKEN y TURBO_TEAM (para Vercel Remote Cache) o TURBO_API (para alternativas autoalojadas como duesee/turbocache).
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"globalEnv": ["NODE_ENV", "NEXT_PUBLIC_API_URL"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": [
"$TURBO_DEFAULT$",
".env.production"
],
"outputs": [
".next/**",
"!.next/cache/**",
"dist/**"
],
"env": [
"STRIPE_PUBLIC_KEY",
"NEXT_PUBLIC_ANALYTICS_ID"
]
},
"lint": {
"outputs": []
},
"typecheck": {
"outputs": []
}
}
}Observe la regla: "outputs": [".next/**", "!.next/cache/**"]. Excluir .next/cache de las salidas de Turborepo es fundamental. La caché del compilador se gestiona con actions/cache, reduciendo el artefacto transferido por Turborepo de 600 MB a 50 MB.
Aprovisionamiento de VM ubuntu-latest; clonación superficial con fetch-depth: 2 para cálculo exacto de diferencias Git.
Búsqueda en el CAS por hash del lockfile. Ofrece un 95% de acierto, omitiendo la descarga de archivos comprimidos.
Carga de binarios SWC y chunks de compilación mediante restore-keys de la compilación previa más reciente.
Evaluación del grafo de tareas. Se ejecutan lint, typecheck y build en paralelo. Paquetes intactos obtienen estado FULL TURBO.
Si hubo dependencias nuevas o fuentes modificadas, los deltas se comprimen y cargan al almacenamiento de GitHub Actions.
A continuación se muestra un workflow .github/workflows/ci.yml industrial completo para monorrepositorios con Next.js 15+, con soporte para caché en 3 capas, control de concurrencia y optimización de memoria.
name: CI Pipeline
on:
push:
branches:
- main
- develop
pull_request:
types:
- opened
- synchronize
- reopened
# Cancelación de ejecuciones redundantes en la misma rama para optimizar cuotas
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
NODE_OPTIONS: "--max-old-space-size=6144"
jobs:
validate:
name: Validation & Typecheck
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
actions: write
steps:
- name: Checkout del Repositorio
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Configurar Entorno Node.js
uses: actions/setup-node@v4
with:
node-version: 22.x
- name: Instalar pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
run_install: false
- name: Resolver Ruta de pnpm Store
id: pnpm-cache-dir
shell: bash
run: |
echo "dir=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Restaurar Caché de pnpm Store
uses: actions/cache@v4
id: pnpm-cache
with:
path: ${{ steps.pnpm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Instalar Dependencias
run: pnpm install --frozen-lockfile --prefer-offline
- name: Restaurar Caché del Compilador Next.js (.next/cache)
uses: actions/cache@v4
with:
path: |
${{ github.workspace }}/apps/*/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-
- name: Ejecutar Quality Gates con Turborepo
run: pnpm turbo run lint typecheck --filter=...[origin/main]
- name: Compilación Incremental con Turborepo
run: pnpm turbo run build --filter=...[origin/main]
env:
NEXT_TELEMETRY_DISABLED: 1
NODE_ENV: productionUn error frecuente al migrar de npm a pnpm es guardar node_modules en actions/cache. En pnpm, node_modules contiene enlaces simbólicos que apuntan a un almacén virtual centralizado. Al empaquetar en tar.gz, el compresor expande los enlaces simbólicos (multiplicando el tamaño a decenas de gigabytes) o corrompe los caminos relativos al desempaquetarse en otro runner.
# INCORRECTO: Intentar almacenar la carpeta node_modules local
- uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('pnpm-lock.yaml') }}
# CORRECTO: Almacenar en caché el CAS global de pnpm
- name: Obtener ruta del store
id: pnpm-cache
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}Guardar en caché el directorio .next en su totalidad. Esta carpeta contiene los manifiestos de rutas y artefactos finales (.next/static, .next/server, routes-manifest.json) asociados a un commit específico. Restaurar todo el directorio causa corrupción de manifiestos, discordancia de hashes y el fallo fatal ChunkLoadError en producción.
# INCORRECTO: Guardar en caché el directorio completo de build
- uses: actions/cache@v4
with:
path: apps/web/.next
key: ${{ runner.os }}-next-build-${{ github.sha }}
# CORRECTO: Guardar exclusivamente los datos intermedios del compilador
- uses: actions/cache@v4
with:
path: apps/web/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-Si la compilación de Next.js depende de NEXT_PUBLIC_API_URL, pero dicha variable no se declara explícitamente en env o globalEnv dentro de turbo.json, Turborepo devolverá un resultado FULL TURBO falso compilado contra una URL desactualizada, enviando a producción código configurado de forma errónea.
// INCORRECTO: Las variables no están integradas en el cálculo de hash
{
"tasks": {
"build": {
"outputs": [".next/**", "!.next/cache/**"]
}
}
}
// CORRECTO: Declaración explícita de variables que afectan el resultado
{
"tasks": {
"build": {
"env": ["NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_SENTRY_DSN"],
"outputs": [".next/**", "!.next/cache/**"]
}
}
}Si se envían múltiples commits a una rama de PR sin concurrency.cancel-in-progress: true, GitHub Actions ejecutará compilaciones simultáneas innecesarias. Cada runner cargará un archivo de más de 500 MB, superando con rapidez el límite de 10 GB por repositorio y activando la expulsión LRU (Least Recently Used) que borra la caché base de main.
Medir los tiempos de ejecución actuales: duración de pnpm install, tiempos de compilación de Next.js y tamaño de artefactos.
Sustituir la caché de node_modules por ~/.local/share/pnpm/store/v3 junto con los flags --frozen-lockfile y --prefer-offline.
Preservar el directorio .next/cache combinando claves de lockfile y SHA de commit con prefijos restore-keys.
Configurar secretos TURBO_TOKEN y TURBO_TEAM y definir estrictamente las exclusiones de salidas en turbo.json.
Activar cancel-in-progress y filtrar ejecuciones de Turborepo contra la rama base (...[origin/main]).