Tu proyecto compila sin problemas en tu máquina, pero en Docker, en GitHub Actions o en un hook de Git el proceso se corta con este mensaje:
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
Suele aparecer durante el paso "Running TypeScript ..." de next build o al ejecutar tsc --noEmit. Este artículo explica por qué ocurre y cómo subir el límite de memoria de Node.js en los tres lugares donde normalmente falta: el Dockerfile, el pipeline de CI y el hook pre-push.
Por qué pasa
Node.js ejecuta tu código sobre el motor V8, que limita la memoria que puede usar el heap de JavaScript. Según la documentación de Node.js, ese límite es el que determinan los recursos del sistema o, si lo indicas, el valor de --max-old-space-size. Dicho de otra forma: el límite por defecto no es el mismo en todas partes.
- En tu equipo, con bastante RAM, el límite por defecto es mayor y el build pasa.
- En un contenedor o en un runner de CI con menos memoria, el límite por defecto es menor.
- Cuando el proyecto crece, la comprobación de tipos y el empaquetado necesitan más memoria. La guía de Next.js indica que la verificación de tipos puede requerir mucha memoria en proyectos grandes.
Mientras el proyecto y el entorno no cambien, el resultado se repite, así que reintentar el job normalmente no sirve. Tampoco suele ser un error de tu código: el proyecto simplemente superó el límite. Si el consumo de memoria crece sin control entre versiones, entonces conviene perfilarlo, pero eso es un problema distinto.
Solución
La opción de V8 que lo resuelve es --max-old-space-size (en MiB). Node.js permite pasarla en la variable de entorno NODE_OPTIONS, y como las variables de entorno las heredan los procesos hijos, el valor llega también a los workers de next build y a tsc.
-
Elige un valor acorde con la RAM real. Empieza con
4096(4 GiB) y sube a6144solo si hace falta y la máquina lo soporta. Deja margen para el sistema y otros procesos; la propia documentación de Node.js sugiere, para una máquina de 2 GiB, usar 1536 para evitar el swap. -
Dockerfile (etapa de build). Define la variable justo antes del comando de build con la instrucción
ENV:FROM node:22-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . ENV NODE_OPTIONS=--max-old-space-size=4096 RUN npm run buildComo cada
FROMempieza desde cero, la variable no pasa a la etapa final a menos que esa etapa parta debuilder. Para un build no hace falta que esté en la imagen final. -
GitHub Actions. Declara la variable en el
envdel job para que la reciban todos los pasos (lint, tipos, tests y build):jobs: build: runs-on: ubuntu-latest env: NODE_OPTIONS: --max-old-space-size=4096 steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci - run: npm run lint - run: npx tsc --noEmit - run: npm test - run: npm run build -
Hook
pre-push(por ejemplo con Husky). Exporta la variable con un valor por defecto, de modo que cada persona pueda sobrescribirlo desde su entorno:# .husky/pre-push export NODE_OPTIONS="${NODE_OPTIONS:---max-old-space-size=4096}" npm run lint && npx tsc --noEmit && npm run buildLa expresión
${NODE_OPTIONS:-valor}usa el valor solo si la variable no está definida o está vacía. Si ya tienesNODE_OPTIONScon otras opciones, el hook no añade nada; en ese caso incluye la opción en tu propio valor. -
Prueba local en PowerShell para confirmar que el cambio hace efecto antes de subirlo:
$env:NODE_OPTIONS = "--max-old-space-size=4096" npm run build
Cómo verificar que quedó resuelto
Primero comprueba que el límite realmente cambió. Este comando imprime el límite del heap en MiB:
NODE_OPTIONS=--max-old-space-size=4096 node -e "console.log(Math.round(require('v8').getHeapStatistics().heap_size_limit / 1048576))"
La documentación del módulo v8 define heap_size_limit como el tamaño máximo del heap, ya sea el límite por defecto o el valor de --max_old_space_size. El número impreso será algo mayor que 4096 (en una prueba con Node.js 24 salió 4288), así que lo importante es que cambie respecto al valor sin la variable.
Para la imagen de Docker, construye solo la etapa de build y revisa la variable:
docker build --target builder -t mi-app-builder .
docker run --rm mi-app-builder printenv NODE_OPTIONS
Debe imprimir --max-old-space-size=4096. Después comprueba lo importante: que el job de CI termine en verde con todos los pasos (lint, tipos, tests y build), que git push pase el hook completo y que el build ya no muestre el FATAL ERROR.
Variantes y errores relacionados
Un valor demasiado alto empeora todo
El valor de --max-old-space-size es solo un tope: no crea memoria que la máquina no tenga. Si lo subes por encima de la RAM disponible, el sistema empieza a usar swap y el build puede quedarse prácticamente colgado. En un caso real, usar 8192 en una máquina de 7,4 GB de RAM llevó a swap y a un cuelgue de unos 25 minutos. En contenedores con límite de memoria, el kernel de Linux puede terminar procesos cuando falta memoria (lo describe la documentación de Docker sobre OOME), y entonces el síntoma es otro: el proceso desaparece sin el mensaje de V8.
No desactives la comprobación de tipos para "arreglarlo"
La guía de Next.js menciona que puedes saltarte el chequeo de tipos del build con typescript.ignoreBuildErrors, pero advierte que puede producir despliegues defectuosos. Es mejor dar más memoria y mantener el chequeo.
Otras formas de reducir el consumo en Next.js
La misma guía documenta opciones como experimental.webpackMemoryOptimizations (disponible desde la 15.0.0, experimental y pensada para Webpack) y desactivar la generación de source maps en producción. Úsalas como complemento, no como sustituto de un límite adecuado.
El CI estaba rojo y nadie lo notó
Un patrón frecuente: el workflow de pruebas y build falla, pero el despliegue vive en otro workflow que sí pasa, así que el error pasa desapercibido durante varios merges. Revisa todos los workflows del repositorio, no solo el que despliega.
Resumen
- El error indica que el proceso alcanzó el límite del heap de V8, que depende del entorno; por eso falla en CI y en Docker aunque en local pase.
- Define
NODE_OPTIONS=--max-old-space-size=4096(o 6144) en el Dockerfile, en elenvdel job de CI y en el hookpre-push. - Ajusta el valor a la RAM real: un número mayor que la memoria física causa swap y cuelgues.
- Verifica con
heap_size_limity con un pipeline completo en verde, sin desactivar la comprobación de tipos. - Vigila todos los workflows para que un CI en rojo no pase inadvertido.
Comentarios
Publicar un comentario