Node-Js
Senior
Quiz-Nodejs-Senior
Nodejs-Streams
Worker-Threads
Nodejs-Performance
Scaling
Nodejs-Securite
Questions-Entretien-Nodejs-Senior
Qcm-Nodejs-Avance
Nodejs-Architecture
Event-Loop-Avance
Back-End
Javascript-Avance
📝
Entretien Node.js Senior — Performance, Streams et Scaling
20 questions avancées Node.js : architecture backend, event loop, streams, worker threads, sécurité, scaling horizontal et patterns professionnels. Idéal pour réussir un entretien Node.js Senior.
Banque de révision : les 25 questions corrigées
Voici l'intégralité des 25 questions de « Entretien Node.js Senior — Performance, Streams et Scaling », chacune avec sa bonne réponse et son explication. Le quiz interactif ci-dessus en tire une sélection aléatoire à chaque tentative — utilisez cette liste complète pour réviser.
Explication :
L'Event Loop de Node.js a 6 phases principales : timers (setTimeout/setInterval), pending callbacks (I/O), idle/prepare (interne), poll (récupère nouveaux événements I/O), check (setImmediate), close callbacks (socket.on("close")). Chaque phase a sa propre file d'attente de callbacks.
Entre chaque phase, Node.js exécute les microtasks (process.nextTick, Promises résolues). nextTick a priorité sur les Promises.
Explication :
process.nextTick() s'exécute immédiatement après la fin de l'opération en cours, avant même les Promises (microtasks), et avant la phase check. setImmediate() s'exécute dans la phase "check" de l'Event Loop, après les I/O du poll. setImmediate() est plus lent que nextTick().
Utilisez process.nextTick() pour des callbacks très prioritaires, mais attention à ne pas bloquer l'Event Loop avec des nextTick récursifs.
Explication :
Le module cluster permet de créer plusieurs processus enfants (workers) qui partagent le même port. Chaque worker a sa propre instance de l'Event Loop, utilisant un cœur CPU différent. Les workers ne partagent pas la mémoire (communication via IPC). Limites : gestion d'état partagé (sessions, cache) nécessite Redis ou similaire.
Utilisez PM2 pour gérer facilement le clustering :
pm2 start app.js -i max. Attention aux fuites mémoire qui tuent un worker à la fois.
Explication :
Libuv est la bibliothèque multiplateforme qui constitue le cœur de Node.js. Elle fournit l'Event Loop, le thread pool (4 threads par défaut pour les opérations bloquantes comme fs.readFile), la gestion des sockets, les timers, les signaux, et l'abstraction des différences système.
La taille du thread pool se configure avec UV_THREADPOOL_SIZE. Utile pour les applications très I/O intensives.
Explication :
Pour diagnostiquer une fuite mémoire : 1) utiliser
node --inspect et Chrome DevTools pour prendre des heap snapshots, 2) outils comme clinic.js (clinic heap), 3) memwatch-next pour détecter les leaks, 4) vérifier les closures qui retiennent des objets, les event listeners non supprimés, les caches sans éviction, les variables globales accidentelles.
Utilisez
process.memoryUsage() pour surveiller la mémoire. Les fuites fréquentes : les tableaux/caches qui grandissent sans limite, les callbacks dans des observables, les timers non nettoyés.
Explication :
Streams : Readable (ex: fs.createReadStream), Writable (fs.createWriteStream), Duplex (net.Socket - lecture+écriture indépendantes), Transform (zlib.createGzip - modifie les données en transit). Les streams sont essentiels pour traiter de gros volumes de données sans charger tout en mémoire.
Utilisez
stream.pipeline() (ou .pipe()) pour chaîner des streams. Préférez pipeline pour une meilleure gestion d'erreurs.
Explication :
Le backpressure (contre-pression) est le mécanisme automatique des streams pour éviter de surcharger la mémoire. Chaque stream a un
highWaterMark. Si le buffer interne dépasse, readable.pause() est appelé. Le writable.write() retourne false pour indiquer qu'il faut attendre l'événement drain.
Lorsque vous utilisez
.pipe(), le backpressure est géré automatiquement. Avec des streams personnalisés, implémentez _write() et _read() correctement.
Explication :
Cluster : multi-processus, chaque worker a sa propre mémoire, communication via IPC, idéal pour utiliser tous les cœurs CPU pour les requêtes HTTP. Worker Threads : threads légers dans le même processus, partagent la mémoire (avec SharedArrayBuffer, Atomics), plus efficaces pour les calculs CPU intensifs.
Utilisez Worker Threads pour le calcul lourd (image processing, crypto) ; utilisez Cluster pour les serveurs HTTP haute concurrence.
Explication :
V8 a un GC générationnel : New Space (Scavenger - rapide) pour les objets jeunes, Old Space (Mark-Compact - plus lent) pour les objets anciens. Flags utiles : --max-old-space-size (limite mémoire), --expose-gc (pour appeler global.gc()), --trace-gc (logs). Optimisations : réutiliser les objets plutôt que d'en créer, éviter les closures qui retiennent des objets, réduire les allocations dans les boucles.
Surveillez la mémoire avec
--trace-gc et --trace-gc-verbose. Pour les applications temps réel, réduisez la taille du New Space pour des GC plus fréquents mais plus courts.
Explication :
Le module
async_hooks permet de suivre le cycle de vie des ressources asynchrones. Utile pour : le tracing distribué (corréler des requêtes à travers les callbacks), les logs contextuels (propager un requestId), les ORM avec contexte automatique, la gestion de sessions sans passer l'objet partout.
Attention : async_hooks a un coût en performance (désactivé par défaut pour cette raison). Utilisez-le avec parcimonie ou en développement.
Explication :
Sécurisation : Helmet (en-têtes de sécurité), validation/sanitization des entrées (Joi, express-validator), rate limiting (express-rate-limit, slow down), CSRF (csurf), XSS (sanitize-html, échappement), injection SQL (utiliser ORM ou paramètres préparés), authentification forte (bcrypt, JWT sécurisé), logging des tentatives.
Activez aussi :
app.disable("x-powered-by"), utilisez HTTPS, gérez les CORS restrictifs, et faites des audits réguliers avec npm audit.
Explication :
Le module inspector (--inspect) permet d'utiliser Chrome DevTools sur une application Node.js. Fonctionnalités : breakpoints (y compris async), profiler CPU (flame graphs), heap snapshots (memory leaks), coverage (code non exécuté), post-mortem avec --inspect-brk, profiling de performance.
Pour une analyse en production :
node --inspect app.js et connectez-vous à chrome://inspect. Pour les conteneurs, exposez le port 9229.
Explication :
Le module
perf_hooks expose l'API Performance Web. performance.now() donne des timers haute résolution (microsecondes). performance.mark() et performance.measure() permettent de mesurer les temps entre deux marqueurs. perf_hooks.performance donne aussi accès aux métriques d'Event Loop.
Utilisez
perf_hooks pour mesurer des opérations critiques (temps de requête DB, temps de traitement). Combine avec async_hooks pour le tracing distribué.
Explication :
Bonnes pratiques : 1)
process.on("uncaughtException") et unhandledRejection pour capturer les erreurs fatales, logger, puis redémarrer (PM2). 2) Middleware d'erreur Express (4 paramètres). 3) Logs structurés (Winston, Pino) avec niveau, trace. 4) Fail fast : l'application ne doit pas continuer après une erreur fatale. 5) Utiliser des retry pour les opérations temporaires.
Ne jamais faire :
process.on("uncaughtException", () => {}) sans redémarrer. L'état de l'application est alors indéterminé.
Explication :
spawn : flux (streams), idéal pour les processus longs (sortie volumineuse). exec : buffer en mémoire, pour les commandes avec petite sortie. execFile : comme exec mais sans shell (plus sûr). fork : spawn spécial pour les scripts Node.js, crée un canal IPC pour communiquer.
Utilisez spawn pour les commandes avec gros volume de données, exec pour les commandes simples. Préférez execFile pour éviter les injections shell.
Explication :
L'Event Loop starvation se produit quand une opération synchrone longue bloque la boucle, empêchant les requêtes entrantes de répondre. Solutions : 1) déléguer le calcul CPU intensif aux Worker Threads, 2) découper en petits blocs avec
setImmediate() (yield), 3) utiliser des batchs avec setTimeout(,0) pour laisser la place aux I/O.
Exemple de découpage :
function processBatch(items, index, callback) { ... if (index < items.length) setImmediate(() => processBatch(items, index+10, callback)); }
Explication :
Quand on require() un module, Node.js exécute le code une fois et met en cache l'export. Les require() suivants retournent la même instance. Cela partage l'état. Pour des instances isolées, on peut exporter une fonction factory qui crée de nouvelles instances (
module.exports = () => new MonModule()) ou utiliser des classes.
Pour invalider le cache (dangereux) :
delete require.cache[require.resolve("./module")]. Utile pour le hot reload en développement uniquement.
Explication :
Quand il y a un cycle, Node.js retourne un objet partiel (ce qui a déjà été exporté) puis continue. Exemple : module A require B, module B require A → A reçoit un export partiel de B. Cela peut causer des
undefined si on accède à des propriétés qui ne sont pas encore définies. Solution : restructurer ou utiliser des imports paresseux (dans les fonctions).
Les imports ES avec
import sont analysés statiquement et détectent les cycles à la compilation, mais peuvent aussi avoir des problèmes.
Explication :
Pour HTTPS : lire les fichiers key et cert (ou un fichier pfx). Exemple :
const https = require("https"); const options = { key: fs.readFileSync("key.pem"), cert: fs.readFileSync("cert.pem") }; https.createServer(options, app).listen(443).
En production, utilisez un reverse proxy comme Nginx pour gérer TLS, ou Let's Encrypt avec certbot. Pour le développement, utilisez
mkcert pour générer des certificats locaux valides.
Explication :
Le module
vm permet d'exécuter du code JavaScript dans un contexte isolé (sandbox) avec des variables globales restreintes. Cas d'usage : exécution de plugins utilisateur, templates dynamiques, calculs sécurisés. Attention : vm n'est pas un sandbox parfait (des échappements sont possibles).
Pour un vrai sandbox, utilisez des conteneurs (Docker) ou des worker threads. Ne faites jamais confiance à vm seul pour exécuter du code non fiable.
Explication :
Les streams gèrent automatiquement le backpressure via le mécanisme de highWaterMark. Si le writable (HTTP) retourne false, le stream précédent (compression) arrête d'écrire, ce qui remonte jusqu'à la source (fichier) qui pause sa lecture.
stream.pipeline() gère tout cela et aussi les erreurs.
Exemple :
stream.pipeline(fs.createReadStream("big.txt"), zlib.createGzip(), res, (err) => { if (err) console.error(err); });
Explication :
Le module
diagnostics_channel permet de créer des canaux nommés. Les bibliothèques peuvent publier des événements (channel.publish(data)) et les outils de monitoring peuvent s'y abonner (channel.subscribe(callback)) sans modifier le code source. Idéal pour l'instrumentation et le tracing.
Moins intrusif que async_hooks pour le tracing. Exemple :
const channel = diagnostics_channel.channel("http.request"); channel.subscribe((data) => { console.log(data.url); });
Explication :
Stratégies : 1) réduire le bundle (esbuild, Webpack), 2) déplacer le code d'initialisation hors de la handler (outside handler = une fois par conteneur), 3) provisioned concurrency (AWS) pour garder des lambdas "chaudes", 4) Lambda SnapStart (AWS) pour sauvegarder/restaurer l'état mémoire, 5) minimiser les dépendances, 6) utiliser des langages plus rapides pour le cold start (Rust, Go).
Utilisez des outils comme
@aws-lambda-powertools pour logger et tracer, et mesurez le cold start avec des métriques CloudWatch.
Explication :
Processus : 1) lancer
node --inspect app.js. 2) Ouvrir chrome://inspect. 3) Prendre un premier snapshot (baseline). 4) Faire des actions qui devraient libérer de la mémoire. 5) Prendre un second snapshot. 6) Mode "Comparison" pour voir les objets ajoutés ou non libérés. 7) Identifier les "retainers" (pourquoi l'objet est retenu).
Pour les applications en production, utilisez
node --inspect app.js (attention à la performance). Pour les environnements conteneurisés, exposez le port 9229.
Explication :
module.createRequire(import.meta.url) permet de créer une fonction require dans un module ES. Utile pour : charger des fichiers JSON (qui n'ont pas d'import standard), importer des modules CommonJS depuis ESM, ou charger des fichiers depuis des chemins dynamiques.
Exemple :
import { createRequire } from "module"; const require = createRequire(import.meta.url); const data = require("./data.json");