Est-il temps d'utiliser le nœud 8 ?
Publié: 2022-03-11Le nœud 8 est sorti ! En fait, Node 8 est maintenant sorti depuis assez longtemps pour voir une utilisation solide dans le monde réel. Il est venu avec un nouveau moteur V8 rapide et de nouvelles fonctionnalités, notamment async/wait, HTTP/2 et les crochets asynchrones. Mais est-ce prêt pour votre projet ? Découvrons-le!
Note de l'éditeur : vous savez probablement que Node 10 (nom de code Dubnium ) est également sorti. Nous choisissons de nous concentrer sur le nœud 8 ( Carbon ) pour deux raisons : (1) le nœud 10 vient d'entrer dans sa phase de support à long terme (LTS) et (2) le nœud 8 a marqué une itération plus importante que le nœud 10. .
Performances dans Node 8 LTS
Nous commencerons par examiner les améliorations de performances et les nouvelles fonctionnalités de cette version remarquable. L'un des principaux domaines d'amélioration concerne le moteur JavaScript de Node.
Qu'est - ce qu'un moteur JavaScript exactement, de toute façon ?
Un moteur JavaScript exécute et optimise le code. Il peut s'agir d'un interpréteur standard ou d'un compilateur juste-à-temps (JIT) qui compile JavaScript en bytecode. Les moteurs JS utilisés par Node.js sont tous des compilateurs JIT, pas des interpréteurs.
Le moteur V8
Node.js utilise le moteur JavaScript Chrome V8 de Google, ou simplement V8 , depuis le début. Certaines versions de Node sont utilisées pour se synchroniser avec une version plus récente de V8. Mais attention à ne pas confondre V8 avec Node 8 car nous comparons ici les versions V8.
C'est facile à trébucher, car dans les contextes logiciels, nous utilisons souvent "v8" comme argot ou même une forme courte officielle pour "version 8", donc certains pourraient confondre "Node V8" ou "Node.js V8" avec "NodeJS 8 ”, mais nous avons évité cela tout au long de cet article pour aider à garder les choses claires : V8 signifiera toujours le moteur, pas la version de Node.
V8 Version 5
Node 6 utilise la version 5 de V8 comme moteur JavaScript. (Les premières versions ponctuelles de Node 8 utilisent également la version 5 de V8, mais elles utilisent une version ponctuelle V8 plus récente que celle de Node 6.)
Compilateurs
Les versions V8 5 et antérieures ont deux compilateurs :
- Full-codegen est un compilateur JIT simple et rapide mais produit un code machine lent.
- Crankshaft est un compilateur JIT complexe qui produit un code machine optimisé.
Fils
Au fond, V8 utilise plus d'un type de thread :
- Le thread principal récupère le code, le compile, puis l'exécute.
- Les threads secondaires exécutent du code pendant que le thread principal optimise le code.
- Le thread du profileur informe le runtime des méthodes non performantes. Le vilebrequin optimise ensuite ces méthodes.
- D'autres threads gèrent le ramasse-miettes.
Processus de compilation
Tout d'abord, le compilateur Full-codegen exécute le code JavaScript. Pendant l'exécution du code, le thread du profileur rassemble des données pour déterminer les méthodes que le moteur va optimiser. Sur un autre fil, Crankshaft optimise ces méthodes.
Questions
L'approche mentionnée ci-dessus présente deux problèmes principaux. Tout d'abord, il est architecturalement complexe. Deuxièmement, le code machine compilé consomme beaucoup plus de mémoire. La quantité de mémoire consommée est indépendante du nombre d'exécutions du code. Même le code qui ne s'exécute qu'une seule fois occupe également une quantité importante de mémoire.
V8 Version 6
La première version de Node à utiliser le moteur V8 release 6 est Node 8.3.
Dans la version 6, l'équipe V8 a créé Ignition et TurboFan pour atténuer ces problèmes. Ignition et TurboFan remplacent respectivement Full-codegen et CrankShaft.
La nouvelle architecture est plus simple et consomme moins de mémoire.
Ignition compile le code JavaScript en bytecode au lieu du code machine, économisant ainsi beaucoup de mémoire. Ensuite, TurboFan, le compilateur d'optimisation, génère un code machine optimisé à partir de ce bytecode.
Améliorations spécifiques des performances
Passons en revue les domaines dans lesquels les performances de Node 8.3+ ont changé par rapport aux anciennes versions de Node.
Création d'objets
La création d'objets est environ cinq fois plus rapide dans Node 8.3+ que dans Node 6.
Taille de la fonction
Le moteur V8 décide si une fonction doit être optimisée en fonction de plusieurs facteurs. Un facteur est la taille de la fonction. Les petites fonctions sont optimisées, tandis que les fonctions longues ne le sont pas.
Comment la taille de la fonction est-elle calculée ?
Le vilebrequin de l'ancien moteur V8 utilise le "nombre de caractères" pour déterminer la taille de la fonction. Les espaces blancs et les commentaires dans une fonction réduisent les chances qu'elle soit optimisée. Je sais que cela pourrait vous surprendre, mais à l'époque, un commentaire pouvait réduire la vitesse d'environ 10 %.
Dans Node 8.3+, les caractères non pertinents tels que les espaces et les commentaires ne nuisent pas aux performances de la fonction. Pourquoi pas?
Parce que le nouveau TurboFan ne compte pas les caractères pour déterminer la taille de la fonction. Au lieu de cela, il compte les nœuds de l'arbre de syntaxe abstraite (AST), de sorte qu'il ne prend en compte que les instructions de fonction réelles . En utilisant Node 8.3+, vous pouvez ajouter des commentaires et des espaces autant que vous le souhaitez.
Arguments de mise en Array
Les fonctions régulières en JavaScript portent un objet d' argument
de type Array
implicite.
Que signifie Array
-like ?
L'objet arguments
agit un peu comme un tableau. Il a la propriété length
mais n'a pas les méthodes intégrées de Array
comme forEach
et map
.
Voici comment fonctionne l'objet 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");
Alors, comment pourrions-nous convertir l'objet arguments
en tableau ? En utilisant le laconique 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)
nuit aux performances dans toutes les versions de Node. Par conséquent, la copie des clés via une boucle for
fonctionne mieux :
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]
La boucle for
est un peu lourde, n'est-ce pas ? Nous pourrions utiliser l'opérateur de propagation, mais il est lent dans le nœud 8.2 et inférieur :
function test() { const r = [...arguments]; console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output [2, 4, 6]
La situation a changé dans le nœud 8.3+. Maintenant, la propagation s'exécute beaucoup plus rapidement, même plus rapidement qu'une boucle for.
Application partielle (curry) et reliure
Currying décompose une fonction qui prend plusieurs arguments en une série de fonctions où chaque nouvelle fonction ne prend qu'un seul argument.
Disons que nous avons une simple fonction d' add
. La version curry de cette fonction prend un argument, num1
. Elle renvoie une fonction qui prend un autre argument num2
et renvoie la somme de num1
et 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
La méthode bind
renvoie une fonction curry avec une syntaxe concise.
function add(num1, num2) { return num1 + num2; } const add5 = add.bind(null, 5); add5(3); // returns 8
La bind
est donc incroyable, mais elle est lente dans les anciennes versions de Node. Dans Node 8.3+, la bind
est beaucoup plus rapide et vous pouvez l'utiliser sans vous soucier des performances.
Expériences
Plusieurs expériences ont été menées pour comparer les performances du nœud 6 au nœud 8 à un niveau élevé. Notez que celles-ci ont été menées sur Node 8.0, elles n'incluent donc pas les améliorations mentionnées ci-dessus qui sont spécifiques à Node 8.3+ grâce à sa mise à niveau V8 release 6.
Le temps de rendu du serveur dans le nœud 8 était de 25 % inférieur à celui du nœud 6. Dans les grands projets, le nombre d'instances de serveur pouvait être réduit de 100 à 75. C'est étonnant. Tester une suite de 500 tests dans Node 8 était 10 % plus rapide. Les builds Webpack étaient 7% plus rapides. En général, les résultats ont montré une amélioration notable des performances dans le nœud 8.
Fonctionnalités du nœud 8
La vitesse n'était pas la seule amélioration de Node 8. Elle a également apporté plusieurs nouvelles fonctionnalités pratiques, peut-être la plus importante, async/wait .
Asynchrone/Attente dans le nœud 8
Les rappels et les promesses sont généralement utilisés pour gérer le code asynchrone en JavaScript. Les rappels sont connus pour produire du code non maintenable. Ils ont semé le chaos (connu spécifiquement sous le nom de callback hell ) dans la communauté JavaScript. Les promesses nous ont longtemps sauvés de l'enfer des rappels, mais il leur manquait toujours la propreté du code synchrone. Async/wait est une approche moderne qui vous permet d'écrire du code asynchrone qui ressemble à du code synchrone.
Et bien que async/wait puisse être utilisé dans les versions précédentes de Node, il nécessitait des bibliothèques et des outils externes, par exemple un prétraitement supplémentaire via Babel. Il est maintenant disponible nativement, prêt à l'emploi.
Je parlerai de certains cas où async/wait est supérieur aux promesses conventionnelles.
Conditionnels
Imaginez que vous récupérez des données et que vous déterminerez si un nouvel appel API est nécessaire en fonction de la charge utile . Jetez un œil au code ci-dessous pour voir comment cela se fait via l'approche des "promesses conventionnelles".
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; } }); };
Comme vous pouvez le voir, le code ci-dessus semble déjà désordonné, juste à partir d'une condition supplémentaire. Async/wait implique moins d'imbrication :
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; } };
La gestion des erreurs
Async/wait vous accorde un accès pour gérer les erreurs synchrones et asynchrones dans try/catch. Supposons que vous souhaitiez analyser JSON provenant d'un appel d'API asynchrone. Un seul try/catch peut gérer à la fois les erreurs d'analyse et les erreurs d'API.
const request = async () => { try { console.log(await getData()); } catch (err) { console.log(err); } };
Valeurs intermédiaires
Que se passe-t-il si une promesse a besoin d'un argument qui doit être résolu à partir d'une autre promesse ? Cela signifie que les appels asynchrones doivent être effectués en série.
En utilisant des promesses conventionnelles, vous pourriez vous retrouver avec un code comme celui-ci :
const request = () => { return fetchUserData() .then(userData => { return fetchCompanyData(userData); }) .then(companyData => { return fetchRetiringPlan(userData, companyData); }) .then(retiringPlan => { const retiringPlan = retiringPlan; }); };
Async/wait brille dans ce cas, où des appels asynchrones chaînés sont nécessaires :
const request = async () => { const userData = await fetchUserData(); const companyData = await fetchCompanyData(userData); const retiringPlan = await fetchRetiringPlan(userData, companyData); };
Asynchrone en parallèle
Que faire si vous souhaitez appeler plusieurs fonctions asynchrones en parallèle ? Dans le code ci-dessous, nous attendrons la résolution de fetchHouseData
, puis nous appellerons fetchCarData
. Bien que chacun d'eux soit indépendant de l'autre, ils sont traités séquentiellement. Vous attendrez deux secondes pour que les deux API se résolvent. Ce n'est pas 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();
Une meilleure approche consiste à traiter les appels asynchrones en parallèle. Vérifiez le code ci-dessous pour avoir une idée de la façon dont cela est réalisé dans async/wait.
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();
Le traitement de ces appels en parallèle ne vous fait attendre qu'une seconde pour les deux appels.
Nouvelles fonctions de la bibliothèque principale
Le nœud 8 apporte également de nouvelles fonctions de base.
Copier des fichiers
Avant Node 8, pour copier des fichiers, nous avions l'habitude de créer deux flux et de diriger les données de l'un vers l'autre. Le code ci-dessous montre comment le flux de lecture dirige les données vers le flux d'écriture. Comme vous pouvez le voir, le code est encombré pour une action aussi simple que la copie d'un fichier.
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);
Dans Node 8, fs.copyFile
et fs.copyFileSync
sont de nouvelles approches pour copier des fichiers avec beaucoup moins de tracas.
const fs = require("fs"); fs.copyFile("firstFile.txt", "secondFile.txt", err => { if (err) { console.log(err); } else { console.log("File copied"); } });
Promesse et rappel
util.promisify
convertit une fonction normale en fonction asynchrone. Notez que la fonction saisie doit suivre le style de rappel Node.js commun. Il devrait prendre un rappel comme dernier argument, c'est-à-dire (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));
Comme vous avez pu le voir, util.promisify
a converti fs.readFile
en une fonction asynchrone.
D'autre part, Node.js est livré avec util.callbackify
. util.callbackify
est l'opposé de util.promisify
: il convertit une fonction asynchrone en une fonction de style rappel Node.js.
Fonction destroy
pour les éléments lisibles et inscriptibles
La fonction destroy
dans Node 8 est un moyen documenté de détruire/fermer/abandonner un flux lisible ou inscriptible :
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']);
Le code ci-dessus entraîne la création d'un nouveau fichier nommé big.txt
(s'il n'existe pas déjà) avec le texte New text.
.
Les fonctions Readable.destroy
et Writeable.destroy
du nœud 8 émettent un événement de close
et un événement d' error
facultatif - destroy
ne signifie pas nécessairement que quelque chose s'est mal passé.
Opérateur de propagation
L'opérateur de propagation (alias ...
) fonctionnait dans le nœud 6, mais uniquement avec des tableaux et d'autres itérables :
const arr1 = [1,2,3,4,5,6] const arr2 = [...arr1, 9] console.log(arr2) // expected output: [1,2,3,4,5,6,9]
Dans le nœud 8, les objets peuvent également utiliser l'opérateur de propagation :
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' } */
Fonctionnalités expérimentales dans Node 8 LTS
Les fonctionnalités expérimentales ne sont pas stables, peuvent devenir obsolètes et être mises à jour avec le temps. N'utilisez aucune de ces fonctionnalités en production tant qu'elles ne sont pas stables.
Crochets asynchrones
Les hooks asynchrones suivent la durée de vie des ressources asynchrones créées dans Node via une API.
Assurez-vous de bien comprendre la boucle d'événements avant d'aller plus loin avec les crochets asynchrones. Cette vidéo pourrait vous aider. Les crochets asynchrones sont utiles pour déboguer les fonctions asynchrones. Ils ont plusieurs applications ; l'un d'eux est les traces de pile d'erreurs pour les fonctions asynchrones.
Jetez un oeil au code ci-dessous. Notez que console.log
est une fonction asynchrone. Ainsi, il ne peut pas être utilisé à l'intérieur de crochets asynchrones. fs.writeSync
est utilisé à la place.
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();
Regardez cette vidéo pour en savoir plus sur les crochets asynchrones. En termes de guide Node.js en particulier, cet article aide à démystifier les crochets asynchrones grâce à une application illustrative.
Modules ES6 dans le nœud 8
Node 8 prend désormais en charge les modules ES6, ce qui vous permet d'utiliser cette syntaxe :
import { UtilityService } from './utility_service';
Pour utiliser les modules ES6 dans Node 8, vous devez procéder comme suit.
- Ajoutez le drapeau
--experimental-modules
à la ligne de commande - Renommer les extensions de fichier de
.js
à.mjs
HTTP/2
HTTP/2 est la dernière mise à jour du protocole HTTP pas souvent mis à jour, et Node 8.4+ le prend en charge nativement en mode expérimental. Il est plus rapide, plus sécurisé et plus efficace que son prédécesseur, HTTP/1.1. Et Google vous recommande de l'utiliser. Mais que fait-il d'autre ?
Multiplexage
Dans HTTP/1.1, le serveur ne pouvait envoyer qu'une réponse par connexion à la fois. En HTTP/2, le serveur peut envoyer plusieurs réponses en parallèle.
Poussée du serveur
Le serveur peut pousser plusieurs réponses pour une seule requête client. Pourquoi est-ce bénéfique ? Prenons l'exemple d'une application Web. Classiquement,
- Le client demande un document HTML.
- Le client découvre les ressources nécessaires à partir du document HTML.
- Le client envoie une requête HTTP pour chaque ressource requise. Par exemple, le client envoie une requête HTTP pour chaque ressource JS et CSS mentionnée dans le document.
La fonctionnalité push du serveur utilise le fait que le serveur connaît déjà toutes ces ressources. Le serveur envoie ces ressources au client. Ainsi, pour l'exemple d'application Web, le serveur pousse toutes les ressources après que le client a demandé le document initial. Cela réduit la latence.
Priorisation
Le client peut définir un schéma de priorisation pour déterminer l'importance de chaque réponse requise. Le serveur peut ensuite utiliser ce schéma pour hiérarchiser l'allocation de mémoire, de CPU, de bande passante et d'autres ressources.
Se débarrasser des vieilles mauvaises habitudes
Étant donné que HTTP/1.1 n'autorisait pas le multiplexage, plusieurs optimisations et solutions de contournement sont utilisées pour couvrir la lenteur de la vitesse et du chargement des fichiers. Malheureusement, ces techniques provoquent une augmentation de la consommation RAM et un rendu retardé :
- Partage de domaine : plusieurs sous-domaines ont été utilisés afin que les connexions soient dispersées et traitées en parallèle.
- Combiner les fichiers CSS et JavaScript pour réduire le nombre de requêtes.
- Sprite maps : combinaison de fichiers image pour réduire les requêtes HTTP.
- Inlining : CSS et JavaScript sont placés directement dans le HTML pour réduire le nombre de connexions.
Désormais, avec HTTP/2, vous pouvez oublier ces techniques et vous concentrer sur votre code.
Mais comment utilisez-vous HTTP/2 ?
La plupart des navigateurs prennent en charge HTTP/2 uniquement via une connexion SSL sécurisée. Cet article peut vous aider à configurer un certificat auto-signé. Ajoutez le fichier .crt
généré et le fichier .key
dans un répertoire appelé ssl
. Ensuite, ajoutez le code ci-dessous à un fichier nommé server.js
.
N'oubliez pas d'utiliser l' --expose-http2
dans la ligne de commande pour activer cette fonctionnalité. C'est-à-dire que la commande run pour notre exemple est 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}`) );
Bien sûr, Node 8, Node 9, Node 10, etc. prennent toujours en charge l'ancien HTTP 1.1 - la documentation officielle de Node.js sur une transaction HTTP standard ne sera pas obsolète pendant longtemps. Mais si vous souhaitez utiliser HTTP/2, vous pouvez aller plus loin avec ce guide Node.js.
Alors, devrais-je utiliser Node.js 8 à la fin ?
Le nœud 8 est arrivé avec des améliorations de performances et de nouvelles fonctionnalités telles que async/wait, HTTP/2 et autres. Des expériences de bout en bout ont montré que le nœud 8 est environ 25 % plus rapide que le nœud 6. Cela entraîne des économies substantielles. Donc pour les projets greenfield, absolument ! Mais pour les projets existants, devriez-vous mettre à jour Node ?
Cela dépend si vous auriez besoin de modifier une grande partie de votre code existant. Ce document répertorie toutes les modifications majeures de Node 8 si vous venez de Node 6. N'oubliez pas d'éviter les problèmes courants en réinstallant tous les packages npm
de votre projet à l'aide de la dernière version de Node 8. De plus, utilisez toujours la même version de Node.js sur les machines de développement que sur les serveurs de production. Bonne chance!
- Pourquoi diable devrais-je utiliser Node.js ? Un tutoriel au cas par cas
- Débogage des fuites de mémoire dans les applications Node.js
- Création d'une API REST sécurisée dans Node.js
- Codage de la fièvre de la cabine : un didacticiel back-end Node.js