É hora de usar o nó 8?

Publicados: 2022-03-11

O nó 8 está fora! Na verdade, o Node 8 já foi lançado há tempo suficiente para ver algum uso sólido no mundo real. Ele veio com um novo motor V8 rápido e com novos recursos, incluindo async/await, HTTP/2 e ganchos assíncronos. Mas está pronto para o seu projeto? Vamos descobrir!

Nota do editor: Você provavelmente está ciente de que o Nó 10 (codinome Dubnium ) também foi lançado. Estamos escolhendo focar no Nó 8 ( Carbono ) por dois motivos: (1) O Nó 10 está entrando na fase de suporte de longo prazo (LTS) e (2) O Nó 8 marcou uma iteração mais significativa do que o Nó 10 fez .

Desempenho no Nó 8 LTS

Começaremos analisando as melhorias de desempenho e os novos recursos desta versão notável. Uma área importante de melhoria está no mecanismo JavaScript do Node.

O que exatamente é um mecanismo JavaScript, afinal?

Um mecanismo JavaScript executa e otimiza o código. Pode ser um interpretador padrão ou um compilador just-in-time (JIT) que compila JavaScript para bytecode. Os mecanismos JS usados ​​pelo Node.js são todos compiladores JIT, não interpretadores.

O motor V8

O Node.js usa o mecanismo JavaScript Chrome V8 do Google , ou simplesmente V8 , desde o início. Algumas versões do Node são usadas para sincronizar com uma versão mais recente do V8. Mas tome cuidado para não confundir V8 com Node 8, pois comparamos as versões V8 aqui.

Isso é fácil de tropeçar, pois em contextos de software geralmente usamos “v8” como gíria ou até mesmo abreviação oficial para “versão 8”, então alguns podem confundir “Node V8” ou “Node.js V8” com “NodeJS 8 ”, mas evitamos isso ao longo deste artigo para ajudar a manter as coisas claras: V8 sempre significará o mecanismo, não a versão do Node.

Versão 5 do V8

O nó 6 usa a versão 5 do V8 como seu mecanismo JavaScript. (As primeiras versões pontuais do Node 8 também usam a versão 5 do V8, mas usam uma versão pontual V8 mais recente do que o Node 6.)

Compiladores

As versões V8 5 e anteriores têm dois compiladores:

  • Full-codegen é um compilador JIT simples e rápido, mas produz código de máquina lento.
  • Crankshaft é um compilador JIT complexo que produz código de máquina otimizado.
Tópicos

No fundo, o V8 usa mais de um tipo de rosca:

  • A thread principal busca o código, compila-o e executa-o.
  • Os threads secundários executam o código enquanto o thread principal está otimizando o código.
  • O encadeamento do criador de perfil informa o tempo de execução sobre métodos sem desempenho. O virabrequim otimiza esses métodos.
  • Outros encadeamentos gerenciam a coleta de lixo.
Processo de compilação

Primeiro, o compilador Full-codegen executa o código JavaScript. Enquanto o código está sendo executado, o thread do criador de perfil coleta dados para determinar quais métodos o mecanismo otimizará. Em outro segmento, o Crankshaft otimiza esses métodos.

Problemas

A abordagem mencionada acima tem dois problemas principais. Primeiro, é arquitetonicamente complexo. Em segundo lugar, o código de máquina compilado consome muito mais memória. A quantidade de memória consumida é independente do número de vezes que o código é executado. Mesmo o código que é executado apenas uma vez também ocupa uma quantidade significativa de memória.

Versão 6 do V8

A primeira versão do Node a usar o mecanismo V8 release 6 é o Node 8.3.

Na versão 6, a equipe V8 criou o Ignition e o TurboFan para mitigar esses problemas. Ignition e TurboFan substituem Full-codegen e CrankShaft, respectivamente.

A nova arquitetura é mais direta e consome menos memória.

O Ignition compila o código JavaScript em bytecode em vez de código de máquina, economizando muita memória. Depois, TurboFan, o compilador otimizador, gera código de máquina otimizado a partir desse bytecode.

Melhorias de desempenho específicas

Vamos passar pelas áreas em que o desempenho no Node 8.3+ mudou em relação às versões mais antigas do Node.

Criando objetos

Criar objetos é cerca de cinco vezes mais rápido no Node 8.3+ do que no Node 6.

Tamanho da função

O motor V8 decide se uma função deve ser otimizada com base em vários fatores. Um fator é o tamanho da função. As funções pequenas são otimizadas, enquanto as funções longas não.

Como o tamanho da função é calculado?

O virabrequim no antigo motor V8 usa “contagem de caracteres” para determinar o tamanho da função. Espaços em branco e comentários em uma função reduzem as chances de otimização. Eu sei que isso pode surpreendê-lo, mas naquela época, um comentário poderia reduzir a velocidade em cerca de 10%.

No Node 8.3+, caracteres irrelevantes, como espaços em branco e comentários, não prejudicam o desempenho da função. Por que não?

Porque o novo TurboFan não conta caracteres para determinar o tamanho da função. Em vez disso, ele conta os nós da árvore de sintaxe abstrata (AST), de forma eficaz, considerando apenas as instruções de função reais . Usando o Node 8.3+, você pode adicionar comentários e espaços em branco o quanto quiser.

Array que definem matrizes

Funções regulares em JavaScript carregam um objeto de argument do tipo Array implícito.

O que significa Array -like?

O objeto arguments age como um array. Ele tem a propriedade length , mas não possui os métodos internos do Array , como forEach e map .

Veja como o objeto de arguments funciona:

 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");

Então, como poderíamos converter o objeto arguments em um array? Usando o 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) prejudica o desempenho em todas as versões do Node. Portanto, copiar as chaves por meio de um loop for tem melhor desempenho:

 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]

O loop for é um pouco complicado, não é? Poderíamos usar o operador spread, mas é lento no Node 8.2 e abaixo:

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

A situação mudou no Node 8.3+. Agora, o spread é executado muito mais rápido, ainda mais rápido do que um loop for.

Aplicação Parcial (Currying) e Encadernação

Currying é quebrar uma função que recebe vários argumentos em uma série de funções onde cada nova função recebe apenas um argumento.

Digamos que temos uma função add simples. A versão curry desta função recebe um argumento, num1 . Ele retorna uma função que recebe outro argumento num2 e retorna a soma de num1 e 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

O método bind retorna uma função curried com uma sintaxe terser.

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

Portanto, o bind é incrível, mas é lento nas versões mais antigas do Node. No Node 8.3+, o bind é muito mais rápido e você pode usá-lo sem se preocupar com nenhum impacto no desempenho.

Experimentos

Vários experimentos foram conduzidos para comparar o desempenho do Nó 6 com o Nó 8 em alto nível. Observe que eles foram conduzidos no Node 8.0 para que não incluam as melhorias mencionadas acima que são específicas do Node 8.3+ graças à atualização V8 versão 6.

O tempo de renderização do servidor no Nó 8 foi 25% menor do que no Nó 6. Em projetos grandes, o número de instâncias do servidor pode ser reduzido de 100 para 75. Isso é surpreendente. Testar um conjunto de 500 testes no Node 8 foi 10% mais rápido. As compilações do Webpack foram 7% mais rápidas. Em geral, os resultados mostraram um notável aumento de desempenho no Node 8.

Recursos do Nó 8

A velocidade não foi a única melhoria no Node 8. Ele também trouxe vários novos recursos úteis - talvez o mais importante, async/await .

Assíncrono/Aguardar no Nó 8

Retornos de chamada e promessas geralmente são usados ​​para lidar com código assíncrono em JavaScript. Os retornos de chamada são notórios por produzirem código insustentável. Eles causaram caos (conhecido especificamente como callback hell ) na comunidade JavaScript. As promessas nos resgataram do inferno do callback por um longo tempo, mas ainda não tinham a limpeza do código síncrono. Async/await é uma abordagem moderna que permite escrever código assíncrono que se parece com código síncrono.

E embora o async/await pudesse ser usado em versões anteriores do Node, ele exigia bibliotecas e ferramentas externas – por exemplo, pré-processamento extra via Babel. Agora está disponível nativamente, pronto para uso.

Vou falar sobre alguns casos em que async/await é superior às promessas convencionais.

Condicionais

Imagine que você está buscando dados e determinará se uma nova chamada de API é necessária com base na carga útil . Dê uma olhada no código abaixo para ver como isso é feito por meio da abordagem de “promessas convencionais”.

 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 você pode ver, o código acima já parece confuso, apenas com uma condicional extra. Async/await envolve menos aninhamento:

 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; } };

Manipulação de erros

Async/await concede acesso para lidar com erros síncronos e assíncronos em try/catch. Digamos que você queira analisar o JSON proveniente de uma chamada de API assíncrona. Um único try/catch pode lidar com erros de análise e erros de API.

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

Valores intermediários

E se uma promessa precisar de um argumento que deve ser resolvido a partir de outra promessa? Isso significa que as chamadas assíncronas devem ser executadas em série.

Usando promessas convencionais, você pode acabar com um 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 brilha neste caso, onde são necessárias chamadas assíncronas encadeadas:

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

Assíncrono em Paralelo

E se você quiser chamar mais de uma função assíncrona em paralelo? No código abaixo, aguardaremos a resolução de fetchHouseData e, em seguida, chamaremos fetchCarData . Embora cada um deles seja independente do outro, eles são processados ​​sequencialmente. Você aguardará dois segundos para que ambas as APIs sejam resolvidas. Isto não é bom.

 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();

Uma abordagem melhor é processar as chamadas assíncronas em paralelo. Verifique o código abaixo para ter uma ideia de como isso é feito em 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();

O processamento dessas chamadas em paralelo faz com que você espere apenas um segundo para ambas as chamadas.

Novas funções da biblioteca principal

O nó 8 também traz algumas novas funções principais.

Copiar arquivos

Antes do Nó 8, para copiar arquivos, costumávamos criar dois fluxos e canalizar dados de um para o outro. O código abaixo mostra como o fluxo de leitura canaliza dados para o fluxo de gravação. Como você pode ver, o código é confuso para uma ação tão simples como copiar um arquivo.

 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);

No Nó 8 fs.copyFile e fs.copyFileSync são novas abordagens para copiar arquivos com muito menos problemas.

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

Promessa e Callbackify

util.promisify converte uma função regular em uma função assíncrona. Observe que a função inserida deve seguir o estilo de retorno de chamada Node.js comum. Ele deve receber um retorno de chamada como último argumento, ou seja, (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 você pode ver, util.promisify converteu fs.readFile em uma função assíncrona.

Por outro lado, o Node.js vem com util.callbackify . util.callbackify é o oposto de util.promisify : ele converte uma função assíncrona em uma função de estilo de retorno de chamada Node.js.

função destroy para legíveis e graváveis

A função destroy no Node 8 é uma maneira documentada de destruir/fechar/abortar um fluxo legível ou gravável:

 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']);

O código acima resulta na criação de um novo arquivo chamado big.txt (se ainda não existir) com o texto New text. .

As funções Readable.destroy e Writeable.destroy no Node 8 emitem um evento close e um evento de error opcionaldestroy não significa necessariamente que algo deu errado.

Operador de Spread

O operador spread (também conhecido como ... ) funcionou no Node 6, mas apenas com arrays e outros iteráveis:

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

No nó 8, os objetos também podem usar o operador spread:

 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' } */

Recursos experimentais no Nó 8 LTS

Os recursos experimentais não são estáveis, podem ficar obsoletos e podem ser atualizados com o tempo. Não use nenhum desses recursos em produção até que eles se tornem estáveis.

Ganchos assíncronos

Os ganchos assíncronos rastreiam o tempo de vida dos recursos assíncronos criados dentro do Node por meio de uma API.

Certifique-se de entender o loop de eventos antes de prosseguir com ganchos assíncronos. Este vídeo pode ajudar. Ganchos assíncronos são úteis para depurar funções assíncronas. Eles têm várias aplicações; um deles são os rastreamentos de pilha de erros para funções assíncronas.

Dê uma olhada no código abaixo. Observe que console.log é uma função assíncrona. Portanto, não pode ser usado dentro de ganchos assíncronos. fs.writeSync é usado em vez disso.

 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();

Assista a este vídeo para saber mais sobre ganchos assíncronos. Em termos de um guia Node.js especificamente, este artigo ajuda a desmistificar os ganchos assíncronos por meio de um aplicativo ilustrativo.

Módulos ES6 no Nó 8

O nó 8 agora suporta módulos ES6, permitindo que você use esta sintaxe:

 import { UtilityService } from './utility_service';

Para usar os módulos ES6 no Nó 8, você precisa fazer o seguinte.

  1. Adicione o --experimental-modules à linha de comando
  2. Renomeie as extensões de arquivo de .js para .mjs

HTTP/2

O HTTP/2 é a atualização mais recente do protocolo HTTP não atualizado com frequência, e o Node 8.4+ o suporta nativamente no modo experimental. É mais rápido, mais seguro e mais eficiente que seu predecessor, HTTP/1.1. E o Google recomenda que você o use. Mas o que mais ele faz?

Multiplexação

No HTTP/1.1, o servidor só podia enviar uma resposta por conexão por vez. No HTTP/2, o servidor pode enviar mais de uma resposta em paralelo.

Push do servidor

O servidor pode enviar várias respostas para uma única solicitação de cliente. Por que isso é benéfico? Tome um aplicativo da web como exemplo. Convencionalmente,

  1. O cliente solicita um documento HTML.
  2. O cliente descobre os recursos necessários no documento HTML.
  3. O cliente envia uma solicitação HTTP para cada recurso necessário. Por exemplo, o cliente envia uma solicitação HTTP para cada recurso JS e CSS mencionado no documento.

O recurso server-push faz uso do fato de que o servidor já conhece todos esses recursos. O servidor envia esses recursos para o cliente. Portanto, para o exemplo do aplicativo da Web, o servidor envia todos os recursos após o cliente solicitar o documento inicial. Isso reduz a latência.

Priorização

O cliente pode definir um esquema de priorização para determinar a importância de cada resposta necessária. O servidor pode então usar esse esquema para priorizar a alocação de memória, CPU, largura de banda e outros recursos.

Abandonando velhos hábitos ruins

Como o HTTP/1.1 não permitia a multiplexação, várias otimizações e soluções alternativas são usadas para encobrir a velocidade lenta e o carregamento de arquivos. Infelizmente, essas técnicas causam um aumento no consumo de RAM e atraso na renderização:

  • Fragmentação de domínio: Vários subdomínios foram usados ​​para que as conexões sejam dispersas e processadas em paralelo.
  • Combinando arquivos CSS e JavaScript para reduzir o número de solicitações.
  • Mapas Sprite: Combinando arquivos de imagem para reduzir solicitações HTTP.
  • Inlining: CSS e JavaScript são colocados diretamente no HTML para reduzir o número de conexões.

Agora, com HTTP/2, você pode esquecer essas técnicas e se concentrar em seu código.

Mas como você usa HTTP/2?

A maioria dos navegadores suporta HTTP/2 apenas por meio de uma conexão SSL segura. Este artigo pode ajudá-lo a configurar um certificado autoassinado. Adicione o arquivo .crt gerado e o arquivo .key em um diretório chamado ssl . Em seguida, adicione o código abaixo a um arquivo chamado server.js .

Lembre-se de usar o --expose-http2 na linha de comando para habilitar esse recurso. Ou seja, o comando de execução para nosso exemplo é 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}`) );

É claro que o Nó 8, Nó 9, Nó 10 etc. ainda suportam o antigo HTTP 1.1 — a documentação oficial do Node.js em uma transação HTTP padrão não ficará obsoleta por muito tempo. Mas se você quiser usar HTTP/2, você pode ir mais fundo com este guia Node.js.

Então, devo usar o Node.js 8 no final?

O Node 8 chegou com melhorias de desempenho e com novos recursos como async/await, HTTP/2 e outros. Experimentos de ponta a ponta mostraram que o Nó 8 é cerca de 25% mais rápido que o Nó 6. Isso leva a uma economia substancial de custos. Então, para projetos greenfield, absolutamente! Mas para projetos existentes, você deve atualizar o Node?

Depende se você precisaria alterar muito do seu código existente. Este documento lista todas as alterações importantes do Node 8 se você estiver vindo do Node 6. Lembre-se de evitar problemas comuns reinstalando todos os pacotes npm do seu projeto usando a versão mais recente do Node 8. Além disso, sempre use a mesma versão do Node.js nas máquinas de desenvolvimento e nos servidores de produção. Boa sorte!

Relacionado:
  • Por que diabos eu usaria o Node.js? Um tutorial caso a caso
  • Depurando vazamentos de memória em aplicativos Node.js
  • Como criar uma API REST segura no Node.js
  • Cabin Fever Coding: um tutorial de back-end do Node.js