Desde el inicio de los entornos de servidor para JavaScript, NodeJS ha reinado como el tiempo de ejecución por excelencia. Node, junto con su gestor de paquetes, NPM, se utiliza ahora ampliamente para proyectos que van desde pequeños proyectos secundarios de aficionados hasta sistemas empresariales de alto tráfico. La mayoría de los desarrolladores no se han atrevido a desafiar a Node debido a su estatus por defecto en la industria y al amplio ecosistema de desarrollo de código abierto.
Deno fue el primer competidor importante, pero nunca llegó a cuajar, probablemente debido a su falta de interoperabilidad sólida entre Node, los paquetes NPM de código abierto y CommonJS.
Hace unos días, el 8 de septiembre de 2023, un nuevo competidor, Bun, anunció oficialmente una versión estable de su tiempo de ejecución y las herramientas que lo acompañan, incluido un gestor de paquetes compatible con NPM, un empaquetador, API y mucho más.
Bun promete toda una serie de ventajas, sobre todo en cuanto a rendimiento y experiencia del desarrollador, al tiempo que presume de unos sólidos estándares de interoperabilidad. En otras palabras, Bun parece ser un sustituto directo para las aplicaciones basadas en Node que mejora todo lo relacionado con la experiencia de desarrollo de aplicaciones JS y su rendimiento.
Como alguien que trabaja principalmente en aplicaciones basadas en funciones sin servidor, mi pregunta inmediata fue: todo esto suena muy bien, pero ¿puedo usarlo realmente? Me lo preguntaba especialmente porque Lambda ha optimizado su servicio para NodeJS, así que ¿cómo le iría a Bun cuando no tiene soporte oficial? Para averiguarlo, ideé un conjunto de pruebas comparativas con el objetivo de responder a mi pregunta.
Si deseas aprender más acerca de Bun te dejo el siguiente video
Las pruebas
La ejecución de pruebas comparativas genéricas tiene limitaciones a la hora de comparar tiempos de ejecución y lenguajes y, por lo tanto, no puede sustituir a la comprobación de casos de uso específicos.
Dicho esto, traté de centrarme en algunas áreas clave que pensé que serían de interés para los desarrolladores en el contexto de la ejecución de JavaScript en un entorno de funciones sin servidor.
Se me ocurrieron las siguientes tres pruebas de referencia que consideran aspectos teóricos, del mundo real y específicos de serverless:
Rendimiento general del procesamiento
Bun afirma que puede procesar la lógica a 3-4 veces la velocidad de NodeJS. Aunque muchas aplicaciones pueden estar más ligadas a la E/S (consultas a bases de datos, llamadas a la red, lecturas/escrituras del sistema de archivos, etc.), este tipo de mejora general, si es precisa, ayuda a todos los sistemas, y especialmente a aquellos con cargas de trabajo ligadas a la CPU y a la memoria. Como tal, quería ver si las afirmaciones de rendimiento puro de Bun se traducirían a un entorno sin servidor.
Prueba: Generar y luego ordenar (con Array.sort
incorporado) 100K números aleatorios 10 veces consecutivas.
API CRUD
Probar cargas de trabajo con mucha CPU es interesante, pero es probable que infle los beneficios de Bun en el mundo real, ya que la realidad es que los entornos tipo Lambda se utilizan con mucha frecuencia para aplicaciones mucho más simples que están más ligadas a la E/S (red, sistema de archivos, etc.).
Un patrón muy común es utilizar API Gateway, Lambda y DynamoDB juntos para crear API CRUD sencillas en las que los cálculos son simples. Quería probar estos escenarios más reales para ver si Bun puede seguir proporcionando beneficios incluso en ausencia de lógica pesada ligada a la CPU.
Test
Implementar una función CRUD Update que valide la entrada, recupere un objeto de DynamoDB, fusione superficialmente las modificaciones solicitadas en el objeto existente y lo devuelva a DynamoDB.
Tiempos de arranque en frío
Las funciones sin servidor tienden a sufrir lo que se conoce como el problema del "arranque en frío". Las funciones de AWS Lambda se ejecutan en un entorno de contenedores, y cada contenedor suele durar entre 10 y 30 minutos.
Cuando llega una solicitud y no hay ningún contenedor preparado y disponible, Lambda preparará un nuevo contenedor para gestionar la solicitud, y ese tiempo de preparación es el tiempo de arranque en frío al que nos referimos. Los tiempos de ejecución soportados oficialmente, como NodeJS, suelen estar más optimizados para iniciarse rápidamente, así que la principal pregunta a responder es ¿cómo se comparará Bun, un tiempo de ejecución no optimizado?
Test: función Hola mundo con arranques en frío inducidos intencionadamente.
Configuración del entorno
Para estas pruebas se utilizó la siguiente configuración:
- REST API Gateway-to-Lambda Function (tiempos de respuesta medidos desde API Gateway para obtener un alcance completo de extremo a extremo)
- 1024 MB de memoria para las funciones Lambda
- Arquitectura x86_64 en Amazon Linux 2 para Bun
- Configuración de tiempo de ejecución base en arquitectura x86_64 para Node.js 18.x
- Capacidad de concurrencia prevista de 5 (excepto para la prueba de arranque en frío)
- Resultados generales de la prueba de procesamiento
Esta prueba se ejecutó 1.000 veces para cada tiempo de ejecución, con datos de tiempo de respuesta recopilados del lado del servidor y recogidos en CloudWatch y X-Ray.
NodeJS
- Tiempo de respuesta medio: 3736 ms
- Tiempo de respuesta mínimo: 3391 ms
- Tiempo de respuesta máximo: 4580 ms
- Tiempo de respuesta p95: 3989 ms
- Tiempo de respuesta p99: 4404 ms
Bun
- Tiempo de respuesta medio: 1836 ms (-50,9%)
- Tiempo de respuesta mínimo: 1564 ms (-53,9%)
- Tiempo de respuesta máximo: 3571 ms (-22,0%)
- Tiempo de respuesta p95: 2027 ms (-49,2 %)
- Tiempo de respuesta p99: 2117 ms (-51,9%)
Resultados de la prueba de la API CRUD
Esta prueba se ejecutó 1.000 veces para cada tiempo de ejecución, con datos de tiempo de respuesta recogidos del lado del servidor y recopilados en CloudWatch y X-Ray.
Node
- Tiempo de respuesta medio: 24 ms
- Tiempo de respuesta mínimo: 17 ms
- Tiempo de respuesta máximo: 135 ms
- Tiempo de respuesta p95: 29 ms
- Tiempo de respuesta p99: 51 ms
Bun
- Tiempo de respuesta medio: 25 ms (+4,2%)
- Tiempo de respuesta mínimo: 16 ms (-5,9%))
- Tiempo de respuesta máximo: 157 ms (+16,3%))
- Tiempo de respuesta p95: 33 ms (+13,8%)
- Tiempo de respuesta p99: 44 ms (-13,7%)
Tiempos de arranque en frío
Esta prueba se ejecutó sólo 10 veces para cada tiempo de ejecución, ya que no hay una gran manera de automatizar los arranques en frío forzados en una prueba de carga. Tenga en cuenta que los resultados aquí reflejan el tiempo de respuesta completo, no sólo los tiempos de inicialización.
Node
- Tiempo de respuesta medio: 302 ms
- Tiempo de respuesta mínimo: 252 ms
- Tiempo de respuesta máximo: 361 ms
- Tiempo de respuesta p95: 312 ms
- Tiempo de respuesta p99: 361 ms
Bun
- Tiempo de respuesta medio: 775 ms (+156%)
- Tiempo de respuesta mínimo: 717 ms (+184%)
- Tiempo de respuesta máximo: 1382 ms (+282%)
- Tiempo de respuesta p95: 1174 ms (+276%)
- Tiempo de respuesta p99: 1382 ms (+282%)
Interpretación de los resultados
Para mi sorpresa, a pesar de que Node está específicamente optimizado para Lambda y que la capa oficial de Bun Lambda no ha sido probada (y por lo tanto no está optimizada), Bun se las arregló para salir adelante de manera significativa en las tareas de CPU. También fue a la par para tareas más simples y de E/S, lo que significa que es poco probable que veas una disminución en el rendimiento por el uso de Bun.
Creo que si AWS Lambda añadiera Bun como tiempo de ejecución oficial, veríamos a Bun funcionar al menos ligeramente mejor que Node, si no con mejoras significativas.
Y si estás ejecutando tu lógica con frameworks y librerías como Express, Nest, Apollo o NextJS, no me sorprendería que vieras un rendimiento consistentemente más rápido (esto es algo que me gustaría probar a continuación).
¿Está listo para la producción? Bueno, yo no llegaría a esa conclusión todavía, porque me encontré con algunos baches en el camino con Bun Lambda Layer, y no está particularmente probado en este momento. Además, todavía hay algunas APIs de NodeJS que no son interoperables con Bun, y Bun en sí es todavía muy nuevo, lo que significa que no conocemos todas sus peculiaridades todavía.
Sin embargo, estos son problemas solucionables, y estas pruebas muestran que, con un poco de esfuerzo, hay un potencial real para que esto sea un tiempo de ejecución legítimo para el desarrollo de aplicaciones sin servidor de producción.
Combatir los problemas del arranque en frío
Obviamente, el problema del arranque en frío es algo que hay que tener en cuenta, pero yo no lo llamaría un "deal breaker" ya que hay dos soluciones populares para hacer este problema casi obsoleto:
La solución oficial de AWS de concurrencia provisionada, que mantiene un número configurado de contenedores siempre calientes por un coste. Dependiendo de la carga a su aplicación, esto podría ser muy razonable o puede ser más caro de un enfoque de lo que vale la pena.
Solución de calentamiento personalizada que hace ping a tu función Lambda cada cierto tiempo para asegurar que un número de contenedores están siempre disponibles. Esta es una buena solución para aplicaciones de bajo rendimiento, ya que es barata y mantiene un contenedor listo para su uso siempre que lo necesite.
Ventajas de rendimiento
En casos comunes de uso de aplicaciones, el simple cambio de NodeJS por Bun puede suponer una ligera mejora en la latencia, dando a tus aplicaciones una sensación más ágil. Cuanto más haga su aplicación en la propia computación, más beneficios obtendrá, por lo que realmente dependerá de sus casos de uso específicos.
Dicho esto, emparejado con una estrategia de arranque en frío, parece que Bun estará al menos a la par con Node mientras ofrece todos sus otros beneficios de experiencia de desarrollo.
Beneficios de coste
Reducir el tiempo de ejecución de las funciones Lambda tiene un impacto positivo directo en el coste de ejecución de tu aplicación, ya que las funciones Lambda se cobran por milisegundo de uso.
Si en tu aplicación hay mucha lógica ligada a la CPU, podrías obtener un enorme ahorro de costes cambiando a Bun, pero si sólo ejecutas algunas funciones sencillas que llaman a bases de datos u otras API y no haces mucho con esos datos en la propia función Lambda, probablemente no verás grandes diferencias, además de que tendrás que pagar por cualquier estrategia de arranque en frío que elijas.
Otra advertencia a tener en cuenta es que el uso del tiempo de ejecución integrado de Node significa que no se le facturan los costes de inicialización (arranque en frío). Por otra parte, se le cobra por la inicialización de los contenedores Lambda para Bun, por lo que además de combatir los problemas de arranque en frío por razones de rendimiento, es probable que desee reducir estos por el costo beneficio también.
Reflexiones finales
Era muy escéptica sobre Bun en general y especialmente en términos de potencial para usarlo en un flujo de trabajo de aplicación sin servidor. Me sorprendió gratamente tener ese escepticismo anulado por estos resultados. Aunque no voy a utilizar personalmente Bun para el desarrollo de Lambda por el momento debido a su novedad, voy a mantener una estrecha vigilancia sobre ella como su ecosistema madura y como herramientas para Bun en Lambda se endurece y optimiza aún más.
Como nota al margen, este artículo no discute la experiencia de desarrollo de Bun, pero el tl;dr
es que fue una experiencia refrescante con tiempos de instalación y construcción de paquetes extremadamente rápidos y soporte nativo efectivo para probar funciones Lambda localmente algo que de otra manera es más doloroso y requiere herramientas adicionales de AWS SAM.
Con más madurez, creo que Bun tiene una oportunidad real de superar a Node como el tiempo de ejecución predeterminado de la industria para el desarrollo de JavaScript y TypeScript, y personalmente estaré apoyándolo. Además, AWS Lambda, si por casualidad lees este artículo, por favor considera apoyar Bun como tiempo de ejecución nativo para Lambda.
Recuerda que si quieres aprender AWS te dejo el siguiente video