¿Es hora de usar Node 8?

Publicado: 2022-03-11

¡Nodo 8 está fuera! De hecho, Node 8 ha estado disponible el tiempo suficiente para ver un uso sólido en el mundo real. Llegó con un motor V8 nuevo y rápido y con nuevas funciones, que incluyen async/await, HTTP/2 y enlaces asíncronos. ¿Pero está listo para su proyecto? ¡Vamos a averiguar!

Nota del editor: probablemente sepa que el Nodo 10 (cuyo nombre en código es Dubnium ) también está fuera. Elegimos centrarnos en el Nodo 8 ( Carbono ) por dos razones: (1) el Nodo 10 acaba de ingresar a su fase de soporte a largo plazo (LTS) y (2) el Nodo 8 marcó una iteración más significativa que el Nodo 10 .

Rendimiento en Nodo 8 LTS

Comenzaremos echando un vistazo a las mejoras de rendimiento y las nuevas características de esta extraordinaria versión. Un área importante de mejora está en el motor de JavaScript de Node.

¿Qué es exactamente un motor de JavaScript, de todos modos?

Un motor de JavaScript ejecuta y optimiza el código. Podría ser un intérprete estándar o un compilador justo a tiempo (JIT) que compila JavaScript en código de bytes. Los motores JS utilizados por Node.js son todos compiladores JIT, no intérpretes.

El motor V8

Node.js ha utilizado el motor de JavaScript Chrome V8 de Google, o simplemente V8 , desde el principio. Algunas versiones de Node se utilizan para sincronizar con una versión más nueva de V8. Pero tenga cuidado de no confundir V8 con Node 8 mientras comparamos las versiones de V8 aquí.

Es fácil tropezar con esto, ya que en contextos de software a menudo usamos "v8" como jerga o incluso como abreviatura oficial de "versión 8", por lo que algunos podrían confundir "Node V8" o "Node.js V8" con "NodeJS 8". ”, pero hemos evitado esto a lo largo de este artículo para ayudar a mantener las cosas claras: V8 siempre significará el motor, no la versión de Node.

Versión V8 5

El nodo 6 utiliza la versión 5 de V8 como su motor de JavaScript. (Las primeras versiones puntuales de Node 8 también utilizan la versión 5 de V8, pero utilizan una versión puntual V8 más reciente que la que utilizó Node 6).

compiladores

Las versiones V8 5 y anteriores tienen dos compiladores:

  • Full-codegen es un compilador JIT simple y rápido, pero produce un código de máquina lento.
  • Crankshaft es un compilador JIT complejo que produce código de máquina optimizado.
Hilos

En el fondo, V8 usa más de un tipo de hilo:

  • El subproceso principal obtiene el código, lo compila y luego lo ejecuta.
  • Los subprocesos secundarios ejecutan código mientras que el subproceso principal optimiza el código.
  • El subproceso del generador de perfiles informa al tiempo de ejecución sobre los métodos que no funcionan. El cigüeñal luego optimiza estos métodos.
  • Otros subprocesos administran la recolección de basura.
Proceso de compilación

Primero, el compilador Full-codegen ejecuta el código JavaScript. Mientras se ejecuta el código, el hilo del generador de perfiles recopila datos para determinar qué métodos optimizará el motor. En otro hilo, Crankshaft optimiza estos métodos.

Cuestiones

El enfoque mencionado anteriormente tiene dos problemas principales. En primer lugar, es arquitectónicamente complejo. En segundo lugar, el código de máquina compilado consume mucha más memoria. La cantidad de memoria consumida es independiente del número de veces que se ejecuta el código. Incluso el código que se ejecuta solo una vez también ocupa una cantidad significativa de memoria.

V8 versión 6

La primera versión de Node que usa el motor V8 versión 6 es Node 8.3.

En la versión 6, el equipo de V8 creó Ignition y TurboFan para mitigar estos problemas. Ignition y TurboFan reemplazan a Full-codegen y CrankShaft, respectivamente.

La nueva arquitectura es más sencilla y consume menos memoria.

Ignition compila código JavaScript en código de bytes en lugar de código de máquina, ahorrando mucha memoria. Posteriormente, TurboFan, el compilador de optimización, genera un código de máquina optimizado a partir de este código de bytes.

Mejoras de rendimiento específicas

Repasemos las áreas donde el rendimiento en Node 8.3+ cambió en relación con las versiones anteriores de Node.

Crear objetos

La creación de objetos es unas cinco veces más rápida en Node 8.3+ que en Node 6.

Tamaño de la función

El motor V8 decide si una función debe optimizarse en función de varios factores. Un factor es el tamaño de la función. Las funciones pequeñas están optimizadas, mientras que las funciones largas no lo están.

¿Cómo se calcula el tamaño de la función?

El cigüeñal en el antiguo motor V8 utiliza el "recuento de caracteres" para determinar el tamaño de la función. Los espacios en blanco y los comentarios en una función reducen las posibilidades de que se optimicen. Sé que esto podría sorprenderte, pero en aquel entonces, un comentario podía reducir la velocidad en aproximadamente un 10 %.

En Node 8.3+, los caracteres irrelevantes, como los espacios en blanco y los comentarios, no dañan el rendimiento de la función. ¿Por qué no?

Porque el nuevo TurboFan no cuenta los caracteres para determinar el tamaño de la función. En cambio, cuenta los nodos del árbol de sintaxis abstracta (AST), por lo que solo considera las instrucciones de función reales . Con Node 8.3+, puede agregar comentarios y espacios en blanco tanto como desee.

Array -ificando argumentos

Las funciones regulares en JavaScript llevan un objeto de argument implícito tipo Array .

¿Qué significa Array -like?

El objeto arguments actúa como una matriz. Tiene la propiedad length pero carece de los métodos integrados de Array como forEach y map .

Así es como funciona el objeto de arguments :

 function foo() { console.log(arguments[0]); // Expected output: a console.log(arguments[1]); // Expected output: b console.log(arguments[2]); // Expected output: c } foo("a", "b", "c");

Entonces, ¿cómo podríamos convertir el objeto de arguments en una matriz? Usando el conciso Array.prototype.slice.call(arguments) .

 function test() { const r = Array.prototype.slice.call(arguments); console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output: [2, 4, 6]

Array.prototype.slice.call(arguments) afecta el rendimiento en todas las versiones de Node. Por lo tanto, copiar las claves a través de un bucle for funciona mejor:

 function test() { const r = []; for (index in arguments) { r.push(arguments[index]); } console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output [2, 4, 6]

El bucle for es un poco engorroso, ¿no? Podríamos usar el operador de propagación, pero es lento en el Nodo 8.2 y hacia abajo:

 function test() { const r = [...arguments]; console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output [2, 4, 6]

La situación ha cambiado en Node 8.3+. Ahora la propagación se ejecuta mucho más rápido, incluso más rápido que un bucle for.

Aplicación parcial (curring) y encuadernación

Curry es dividir una función que toma múltiples argumentos en una serie de funciones donde cada nueva función toma solo un argumento.

Digamos que tenemos una función de add simple. La versión curry de esta función toma un argumento, num1 . Devuelve una función que toma otro argumento num2 y devuelve la suma de num1 y num2 :

 function add(num1, num2) { return num1 + num2; } add(4, 6); // returns 10 function curriedAdd(num1) { return function(num2) { return num1 + num2; }; } const add5 = curriedAdd(5); add5(3); // returns 8

El método bind devuelve una función curry con una sintaxis breve.

 function add(num1, num2) { return num1 + num2; } const add5 = add.bind(null, 5); add5(3); // returns 8

Entonces, bind es increíble, pero es lento en versiones anteriores de Node. En Node 8.3+, bind es mucho más rápido y puede usarlo sin preocuparse por los impactos en el rendimiento.

Experimentos

Se han realizado varios experimentos para comparar el rendimiento del Nodo 6 con el Nodo 8 en un alto nivel. Tenga en cuenta que estos se realizaron en Node 8.0, por lo que no incluyen las mejoras mencionadas anteriormente que son específicas de Node 8.3+ gracias a su actualización V8 versión 6.

El tiempo de procesamiento del servidor en el Nodo 8 fue un 25 % menor que en el Nodo 6. En proyectos grandes, la cantidad de instancias del servidor podría reducirse de 100 a 75. Esto es asombroso. Probar un conjunto de 500 pruebas en Node 8 fue un 10 % más rápido. Las compilaciones de paquetes web fueron un 7 % más rápidas. En general, los resultados mostraron un aumento notable del rendimiento en el Nodo 8.

Características del nodo 8

La velocidad no fue la única mejora en Node 8. También trajo varias características nuevas y útiles, quizás lo más importante, async/await .

Asíncrono/espera en el nodo 8

Las devoluciones de llamada y las promesas generalmente se usan para manejar código asíncrono en JavaScript. Las devoluciones de llamada son notorias por producir código que no se puede mantener. Han causado caos (conocido específicamente como callback hell ) en la comunidad de JavaScript. Promises nos rescató del infierno de las devoluciones de llamadas durante mucho tiempo, pero aún carecían de la limpieza del código síncrono. Async/await es un enfoque moderno que le permite escribir código asíncrono que parece código síncrono.

Y aunque async/await se podía usar en versiones anteriores de Node, requería bibliotecas y herramientas externas, por ejemplo, preprocesamiento adicional a través de Babel. Ahora está disponible de forma nativa, lista para usar.

Hablaré sobre algunos casos en los que async/await es superior a las promesas convencionales.

Condicionales

Imagine que está obteniendo datos y determinará si se necesita una nueva llamada a la API en función de la carga útil . Eche un vistazo al código a continuación para ver cómo se hace esto a través del enfoque de "promesas convencionales".

 const request = () => { return getData().then(data => { if (!data.car) { return fetchForCar(data.id).then(carData => { console.log(carData); return carData; }); } else { console.log(data); return data; } }); };

Como puede ver, el código anterior ya parece desordenado, solo por un condicional adicional. Async/await implica menos anidamiento:

 const request = async () => { const data = await getData(); if (!data.car) { const carData = await fetchForCar(data); console.log(carData); return carData; } else { console.log(data); return data; } };

Manejo de errores

Async/await le otorga acceso para manejar errores sincrónicos y asincrónicos en try/catch. Supongamos que desea analizar JSON procedente de una llamada API asíncrona. Un solo intento/captura podría manejar tanto errores de análisis como errores de API.

 const request = async () => { try { console.log(await getData()); } catch (err) { console.log(err); } };

Valores intermedios

¿Qué pasa si una promesa necesita un argumento que debe resolverse a partir de otra promesa? Esto significa que las llamadas asincrónicas deben realizarse en serie.

Usando promesas convencionales, podría terminar con un código como este:

 const request = () => { return fetchUserData() .then(userData => { return fetchCompanyData(userData); }) .then(companyData => { return fetchRetiringPlan(userData, companyData); }) .then(retiringPlan => { const retiringPlan = retiringPlan; }); };

Async/await brilla en este caso, donde se necesitan llamadas asíncronas encadenadas:

 const request = async () => { const userData = await fetchUserData(); const companyData = await fetchCompanyData(userData); const retiringPlan = await fetchRetiringPlan(userData, companyData); };

Asíncrono en Paralelo

¿Qué sucede si desea llamar a más de una función asíncrona en paralelo? En el siguiente código, esperaremos a que se resuelva fetchHouseData y luego llamaremos a fetchCarData . Aunque cada uno de estos es independiente del otro, se procesan secuencialmente. Esperará dos segundos para que ambas API se resuelvan. Esto no está bien.

 function fetchHouseData() { return new Promise(resolve => setTimeout(() => resolve("Mansion"), 1000)); } function fetchCarData() { return new Promise(resolve => setTimeout(() => resolve("Ferrari"), 1000)); } async function action() { const house = await fetchHouseData(); // Wait one second const car = await fetchCarData(); // ...then wait another second. console.log(house, car, " in series"); } action();

Un mejor enfoque es procesar las llamadas asincrónicas en paralelo. Consulte el código a continuación para tener una idea de cómo se logra esto en async/await.

 async function parallel() { houseDataPromise = fetchHouseData(); carDataPromise = fetchCarData(); const house = await houseDataPromise; // Wait one second for both const car = await carDataPromise; console.log(house, car, " in parallel"); } parallel();

El procesamiento de estas llamadas en paralelo le hace esperar solo un segundo para ambas llamadas.

Nuevas funciones principales de la biblioteca

Node 8 también trae algunas funciones básicas nuevas.

Copiar archivos

Antes de Node 8, para copiar archivos, solíamos crear dos flujos y canalizar datos de uno a otro. El siguiente código muestra cómo el flujo de lectura canaliza los datos al flujo de escritura. Como puede ver, el código está desordenado para una acción tan simple como copiar un archivo.

 const fs = require('fs'); const rd = fs.createReadStream('sourceFile.txt'); rd.on('error', err => { console.log(err); }); const wr = fs.createWriteStream('target.txt'); wr.on('error', err => { console.log(err); }); wr.on('close', function(ex) { console.log('File Copied'); }); rd.pipe(wr);

En Node 8 fs.copyFile y fs.copyFileSync son nuevos enfoques para copiar archivos con mucha menos molestia.

 const fs = require("fs"); fs.copyFile("firstFile.txt", "secondFile.txt", err => { if (err) { console.log(err); } else { console.log("File copied"); } });

Prometer y devolver la llamada

util.promisify convierte una función normal en una función asíncrona. Tenga en cuenta que la función ingresada debe seguir el estilo común de devolución de llamada de Node.js. Debería tomar una devolución de llamada como último argumento, es decir, (error, payload) => { ... } .

 const { promisify } = require('util'); const fs = require('fs'); const readFilePromisified = promisify(fs.readFile); const file_path = process.argv[2]; readFilePromisified(file_path) .then((text) => console.log(text)) .catch((err) => console.log(err));

Como puede ver, util.promisify ha convertido fs.readFile en una función asíncrona.

Por otro lado, Node.js viene con util.callbackify . util.callbackify es lo opuesto a util.promisify : convierte una función asíncrona en una función de estilo de devolución de llamada de Node.js.

Función destroy para Readables y Writables

La función de destroy en el Nodo 8 es una forma documentada de destruir/cerrar/abortar un flujo legible o escribible:

 const fs = require('fs'); const file = fs.createWriteStream('./big.txt'); file.on('error', errors => { console.log(errors); }); file.write(`New text.\n`); file.destroy(['First Error', 'Second Error']);

El código anterior da como resultado la creación de un nuevo archivo llamado big.txt (si aún no existe) con el texto New text. .

Las funciones Readable.destroy y Writeable.destroy en el nodo 8 emiten un evento de close y un evento de error opcional : destroy no significa necesariamente que algo salió mal.

Operador de propagación

El operador de propagación (también conocido como ... ) funcionó en el Nodo 6, pero solo con matrices y otros iterables:

 const arr1 = [1,2,3,4,5,6] const arr2 = [...arr1, 9] console.log(arr2) // expected output: [1,2,3,4,5,6,9]

En el Nodo 8, los objetos también pueden usar el operador de propagación:

 const userCarData = { type: 'ferrari', color: 'red' }; const userSettingsData = { lastLoggedIn: '12/03/2019', featuresPlan: 'premium' }; const userData = { ...userCarData, name: 'Youssef', ...userSettingsData }; console.log(userData); /* Expected output: { type: 'ferrari', color: 'red', name: 'Youssef', lastLoggedIn: '12/03/2019', featuresPlan: 'premium' } */

Funciones experimentales en Node 8 LTS

Las características experimentales no son estables, podrían quedar obsoletas y actualizarse con el tiempo. No utilice ninguna de estas funciones en producción hasta que se estabilicen.

Ganchos asíncronos

Los enlaces asíncronos rastrean la vida útil de los recursos asíncronos creados dentro de Node a través de una API.

Asegúrese de comprender el bucle de eventos antes de continuar con los ganchos asíncronos. Este video podría ayudar. Los ganchos asíncronos son útiles para depurar funciones asíncronas. Tienen varias aplicaciones; uno de ellos es el seguimiento de la pila de errores para funciones asíncronas.

Eche un vistazo al código a continuación. Tenga en cuenta que console.log es una función asíncrona. Por lo tanto, no se puede usar dentro de ganchos asíncronos. En su lugar, se utiliza fs.writeSync .

 const asyncHooks = require('async_hooks'); const fs = require('fs'); const init = (asyncId, type, triggerId) => fs.writeSync(1, `${type} \n`); const asyncHook = asyncHooks.createHook({ init }); asyncHook.enable();

Mire este video para obtener más información sobre los ganchos asíncronos. En términos de una guía de Node.js específicamente, este artículo ayuda a desmitificar los ganchos asíncronos a través de una aplicación ilustrativa.

Módulos ES6 en Nodo 8

Node 8 ahora admite módulos ES6, lo que le permite usar esta sintaxis:

 import { UtilityService } from './utility_service';

Para usar los módulos ES6 en el Nodo 8, debe hacer lo siguiente.

  1. Agregue el --experimental-modules a la línea de comando
  2. Cambiar el nombre de las extensiones de archivo de .js a .mjs

HTTP/2

HTTP/2 es la última actualización del protocolo HTTP, que no se actualiza con frecuencia, y Node 8.4+ lo admite de forma nativa en modo experimental. Es más rápido, más seguro y más eficiente que su predecesor, HTTP/1.1. Y Google recomienda que lo uses. ¿Pero qué más hace?

multiplexación

En HTTP/1.1, el servidor solo podía enviar una respuesta por conexión a la vez. En HTTP/2, el servidor puede enviar más de una respuesta en paralelo.

Empuje del servidor

El servidor puede enviar múltiples respuestas para una sola solicitud de cliente. ¿Por qué es esto beneficioso? Tome una aplicación web como ejemplo. Convencionalmente,

  1. El cliente solicita un documento HTML.
  2. El cliente descubre los recursos necesarios del documento HTML.
  3. El cliente envía una solicitud HTTP para cada recurso requerido. Por ejemplo, el cliente envía una solicitud HTTP para cada recurso JS y CSS mencionado en el documento.

La función de inserción del servidor aprovecha el hecho de que el servidor ya conoce todos esos recursos. El servidor envía esos recursos al cliente. Entonces, para el ejemplo de la aplicación web, el servidor envía todos los recursos después de que el cliente solicita el documento inicial. Esto reduce la latencia.

Priorización

El cliente puede establecer un esquema de priorización para determinar qué tan importante es cada respuesta requerida. Luego, el servidor puede usar este esquema para priorizar la asignación de memoria, CPU, ancho de banda y otros recursos.

Deshacerse de viejos malos hábitos

Dado que HTTP/1.1 no permitía la multiplexación, se utilizan varias optimizaciones y soluciones alternativas para cubrir la velocidad lenta y la carga de archivos. Desafortunadamente, estas técnicas provocan un aumento en el consumo de RAM y un retraso en el renderizado:

  • Fragmentación de dominios: se utilizaron varios subdominios para que las conexiones se dispersen y se procesen en paralelo.
  • Combinando archivos CSS y JavaScript para reducir el número de solicitudes.
  • Mapas de sprites: combinación de archivos de imagen para reducir las solicitudes HTTP.
  • En línea: CSS y JavaScript se colocan en el HTML directamente para reducir la cantidad de conexiones.

Ahora, con HTTP/2, puedes olvidarte de estas técnicas y concentrarte en tu código.

Pero, ¿cómo se usa HTTP/2?

La mayoría de los navegadores admiten HTTP/2 solo a través de una conexión SSL segura. Este artículo puede ayudarlo a configurar un certificado autofirmado. Agregue el archivo .crt generado y el archivo .key en un directorio llamado ssl . Luego, agregue el siguiente código a un archivo llamado server.js .

Recuerde usar el --expose-http2 en la línea de comando para habilitar esta función. Es decir, el comando de ejecución de nuestro ejemplo es node server.js --expose-http2 .

 const http2 = require('http2'); const path = require('path'); const fs = require('fs'); const PORT = 3000; const secureServerOptions = { cert: fs.readFileSync(path.join(__dirname, './ssl/server.crt')), key: fs.readFileSync(path.join(__dirname, './ssl/server.key')) }; const server = http2.createSecureServer(secureServerOptions, (req, res) => { res.statusCode = 200; res.end('Hello from Toptal'); }); server.listen( PORT, err => err ? console.error(err) : console.log(`Server listening to port ${PORT}`) );

Por supuesto, el nodo 8, el nodo 9, el nodo 10, etc. todavía son compatibles con el antiguo HTTP 1.1: la documentación oficial de Node.js sobre una transacción HTTP estándar no estará obsoleta durante mucho tiempo. Pero si desea utilizar HTTP/2, puede profundizar más con esta guía de Node.js.

Entonces, ¿debería usar Node.js 8 al final?

Node 8 llegó con mejoras de rendimiento y con nuevas características como async/await, HTTP/2 y otras. Los experimentos de extremo a extremo han demostrado que el Nodo 8 es aproximadamente un 25 % más rápido que el Nodo 6. Esto conduce a un ahorro sustancial de costos. Entonces, para proyectos greenfield, ¡absolutamente! Pero para proyectos existentes, ¿debería actualizar Node?

Depende de si necesitaría cambiar gran parte de su código existente. Este documento enumera todos los cambios importantes del Nodo 8 si viene del Nodo 6. Recuerde evitar problemas comunes reinstalando todos los paquetes npm de su proyecto usando la última versión del Nodo 8. Además, utilice siempre la misma versión de Node.js en las máquinas de desarrollo que en los servidores de producción. ¡La mejor de las suertes!

Relacionados:
  • ¿Por qué diablos usaría Node.js? Un tutorial caso por caso
  • Depuración de fugas de memoria en aplicaciones Node.js
  • Creación de una API REST segura en Node.js
  • Codificación de Cabin Fever: un tutorial de back-end de Node.js