Paris, Bordeaux, Niort, Nantes, Rennes, Toulouse, Aix, Nice

Node ou DENO, TELLE EST LA QUESTION !? 🤔

Je vais réparer le monde que j’ai brisé, et le rendre meilleur qu’il l’était auparavant

. . .

Lors de la JS Conf de 2018, ayant eu lieu à Berlin, Ryan Dahl évoqua les 10 erreurs de conception de NodeJS. Quelque temps plus tard (le 13 mai 2020 exactement), la version 1.0.0 de Deno était née, ainsi que plusieurs nouvelles fonctionnalités. La citation précédente (tirée de l’épisode 2, de la saison 3 de Mr. Robot), n’aurait pas mieux traduit l’état d’esprit de Ryan Dahl à cette époque, vis-à-vis de NodeJS.

Théorie

Si vous vous interrogez… C’est quoi NodeJS ? Qui est Ryan Dahl ? C’est quoi Deno ? Cet article est fait pour vous ! 😉

NodeJS est un environnement d’exécution pour le langage JavaScript, basé sur le moteur Chrome V8. Si vous êtes déjà adepte de ce langage de programmation, vous avez forcément NodeJS (et NPM) installé sur votre ordinateur. Historiquement, le moteur Chrome V8 (développé par l’équipe Chromium) voit le jour en 2008, et avec lui la possibilité de compiler directement le code JavaScript en code machine natif, avant de l’exécuter. De nos jours, il est embarqué dans plusieurs solutions incontournables, telles que Chrome, MongoDB ou encore NodeJS.

Ryan Dahl est, ni plus, ni moins, que le créateur de NodeJS. Développé depuis 2008 avec le langage C++ (et basé sur le moteur Chrome V8), NodeJS intégrera quelque temps plus tard, son propre gestionnaire de paquets (NPM), et deviendra très vite un indispensable de l’écosystème JavaScript.

NB : Il se peut que je fasse quelques raccourcis lors de mes explications. En effet, l’écosystème JavaScript est tellement vaste aujourd’hui, que ces quelques lignes / paragraphes ne suffisent pas à décrire en totalité ce sujet…

Depuis 2010, les technologies JavaScript ne cessent de croitre. La preuve : il fait partie des langages de programmation les plus utilisés par les développeurs, avec Java et Python. Parmi ces technologies, on retrouve les frameworks frontend, tels que Angular, React ou encore VueJS ; mais aussi les frameworks backend, notamment ExpressJS, Polka, Koa, etc… En 2018, alors que tout le monde avait les yeux rivés sur le concept de JAMStack, Ryan Dahl commença à travailler sur le « successeur » de NodeJS, j’ai nommé : Deno !

Tout comme NodeJS, Deno est également basé sur le moteur Chrome V8, mais contrairement à son homologue, celui-ci est développé avec le langage Rust. De même, la gestion de l’asynchronisme diffère, puisque cette fois encore, Deno se réfère à Tokio pour le traitement des événements.

NB : Pour rappel, JavaScript est un langage synchrone. C’est-à-dire qu’il exécute une seule opération à la fois (au sein de ce qu’on appelle, la CallStack). Les opérations asynchrones, telles que les appels XHR, ou encore les timers, sont pris en charge par l’environnement dans lequel s’exécute le code (soit le navigateur, soit NodeJS / Deno). En général, on parle d’APIs Web.

Revenons-en au sujet : nous sommes le 13 mai 2020, la version 1.0.0 de Deno voit le jour. Parmi son lot de nouveautés, on retrouve en premier lieu l’exécution native du code TypeScript. Contrairement à NodeJS qui prend « seulement » en charge la syntaxe CommonJS (ou ES Modules via l’extension .mjs), Deno supporte complètement le sur-ensemble typé de Microsoft, à savoir TypeScript.

Seconde nouveauté : la gestion des dépendances. La trop forte relation avec NPM (et le package.json) figure parmi les erreurs de conception de NodeJS, d’après Ryan Dahl. Pour pallier à cela, Deno récupère ce dont il a besoin directement à partir du Web. Il suffit alors d’importer les modules depuis une URL au sein du code (plutôt que faire référence aux node_modules). Cette fonctionnalité donnera lieu à la convention « deps.ts » , qui (comme son homologue, le package.json) permet de regrouper toutes les dépendances externes au sein d’un seul et même fichier.

deps.ts

L’autre changement notable qui vient avec Rust, est l’omniprésence de la sécurité lors de l’exécution des scripts. En effet, Deno ne vous permettra pas de lire et / ou d’écrire un fichier sans en être préalablement autorisé. Pour cela, il faut spécifier les autorisations lors de l’interprétation du code. Il en va de même avec les appels externes. Par exemple, si vous souhaitez réaliser une API qui ira écrire dans une base de données distante, vous devrez autoriser les accès réseaux. Cela se traduit simplement par l’ajout de « flags » lors de l’utilisation de l’outil de ligne de commande :

deno run --allow-net main.ts 

Aujourd’hui encore, NodeJS ne se soucie pas de cette dimension, ce qui lui vaut quelques critiques…

Concernant le coût de mise en oeuvre de Deno, comme pour NodeJS, tout a été pensé. Que vous soyez sur Linux, Windows ou Mac OS ; que ce soit via Curl, le PowerShell ou encore HomeBrew ; les moyens pour installer l’outil de ligne de commande ne manquent pas. Celle-ci se veut d’ailleurs très pratique, puisqu’elle propose un mode REPL, la possibilité de linter et / ou de formater le code, ainsi que de mettre Deno à jour, tout simplement.

Les fonctionnalités de Deno sont nombreuses ! Je pourrais aussi mentionner sa capacité à compiler le WebAssembly nativement, mais ne l’ayant pas encore testé, je vous invite à aller faire un tour sur la documentation officielle.

En pratique…

Trêve de théorie, place à la pratique. Il paraît que Deno est plus performant que NodeJS (puisque codé en Rust), voyons si c’est vraiment le cas… Ici, j’ai choisi de confronter ces deux environnements JavaScript avec trois cas d’usage :

  • L’exécution d’un script simple
  • L’exécution d’un script avec interactions au système de fichiers
  • L’exécution d’un script avec accès réseaux

NB : Les versions de NodeJS et de Deno utilisées sont respectivement 14.8.0 et 1.3.0.

#1 – Fibonacci

Vous l’aurez reconnu, ce premier script permet de récupérer le n-ième nombre de la suite de Fibonacci. J’ai volontairement réalisé deux fonctions, une itérative (pour un parcours linéaire) et une récursive (pour un parcours d’arbre), afin de révéler s’il existe une différence de traitement de ces fonctions, entre NodeJS et Deno. En ajoutant un wrapper de temps (ici showTime()), j’obtiens les résultats suivants :

x 1000 2000 3000 4000 5000
Node 3ms 6ms 12ms 24ms 36ms
Deno 3ms 8ms 14ms 27ms 39ms

iterativeFibonacci

x 10 20 30 40 50
Node < 1ms 2ms 9ms 1050ms 141.680s
Deno 1ms 3ms 13ms 1320ms 183.450s

recursiveFibonacci

On remarque très vite que le parcours linéaire (itératif) est drastiquement plus performant que le parcours d’arbre (récursif). Plus intéressant encore, les chiffres sont réguliers ! Peu importe l’environnement, les comportements sont similaires : 

  • Temps d’exécution linéaire avec iterativeFibonacci()
  • Temps d’exécution exponentiel avec recursiveFibonacci()

Malheureusement, les chiffres ne mentent pas. On est forcé de constater que Deno est un peu en retard vis-à-vis de NodeJS. De manière récursive, ce dernier récupère la 5000ème occurrence de la suite de Fibonacci en 2 minutes et 20 secondes, alors qu’il faut environ 40 secondes supplémentaires à Deno pour arriver au même résultat. Malgré ce léger retard, je me suis aperçu lors de mes tests, que la CallStack se remplissait plus vite avec NodeJS (une différence d’environ 150 à 200 opérations), pour une même allocation de ressources.

Fait intéressant :

En parlant de « tests », j’en profite pour signaler que Deno est livré avec une API de test unitaire intégrée. Il est donc très simple de tester rapidement son code, là où avec NodeJS, j’aurais eu besoin de NPM pour récupérer Karma / Mocha (ou mieux Jest), afin de lancer mes tests unitaires. Voici un exemple concret, avec les fonctions de Fibonacci :

#2 – Files Renamer

Passons maintenant à un cas d’usage plus pratique, avec un script de renommage massif de fichiers.

filesRenamer.js
filesRenamer.ts

Vous l’aurez remarqué, je suis passé à TypeScript dans ce second script. De plus, si vous tentez de l’exécuter, vous allez très vite déchanter… À partir de maintenant la sécurité entre en jeu ! En effet, lorsqu’on va vouloir interagir avec les fichiers (en lecture ou écriture), il va falloir autoriser Deno à le faire, en passant par la commande suivante :

deno run -–allow-read –-allow-write filesRenamer.ts 

Plutôt simple, non !? 😏 Il suffit d’y penser…

Ce qui est intéressant ici (outre les performances) ce sont les différences et similitudes qui existent entre l’API de Deno et celle de NodeJS. Dans l’ensemble, même si les scripts sont construits de la même façon (lancement avec les arguments, lecture du dossier, lecture du fichier, écriture du fichier), on s’aperçoit que l’on gagne quelques lignes de code avec Deno. En faisant un focus sur les fonctions readDir(), on remarque qu’elles ne retournent pas la même structure de données. L’une retourne seulement les noms des fichiers contenus dans le dossier parcouru, alors que l’autre retourne une liste d’objets, qui disposent notamment du nom du fichier, mais surtout de la nature du fichier. Cela m’évite donc l’appel à la fonction stat() pour savoir s’il s’agit d’un dossier (ou non), puisque la donnée est directement accessible.

Je trouve que Ryan Dahl a su tirer parti des avantages et inconvénients de NodeJS, et a ainsi comblé les manques avec Deno. L’exemple le plus concret de cette conjecture est l’utilisation native des promesses à l’instar de l’usage des fonctions callback. En outre, Deno a su conserver les versions synchrones et asynchrones pour certaines fonctions : chmod / chmodSync, mkdir / mkdirSync, remove / removeSync, etc… Ce qui est plutôt une bonne approche si on souhaite séduire un plus large public.

NB : La version 10 de NodeJS signe l’arrivée des fonctions promesses du module « fs ». Avant cela, il fallait « promisifier » toutes les fonctions grâce au module « util » de NodeJS.

Fichiers 1000 2000 3000 4000 5000
Node 220ms 425ms 625ms 980ms 1025ms
Deno 455ms 885ms 1340ms 2050ms 2255ms

Pour ce qui est des performances, encore une fois, les données ci-dessus corroborent les temps d’exécution obtenue sur les fonctions de Fibonacci. NodeJS reste plus rapide que Deno à l’heure actuelle. D’ailleurs, d’après ce test, ce dernier est au moins 2 fois plus lent à exécuter le code JavaScript / TypeScript que son homologue.

#3 – Web Server

Le dernier aspect que je souhaite mettre en lumière, est la mise en oeuvre d’un serveur HTTP. Dans ces deux derniers scripts, que ce soit pour NodeJS ou Deno, la mise en place d’un serveur Web se fait très simplement (comme le veut la philosophie du JavaScript). Tous deux utilisent leur module « http » : NodeJS l’importe depuis les node_modules, alors que Deno le récupère depuis ses librairies standard. 

NB : La récupération de modules à partir d’URLs ne signifie pas que le Web est constamment sollicité. Au premier appel, Deno met en cache la version du module spécifié lors de l’import, pour les prochains usages.

Pour ce qui est de leur temps de réponse, j’ai constaté qu’ils mettent 2ms à répondre à la requête « /whoami » en GET. Évidemment, l’exemple ci-dessous est trivial, et si l’on souhaite mettre en oeuvre un service backend performant, on ira tout de suite chercher un framework adapté qui offre plus de fonctionnalités. Cependant, ces deux bouts de code représentent la base de certains frameworks Web (notamment ExpressJS pour NodeJS, ou Alosaur pour Deno).

webServer.js
webServer.ts

Autre fait intéressant :

Deno implémente la plupart des APIs Web. C’est-à-dire que les fonctions telles que setTimeout, clearTimeout, setInterval, clearInterval sont accessibles, mais également fetch ! Donc, si vous souhaitez récupérer une ressource à partir d’une URL, c’est possible nativement sans avoir besoin d’utiliser un éventuel Axios (bien qu’il existe déjà en tant que librairie tierce), ou tout autre librairie similaire. Puisqu’une démo vaut mieux que des mots, voici ce que je vous propose :

deno run —allow-net getArticles.ts mrdoomy
getArticles.ts

Contre toute attente, ces deux environnements d’exécution pour le langage JavaScript ne sont pas si différents l’un de l’autre. Ce qui m’a heurté en premier lieu avec Deno, c’est l’utilisation de dépendances au travers d’import qui font directement référence au Web. Le fait de se passer de NPM (et du package.json) est assez déroutant, mais on s’y fait vite grâce à la convention « deps.ts » .

Ensuite, l’utilisation native du TypeScript est fortement appréciée. J’insiste sur le mot « native » , car avec NodeJS il aurait fallu configurer son environnement et transpiler le code pour finalement l’exécuter. Bien sûr, ces tâches sont généralement prises en charge par un bundler (Webpack / RollupJS), mais malgré tout, il s’agit d’une couche supplémentaire dont on pourrait se délester.

Enfin, le concept de permissions m’a tout de suite plu. En effet, le fait d’autoriser (ou non) la lecture, l’écriture, les accès réseaux, etc…  Permet d’être pleinement maitre du code que l’on joue. Les éventuels risques liés à la sécurité sont ainsi gérés, là où NodeJS est pour l’instant incapable de s’en prémunir…

NB : Je suis ravi de devoir spécifier la lecture et l’écriture (distinctement) lorsque je travaille sur le système de fichiers avec un chemin absolu. Une erreur est vite arrivée… Mais bien sûr, personne ne fait ça. 😅

À l’heure où j’écris ces quelques lignes / paragraphes, Deno a le vent en poupe ! Comparé à NodeJS, il est plus sécurisé et plus léger. Bien qu’il n’arrive pas (encore) à égaliser ce dernier en matière de rapidité d’exécution, il représente un fort (et unique) concurrent en tant qu’environnement JavaScript.

De par son mode fonctionnement, ainsi que ses nombreuses fonctionnalités, Ryan Dahl a clairement su combler les lacunes de sa précédente création en développant cette nouvelle technologie. Deno s’inscrit aujourd’hui dans un contexte de Web moderne (notamment au regard des appels de dépendances). Le support du TypeScript, « corrige » l’aspect faiblement typé du JavaScript, et fait ainsi de Deno une solution complète. De plus, la présence de Rust au sein de son code, promet bien des choses en termes de performances.

La communauté est forte ! À tel point qu’on voit apparaitre chaque jour de plus en plus de librairies tierces, je veux notamment parler de MongoDB, Prettier, GraphQL, Moment, etc… Certains incontournables de NPM sont déjà prêt pour Deno. De même, si vous souhaitez jouer avec de l’authentification / du chiffrement au sein de tes APIs ; BCrypt, JWT et OAuth2 (pour ne citer qu’eux) répondent aussi présent à l’appel ! D’ailleurs, je tiens à signaler qu’il existe une multitude de frameworks backend avec Deno, le choix vous appartient (mais je vous conseille d’aller faire un tour chez Alosaur).

Le mot de la fin

Pour l’instant, je n’abandonnerais pas NodeJS. Il s’agit d’une solution mature dans l’écosystème Web, qui commence à se répandre dans le monde de l’entreprise. En France, les petites / moyennes entreprises ont déjà opté pour cette solution, et les grandes s’y mettent davantage (au détriment de Spring / Django). Cependant, je suis très enthousiaste à propos de Deno. Au même titre que GraphQL vis-à-vis de REST, je le considère actuellement comme une alternative, mais je pense qu’il va faire changer les moeurs. L’aspect sécuritaire devrait inciter les professionnels à migrer certaines de leurs applications vers l’environnement JavaScript. Bien que les dépendances standard de Deno soient stables, elles ne sont (pour la plupart) pas encore disponibles en version « finale » / 1.0.0, mais lorsque ce sera le cas, nous devrions assister à un changement majeur / une migration au sein de la communauté des développeurs… Se laisseront-ils tenter par le côté obscur !? 🙄

CET ARTICLE A ÉTÉ RÉDIGÉ PAR :

Damien Chazoule
INGÉNIEUR INFORMATIQUE
ALLTECH Niort