Mejores prácticas de producción: rendimiento y confiabilidad
import CodeTabs from '@components/primitives/Tabs/CodeTabs.astro';Este artículo discute las mejores prácticas de rendimiento y confiabilidad para aplicaciones Express desplegadas en producción.
Este tema claramente cae en el mundo “devops”, abarcando tanto el desarrollo tradicional como las operaciones. En consecuencia, la información se divide en dos partes:
- Things to do in your code (the dev part):
- Things to do in your environment / setup (the ops part):
Cosas que hacer en tu código
Aquí hay algunas cosas que puedes hacer en tu código para mejorar el rendimiento de tu aplicación:
- Usa compresión gzip
- No uses funciones síncronas
- Haz logging correctamente
- Maneja las excepciones correctamente
Usa compresión gzip
La compresión gzip puede disminuir enormemente el tamaño del cuerpo de la respuesta y por ende aumentar la velocidad de una app web. Usa el middleware compression para la compresión gzip en tu app de Express. Por ejemplo:
const compression = require('compression');const express = require('express');const app = express();
app.use(compression());import compression from 'compression';import express from 'express';
const app = express();
app.use(compression());Para un sitio web de alto tráfico en producción, la mejor forma de implementar la compresión es a nivel de reverse proxy (consulta Usa un reverse proxy). En ese caso, no necesitas usar el middleware de compresión. Para más detalles sobre cómo habilitar la compresión gzip en Nginx, consulta Module ngx_http_gzip_module en la documentación de Nginx.
No uses funciones síncronas
Las funciones y métodos síncronos bloquean el proceso en ejecución hasta que retornan. Una sola llamada a una función síncrona podría retornar en unos pocos microsegundos o milisegundos, sin embargo en sitios web de alto tráfico, estas llamadas se acumulan y reducen el rendimiento de la app. Evita su uso en producción.
Aunque Node y muchos módulos proporcionan versiones síncronas y asíncronas de sus funciones, siempre usa la versión asíncrona en producción. La única vez que se puede justificar una función síncrona es al inicio inicial.
Puedes usar el flag de línea de comandos --trace-sync-io para imprimir una advertencia y un stack trace siempre que tu aplicación use una API síncrona. Por supuesto, no querrías usar esto en producción, sino más bien para asegurar que tu código esté listo para producción. Consulta la documentación de opciones de línea de comandos de node para más información.
Haz logging correctamente
En general, hay dos razones para hacer logging desde tu app: para depuración y para registrar la actividad de la app (esencialmente, todo lo demás). Usar console.log() o console.error() para imprimir mensajes de log en la terminal es una práctica común en desarrollo. Pero estas funciones son síncronas cuando el destino es una terminal o un archivo, por lo que no son adecuadas para producción, a menos que redirijas la salida a otro programa.
Para depuración
Si estás haciendo logging para depuración, entonces en lugar de usar console.log(), usa un módulo especial de depuración como debug. Este módulo te permite usar la variable de entorno DEBUG para controlar qué mensajes de depuración se envían a console.error(), si acaso. Para mantener tu app puramente asíncrona, aún querrías redirigir console.error() a otro programa. Pero entonces, no vas a depurar en producción, ¿verdad?
Para actividad de la app
Si estás registrando la actividad de la app (por ejemplo, seguimiento de tráfico o llamadas a la API), en lugar de usar console.log(), usa una librería de logging como Pino, que es la opción más rápida y eficiente disponible.
Maneja las excepciones correctamente
Las apps de Node se caen cuando encuentran una excepción no capturada. No manejar las excepciones y tomar las acciones apropiadas hará que tu app de Express se caiga y quede fuera de línea. Si sigues el consejo en Asegúrate de que tu app se reinicie automáticamente a continuación, entonces tu app se recuperará de una caída. Afortunadamente, las apps de Express típicamente tienen un tiempo de inicio corto. Sin embargo, quieres evitar caer en primer lugar, y para hacer eso, necesitas manejar las excepciones correctamente.
Para asegurar que manejas todas las excepciones, usa las siguientes técnicas:
Antes de profundizar en estos temas, debes tener una comprensión básica del manejo de errores en Node/Express: usar callbacks con error como primer argumento, y propagar errores en el middleware. Node usa una convención de “error-first callback” para retornar errores de funciones asíncronas, donde el primer parámetro de la función callback es el objeto de error, seguido por los datos de resultado en los parámetros subsiguientes. Para indicar que no hay error, pasa null como primer parámetro. La función callback debe correspondientemente seguir la convención de error-first callback para manejar el error de forma significativa. Y en Express, la mejor práctica es usar la función next() para propagar errores a través de la cadena de middleware.
Para más sobre los fundamentos del manejo de errores, consulta:
Usa try-catch
Try-catch es una construcción del lenguaje JavaScript que puedes usar para capturar excepciones en código síncrono. Usa try-catch, por ejemplo, para manejar errores de análisis JSON como se muestra a continuación.
Aquí hay un ejemplo de uso de try-catch para manejar una excepción potencial que podría caer el proceso. Esta función de middleware acepta un parámetro de campo query llamado “params” que es un objeto JSON.
app.get('/search', (req, res) => { // Simulating async operation setImmediate(() => { const jsonStr = req.query.params; try { const jsonObj = JSON.parse(jsonStr); res.send('Success'); } catch (e) { res.status(400).send('Invalid JSON string'); } });});Sin embargo, try-catch funciona solo para código síncrono. Como la plataforma Node es principalmente asíncrona (particularmente en un entorno de producción), try-catch no capturará muchas excepciones.
Usa promesas
Cuando se lanza un error en una función async o se espera una promesa rechazada dentro de una función async, esos errores se pasarán al manejador de errores como si se llamara next(err)
app.get('/', async (req, res, next) => { const data = await userData(); // If this promise fails, it will automatically call `next(err)` to handle the error.
res.send(data);});
app.use((err, req, res, next) => { res.status(err.status ?? 500).send({ error: err.message });});Además, puedes usar funciones asíncronas para tu middleware, y el router manejará los errores si la promesa falla, por ejemplo:
app.use(async (req, res, next) => { req.locals.user = await getUser(req);
next(); // This will be called if the promise does not throw an error.});La mejor práctica es manejar los errores lo más cerca posible del sitio. Así que aunque esto ahora se maneja en el router, es mejor capturar el error en el middleware y manejarlo sin depender de un middleware separado de manejo de errores.
Lo que no debes hacer
Una cosa que no debes hacer es escuchar el evento uncaughtException, emitido cuando una excepción sube hasta el event loop. Añadir un event listener para uncaughtException cambiará el comportamiento predeterminado del proceso que encuentra una excepción; el proceso continuará ejecutándose a pesar de la excepción. Esto podría parecer una buena forma de evitar que tu app se caiga, pero continuar ejecutando la app después de una excepción no capturada es una práctica peligrosa y no se recomienda, porque el estado del proceso se vuelve poco confiable e impredecible.
Además, usar uncaughtException es oficialmente reconocido como rudimentario. Así que escuchar uncaughtException es simplemente una mala idea. Por esto recomendamos cosas como múltiples procesos y supervisores: caer y reiniciarse es a menudo la forma más confiable de recuperarse de un error.
Tampoco recomendamos usar domains. Generalmente no resuelve el problema y es un módulo deprecado.
Cosas que hacer en tu entorno / configuración
Aquí hay algunas cosas que puedes hacer en el entorno de tu sistema para mejorar el rendimiento de tu app:
- Establece NODE_ENV a “production”
- Asegúrate de que tu app se reinicie automáticamente
- Ejecuta tu app en un cluster
- Cachéa los resultados de las peticiones
- Usa un balanceador de carga
- Usa un reverse proxy
Establece NODE_ENV a “production”
La variable de entorno NODE_ENV especifica el entorno en el que se ejecuta una aplicación (usualmente, development o production). Una de las cosas más simples que puedes hacer para mejorar el rendimiento es establecer NODE_ENV a production.
Establecer NODE_ENV a “production” hace que Express:
- Cachée las plantillas de vistas.
- Cachée los archivos CSS generados desde extensiones CSS.
- Genere mensajes de error menos verbosos.
Las pruebas indican que solo con esto se puede mejorar el rendimiento de la app por un factor de tres.
Si necesitas escribir código específico para un entorno, puedes verificar el valor de NODE_ENV con process.env.NODE_ENV. Ten en cuenta que verificar el valor de cualquier variable de entorno incurre en una penalización de rendimiento, por lo que debe hacerse con moderación.
En desarrollo, típicamente estableces variables de entorno en tu shell interactivo, por ejemplo usando export o tu archivo .bash_profile. Pero en general, no deberías hacer eso en un servidor de producción; en su lugar, usa el init system de tu SO (systemd). La siguiente sección proporciona más detalles sobre el uso de tu init system en general, pero establecer NODE_ENV es tan importante para el rendimiento (y fácil de hacer) que se destaca aquí.
Con systemd, usa la directiva Environment en tu archivo unit. Por ejemplo:
Environment=NODE_ENV=productionPara más información, consulta Using Environment Variables In systemd Units.
Asegúrate de que tu app se reinicie automáticamente
En producción, no quieres que tu aplicación esté fuera de línea, nunca. Esto significa que necesitas asegurarte de que se reinicie tanto si la app se cae como si el servidor mismo se cae. Aunque esperas que ninguno de esos eventos ocurra, realísticamente debes considerar ambas eventualidades:
- Usar un gestor de procesos para reiniciar la app (y Node) cuando se cae.
- Usar el init system proporcionado por tu SO para reiniciar el gestor de procesos cuando el SO se cae. También es posible usar el init system sin un gestor de procesos.
Las aplicaciones Node se caen si encuentran una excepción no capturada. Lo primero que necesitas hacer es asegurarte de que tu app esté bien probada y maneje todas las excepciones (consulta maneja las excepciones correctamente para más detalles). Pero como medida de seguridad, pon un mecanismo en marcha para asegurar que si y cuando tu app se caiga, se reinicie automáticamente.
Usa un gestor de procesos
En desarrollo, iniciaste tu app simplemente desde la línea de comandos con node server.js o algo similar. Pero hacer esto en producción es una receta para el desastre. Si la app se cae, estará fuera de línea hasta que la reinicies. Para asegurar que tu app se reinicie si se cae, usa un gestor de procesos. Un gestor de procesos es un “contenedor” para aplicaciones que facilita el despliegue, proporciona alta disponibilidad y te permite gestionar la aplicación en tiempo de ejecución.
Además de reiniciar tu app cuando se cae, un gestor de procesos puede permitirte:
- Obtener información sobre el rendimiento en tiempo de ejecución y el consumo de recursos.
- Modificar configuraciones dinámicamente para mejorar el rendimiento.
- Controlar el clustering (pm2).
Históricamente, fue popular usar un gestor de procesos de Node.js como PM2. Consulta su documentación si deseas hacer esto. Sin embargo, recomendamos usar tu init system para la gestión de procesos.
Usa un init system
La siguiente capa de confiabilidad es asegurar que tu app se reinicie cuando el servidor se reinicie. Los sistemas aún pueden caerse por una variedad de razones. Para asegurar que tu app se reinicie si el servidor se cae, usa el init system integrado en tu SO. El init system principal en uso hoy es systemd.
Hay dos formas de usar init systems con tu app de Express:
- Ejecuta tu app en un gestor de procesos, e instala el gestor de procesos como un servicio con el init system. El gestor de procesos reiniciará tu app cuando la app se caiga, y el init system reiniciará el gestor de procesos cuando el SO se reinicie. Este es el enfoque recomendado.
- Ejecuta tu app (y Node) directamente con el init system. Esto es algo más simple, pero no obtienes las ventajas adicionales de usar un gestor de procesos.
Systemd
Systemd es un gestor de sistemas y servicios de Linux. La mayoría de las principales distribuciones de Linux han adoptado systemd como su init system predeterminado.
Un archivo de configuración de servicio de systemd se llama unit file, con un nombre de archivo que termina en .service. Aquí hay un ejemplo de unit file para gestionar una app de Node directamente. Reemplaza los valores encerrados en <angle brackets> por los de tu sistema y app:
[Unit]Description=<Awesome Express App>
[Service]Type=simpleExecStart=/usr/local/bin/node </projects/myapp/index.js>WorkingDirectory=</projects/myapp>
User=nobodyGroup=nogroup
Environment=NODE_ENV=production
LimitNOFILE=infinity
LimitCORE=infinity
StandardInput=nullStandardOutput=syslogStandardError=syslogRestart=always
[Install]WantedBy=multi-user.targetPara más información sobre systemd, consulta la referencia de systemd (man page).
Ejecuta tu app en un cluster
En un sistema multi-core, puedes aumentar el rendimiento de una app de Node muchas veces lanzando un cluster de procesos. Un cluster ejecuta múltiples instancias de la app, idealmente una instancia en cada núcleo de CPU, distribuyendo así la carga y las tareas entre las instancias.

IMPORTANTE: Como las instancias de la app se ejecutan como procesos separados, no comparten el mismo espacio de memoria. Es decir, los objetos son locales a cada instancia de la app. Por lo tanto, no puedes mantener estado en el código de la aplicación. Sin embargo, puedes usar un almacén de datos en memoria como Redis para almacenar datos y estado relacionados con la sesión. Esta advertencia aplica a esencialmente todas las formas de escalado horizontal, ya sea clustering con múltiples procesos o múltiples servidores físicos.
En apps en cluster, los procesos worker pueden caer individualmente sin afectar al resto de los procesos. Aparte de las ventajas de rendimiento, el aislamiento de fallos es otra razón para ejecutar un cluster de procesos de la app. Siempre que un proceso worker se cae, asegúrate de registrar el evento y crear un nuevo proceso usando cluster.fork().
Usando el módulo cluster de Node
El clustering es posible gracias al módulo cluster de Node. Este permite que un proceso master genere procesos worker y distribuya las conexiones entrantes entre los workers.
Usando PM2
Si despliegas tu aplicación con PM2, entonces puedes aprovechar el clustering sin modificar el código de tu aplicación. Primero debes asegurar que tu aplicación sea stateless, lo que significa que no se almacenan datos locales en el proceso (como sesiones, conexiones websocket y similares).
Al ejecutar una aplicación con PM2, puedes habilitar el modo cluster para ejecutarla en un cluster con un número de instancias de tu elección, como por ejemplo coincidir con el número de CPUs disponibles en la máquina. Puedes cambiar manualmente el número de procesos en el cluster usando la herramienta de línea de comandos pm2 sin detener la app.
Para habilitar el modo cluster, inicia tu aplicación así:
$ pm2 start npm --name my-app -i 4 -- start
$ pm2 start npm --name my-app -i max -- startEsto también se puede configurar dentro de un archivo de proceso PM2 (ecosystem.config.js o similar) estableciendo exec_mode a cluster e instances al número de workers a iniciar.
Una vez en ejecución, la aplicación puede escalarse así:
$ pm2 scale my-app +3
$ pm2 scale my-app 2Para más información sobre clustering con PM2, consulta Cluster Mode en la documentación de PM2.
Cachéa los resultados de las peticiones
Otra estrategia para mejorar el rendimiento en producción es cachéar el resultado de las peticiones, de modo que tu app no repita la operación para servir la misma petición repetidamente.
Usa un servidor de caché como Varnish o Nginx (consulta también Nginx Caching) para mejorar enormemente la velocidad y el rendimiento de tu app.
Usa un balanceador de carga
No importa qué tan optimizada esté una app, una sola instancia puede manejar solo una cantidad limitada de carga y tráfico. Una forma de escalar una app es ejecutar múltiples instancias de ella y distribuir el tráfico mediante un balanceador de carga. Configurar un balanceador de carga puede mejorar el rendimiento y la velocidad de tu app, y permitirle escalar más de lo que es posible con una sola instancia.
Un balanceador de carga es usualmente un reverse proxy que orquesta el tráfico hacia y desde múltiples instancias y servidores de aplicación. Puedes configurar fácilmente un balanceador de carga para tu app usando Nginx o HAProxy.
Con balanceo de carga, puede que necesites asegurar que las peticiones asociadas con un ID de sesión particular se conecten al proceso que las originó. Esto se conoce como session affinity, o sticky sessions, y puede abordarse con la sugerencia anterior de usar un almacén de datos como Redis para los datos de sesión (dependiendo de tu aplicación). Para una discusión, consulta Using multiple nodes.
Usa un reverse proxy
Un reverse proxy se sitúa frente a una app web y realiza operaciones de soporte en las peticiones, aparte de dirigir las peticiones a la app. Puede manejar páginas de error, compresión, caché, servir archivos y balanceo de carga entre otras cosas.
Delegar tareas que no requieren conocimiento del estado de la aplicación a un reverse proxy libera a Express para realizar tareas especializadas de la aplicación. Por esta razón, se recomienda ejecutar Express detrás de un reverse proxy como Nginx o HAProxy en producción.