Les avantages et les inconvénients
Node.js vous permet de n’avoir qu’une seule technologie que ce soit pour le front ou le back : Javascript. Finie la recherche d’un profil idéal qui maîtrise 3 technos : quelqu’un maîtrisant Javascript peut très vite monter en compétence et être opérationnel en quelques jours.
C’est certes un avantage en termes de recrutement, mais qu’en est-il des avantages techniques ?
- Une application Node.js peut être lancée sur n’importe quelle plateforme, tant que vous avez installé Node sur la machine, ce qui laisse la liberté sur le choix du serveur.
- Node.js est event-driven. C'est-à-dire qu’il est nativement fait pour réagir à des événements. Un appel à l’API est un événement, l’ouverture d’un fichier est un événement…
Nous avons parlé des avantages, mais qu’en est-il des inconvénients ? Il y en a un principal : Node.js est mono-thread. Nous en reparlerons plus tard, mais il faudra à tout prix éviter les gros calculs pour ne pas bloquer le thread d’exécution de notre app.
Quelques connaissances utiles
Le cycle de release
Node.js a un cycle de release bien défini : il y a une release majeure tous les 6 mois, avec potentiellement des breaking changes. Les releases paires sont maintenues pendant une longue période (30 mois), ce qui permet de les utiliser librement. La dernière release paire est appelée la version “active”.
Les versions impaires sont utiles pour préparer le passage à la version paire suivante pour les développeurs de librairies.
Rythme des releases Node.js
Pour éviter d’avoir des problèmes de compatibilité, il faut choisir une version active ou en maintenance paire de Node.js pour tous les environnements (même de dév).
Gestion des erreurs
Les erreurs qui ne sont pas catchées avec un bloc try/catch termineront le processus. Ceci est fait pour éviter que l’application ne se trouve dans un état inconnu et qu’elle soit corrompue. Pour cela, il y a plusieurs choses à mettre en place :
- Utiliser un autre processus qui relancera l’app si elle est down : cela permet de repartir à un état connu de l’application et d’éviter trop de downtimes. Il y a beaucoup d'outils pour mettre ça en place : upstart, forever, pm2… Si votre application est hébergée par un fournisseur, il aura aussi des solutions à vous fournir.
- Monitorer l’application : cela permet de savoir quand l’application a eu des erreurs et surtout de trouver pourquoi. L’objectif étant ensuite de corriger ces erreurs pour éviter au maximum leur reproduction. Pour bien monitorer votre app, utilisez un logger (Winston, Loglevel, npmlog…) et récupérez via un outil (pm2, prometheus + grafana, appmetrics...) les performances de votre app. Cela vous permettra de pouvoir prévenir un potentiel downtime ou un crash.
Il existe un moyen de réagir aux erreurs non catchées : process.on(‘uncaughtException’). Il ne faut pas l’utiliser pour éviter que l’application s’arrête car elle se retrouverait dans un état corrompu. On peut en revanche l’utiliser pour nettoyer des ressources utilisées par l’application avant qu’elle ne se ferme et donc toujours le terminer avec process.exit(errorCode) s’il n’y a aucune opération asynchrone en cours ou avec process.exitCode = errorCode qui permet de ne pas forcer la fin du processus immédiatement.
Mono-thread
Coder avec Node, c’est faire du Javascript, et Javascript est mono-thread. Cela signifie que toute opération synchrone bloque le processus qui correspond à votre application. Notez bien que j’ai dit Javascript : Node.js propose des fonctions synchrone ou asynchrone (ex: fs.readFile et fs.readFileSync), mais les opérations faites en Javascript, comme une boucle for, un map, des additions et autres sont elles 100% synchrones.
Si vous voulez savoir si l’opération que vous faites est bloquante ou non, référez vous à la documentation Node officielle.
Si vous voulez mieux comprendre le côté bloquant ou non du Javascript, je vous conseille la vidéo de Philip Roberts sur l’event-loop, faite lors d’une JsConf.
Si vous bloquez le processus de votre application, elle ne réagira plus. Vous avez peut-être déjà vu des sites ou des applis qui ne réagissaient plus au clic. Bloquer votre processus principal aura le même effet.
Documentation Node : https://nodejs.org/api/
Vidéo event loop : https://www.youtube.com/watch?v=8aGhZQkoFbQ
Conséquences du mono-thread
Il faut donc éviter au maximum de bloquer le processus. Mais qu’est-ce qui peut bloquer le processus ? Par exemple :
- des boucles for ou des map() sur de très grandes arrays, surtout si on les enchaîne aussi avec des filter() par exemple. Il ne faut pas oublier que chacune de ces opérations est un parcours complet de l’array.
- des regex complexes. Par exemple, pour une vérification d’email : /^[a-zA-Z0-9][a-zA-Z0-9_\\.\\-\\+&]*@([a-zA-Z0-9]([a-zA-Z0-9]*[\\-]?[a-zA-Z0-9]+)*\\.)+[a-zA-Z]{2,10}$/ qui est de plus en plus longue à résoudre plus l’email est long.
Bref, tout ce qui est une opération complexe est à éviter. Vous pouvez utiliser performance.now() avant et après l’opération et faire la différence pour avoir le temps qu’elle prend en millisecondes.
Mais comment faire si on doit passer par une opération complexe même après avoir optimisé son code au maximum ?
Il y a quelques solutions :
- Utiliser d’autres programmes écrits dans d’autres langages qui sont multithreadés en les appelant dans notre programme Node. On peut par exemple utiliser le gzip ou le SSL du reverseProxy quand on écrit une API.
- Utiliser les Worker Threads de Node. Ils sont un moyen mis à notre disposition depuis la version 11.7.0 pour exécuter du code sur d’autres threads que le thread principal.
Disons, par exemple, que nous devons calculer un nombre de la suite de Fibonacci. Pour éviter de bloquer le processus principal, nous allons utiliser les Worker Threads :
index.js
<pre>
const {Worker} = require(“worker_threads”)
let num = 40
// Create new worker
const worker = new Worker(“./worker.js”, {workerData: {num: num}})
//Listen for a new message from worker
worker.once(“message”, result => {
console.log(`${num}th Fibonacci Number: ${result}`)
})
worker.on(”error”, result => {
console.log(error)
})
worker.on(“exit”, exitCode => {
console.log(exitCode)
})
console.log(“Executed in the parent thread”)
</pre>
worker.js
<pre>
const {parentPort, workerData} = require(“worker_threads”)
parentPort.postMessage(getFib(workerData.num))
function getFib(num) {
if (num === 0) {
return 0
}
else if (num === 1) {
return 1
}
else {
return getFib(num - 1) + getFib(num - 2)
}
}
</pre>
Ici, nous calculons le 40ème numéro de la suite de Fibonacci sans bloquer le processus principal et celui-ci sera affiché grâce au console.log() dans worker.once(“message”, …).
Pour le cas des APIs, vous pouvez aussi utiliser un Cluster qui crée des threads partageant les ports du serveur. Le processeur principal servira alors juste à rediriger les connexions utilisateurs vers le bon thread.
Cluster : https://nodejs.org/api/cluster.html#cluster
Worker threads : https://nodejs.org/api/worker_threads.html#worker-threads
Code : https://github.com/amatthieu/worker-thread-demo
Liste de bonnes pratiques :https://github.com/goldbergyoni/nodebestpractices