Node.js 21 : Une mise à jour révolutionnaire
Node.js 21 marque une étape importante avec des améliorations de performance significatives, de nouvelles APIs de sécurité et une meilleure intégration des standards web.
1. Nouvelles APIs de Sécurité
Node.js 21 introduit des APIs de sécurité modernes :
// Web Crypto API native
import { webcrypto } from 'node:crypto';
async function securePasswordHashing(password, salt) {
const encoder = new TextEncoder();
const data = encoder.encode(password + salt);
// Utilisation de SubtleCrypto
const hashBuffer = await webcrypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
// Génération sécurisée de clés
async function generateSecureKey() {
return await webcrypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256,
},
true, // extractable
['encrypt', 'decrypt']
);
}
// Chiffrement/déchiffrement moderne
class SecureStorage {
constructor() {
this.key = null;
}
async initialize() {
this.key = await generateSecureKey();
}
async encrypt(data) {
const encoder = new TextEncoder();
const encodedData = encoder.encode(data);
const iv = webcrypto.getRandomValues(new Uint8Array(12));
const encrypted = await webcrypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
this.key,
encodedData
);
return {
encrypted: Array.from(new Uint8Array(encrypted)),
iv: Array.from(iv)
};
}
async decrypt(encryptedData, iv) {
const decrypted = await webcrypto.subtle.decrypt(
{ name: 'AES-GCM', iv: new Uint8Array(iv) },
this.key,
new Uint8Array(encryptedData)
);
const decoder = new TextDecoder();
return decoder.decode(decrypted);
}
}
2. Performance des WebStreams
import { Readable, Writable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
// WebStreams natifs avec Node.js 21
class HighPerformanceProcessor extends TransformStream {
constructor(options = {}) {
const { batchSize = 1000, concurrency = 4 } = options;
super({
transform(chunk, controller) {
// Traitement optimisé des chunks
this.processChunk(chunk, controller);
},
flush(controller) {
// Finalisation du traitement
this.finalize(controller);
}
});
this.batchSize = batchSize;
this.concurrency = concurrency;
this.buffer = [];
}
async processChunk(chunk, controller) {
this.buffer.push(chunk);
if (this.buffer.length >= this.batchSize) {
const batch = this.buffer.splice(0, this.batchSize);
const processed = await this.processBatch(batch);
controller.enqueue(processed);
}
}
async processBatch(batch) {
// Traitement parallèle avec limite de concurrence
const semaphore = new Semaphore(this.concurrency);
const promises = batch.map(async (item) => {
await semaphore.acquire();
try {
return await this.processItem(item);
} finally {
semaphore.release();
}
});
return await Promise.all(promises);
}
async processItem(item) {
// Logique métier intensive
return item.toString().toUpperCase();
}
}
// Utilisation avec des fichiers volumineux
async function processLargeFile(inputPath, outputPath) {
const readStream = new ReadableStream({
start(controller) {
const fileStream = fs.createReadStream(inputPath);
fileStream.on('data', chunk => controller.enqueue(chunk));
fileStream.on('end', () => controller.close());
fileStream.on('error', err => controller.error(err));
}
});
const processor = new HighPerformanceProcessor({
batchSize: 5000,
concurrency: 8
});
const writeStream = new WritableStream({
write(chunk) {
return fs.promises.appendFile(outputPath, chunk);
}
});
await readStream
.pipeThrough(processor)
.pipeTo(writeStream);
}
3. Améliorations du Watch Mode
// node --watch --watch-path=./src app.js
// Nouvelle API pour le watch mode programmatique
import { watch } from 'node:fs/promises';
import { spawn } from 'node:child_process';
class DevelopmentWatcher {
constructor(config) {
this.config = {
paths: ['./src', './config'],
ignore: [/node_modules/, /\.git/, /\.tmp/],
commands: {
js: 'node --check',
ts: 'tsc --noEmit',
json: 'node -e "JSON.parse(require(\"fs\").readFileSync(process.argv[1]))"'
},
debounce: 100,
...config
};
this.timeouts = new Map();
}
async start() {
console.log('🔍 Starting development watcher...');
for (const path of this.config.paths) {
this.watchPath(path);
}
}
async watchPath(path) {
try {
const watcher = watch(path, { recursive: true });
for await (const event of watcher) {
if (this.shouldIgnore(event.filename)) continue;
this.debounceAction(event.filename, () => {
this.handleFileChange(event);
});
}
} catch (error) {
console.error(`Error watching ${path}:`, error);
}
}
shouldIgnore(filename) {
return this.config.ignore.some(pattern =>
pattern.test ? pattern.test(filename) : filename.includes(pattern)
);
}
debounceAction(key, action) {
if (this.timeouts.has(key)) {
clearTimeout(this.timeouts.get(key));
}
this.timeouts.set(key, setTimeout(() => {
action();
this.timeouts.delete(key);
}, this.config.debounce));
}
async handleFileChange(event) {
const { eventType, filename } = event;
console.log(`📝 ${eventType}: ${filename}`);
// Validation automatique basée sur l'extension
const ext = filename.split('.').pop();
const command = this.config.commands[ext];
if (command) {
await this.runCommand(command, filename);
}
// Hot reload pour les serveurs Express
if (filename.endsWith('.js') && this.server) {
await this.restartServer();
}
}
async runCommand(command, filename) {
return new Promise((resolve, reject) => {
const fullCommand = `${command} ${filename}`;
const child = spawn('bash', ['-c', fullCommand]);
child.stdout.on('data', data => {
process.stdout.write(`✅ ${data}`);
});
child.stderr.on('data', data => {
process.stderr.write(`❌ ${data}`);
});
child.on('close', code => {
code === 0 ? resolve() : reject(new Error(`Command failed with code ${code}`));
});
});
}
async restartServer() {
if (this.server) {
console.log('🔄 Restarting server...');
this.server.kill();
}
this.server = spawn('node', ['app.js'], {
stdio: 'inherit'
});
}
}
// Usage
const watcher = new DevelopmentWatcher({
paths: ['./src', './routes', './middleware'],
ignore: [/\.test\.js$/, /\.spec\.js$/],
debounce: 200
});
watcher.start();
4. Nouvelles APIs de Test Intégrées
// Node.js 21 inclut un test runner natif amélioré
import { test, describe, it, before, after, beforeEach, afterEach } from 'node:test';
import { strict as assert } from 'node:assert';
describe('API Tests', () => {
let server;
before(async () => {
// Setup global avant tous les tests
server = await startTestServer();
});
after(async () => {
// Cleanup global après tous les tests
await server.close();
});
describe('User Management', () => {
let userId;
beforeEach(async () => {
// Setup avant chaque test
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Test User',
email: 'test@example.com'
})
});
const user = await response.json();
userId = user.id;
});
afterEach(async () => {
// Cleanup après chaque test
if (userId) {
await fetch(`/api/users/${userId}`, { method: 'DELETE' });
}
});
it('should create user successfully', async (t) => {
// Test avec sous-tests
await t.test('validates input data', () => {
assert.ok(userId, 'User ID should be generated');
});
await t.test('returns proper response format', async () => {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
assert.equal(user.name, 'Test User');
assert.equal(user.email, 'test@example.com');
assert.ok(user.createdAt);
});
});
it('should handle concurrent requests', async (t) => {
const concurrency = 10;
const promises = Array.from({ length: concurrency }, async (_, i) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: `Concurrent User ${i}`,
email: `user${i}@example.com`
})
});
return response.json();
});
const users = await Promise.all(promises);
assert.equal(users.length, concurrency);
// Cleanup
await Promise.all(users.map(user =>
fetch(`/api/users/${user.id}`, { method: 'DELETE' })
));
});
it('should benchmark performance', async (t) => {
const iterations = 1000;
const start = performance.now();
for (let i = 0; i < iterations; i++) {
await fetch(`/api/users/${userId}`);
}
const duration = performance.now() - start;
const avgResponseTime = duration / iterations;
console.log(`Average response time: ${avgResponseTime.toFixed(2)}ms`);
assert.ok(avgResponseTime < 50, 'Response time should be under 50ms');
});
});
describe('Error Handling', () => {
it('should handle invalid requests gracefully', async () => {
const response = await fetch('/api/users/invalid-id');
assert.equal(response.status, 404);
const error = await response.json();
assert.ok(error.message);
assert.equal(error.code, 'USER_NOT_FOUND');
});
it('should validate input properly', async () => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: '', // Invalid empty name
email: 'invalid-email' // Invalid email format
})
});
assert.equal(response.status, 400);
const error = await response.json();
assert.ok(Array.isArray(error.errors));
assert.ok(error.errors.length > 0);
});
});
});
// Exécution avec options avancées
// node --test --test-reporter=spec --test-concurrency=4 test/
5. Optimisations Memory et Garbage Collection
// Node.js 21 améliore la gestion mémoire
import { performance, PerformanceObserver } from 'node:perf_hooks';
class MemoryOptimizedProcessor {
constructor() {
this.setupPerformanceMonitoring();
this.memoryThreshold = 100 * 1024 * 1024; // 100MB
}
setupPerformanceMonitoring() {
const obs = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.entryType === 'gc') {
console.log(`GC: ${entry.kind} - Duration: ${entry.duration}ms`);
if (entry.duration > 100) {
console.warn('Long GC detected, optimizing...');
this.optimizeMemory();
}
}
});
});
obs.observe({ entryTypes: ['gc', 'measure'] });
}
optimizeMemory() {
// Force garbage collection (en développement uniquement)
if (global.gc && process.env.NODE_ENV === 'development') {
const before = process.memoryUsage();
global.gc();
const after = process.memoryUsage();
console.log(`Memory freed: ${((before.heapUsed - after.heapUsed) / 1024 / 1024).toFixed(2)}MB`);
}
}
async processLargeDataset(dataset) {
const batchSize = 1000;
const results = [];
for (let i = 0; i < dataset.length; i += batchSize) {
const batch = dataset.slice(i, i + batchSize);
// Traitement par batch pour éviter les pics mémoire
const batchResults = await Promise.all(
batch.map(item => this.processItem(item))
);
results.push(...batchResults);
// Vérification périodique de la mémoire
if (i % (batchSize * 10) === 0) {
const memUsage = process.memoryUsage();
if (memUsage.heapUsed > this.memoryThreshold) {
console.log('Memory threshold exceeded, pausing...');
// Pause pour permettre le GC
await new Promise(resolve => setTimeout(resolve, 100));
this.optimizeMemory();
}
}
}
return results;
}
async processItem(item) {
// Simulation de traitement intensif
return {
id: item.id,
processed: true,
timestamp: Date.now(),
data: item.data?.slice(0, 100) // Limitation de la taille des données
};
}
}
// Usage avec monitoring
async function runOptimizedProcess() {
const processor = new MemoryOptimizedProcessor();
// Génération de données de test
const largeDataset = Array.from({ length: 50000 }, (_, i) => ({
id: i,
data: 'x'.repeat(1000) // 1KB par item
}));
console.log('Starting optimized processing...');
const results = await processor.processLargeDataset(largeDataset);
console.log(`Processed ${results.length} items`);
}
Migration vers Node.js 21
6. Guide de Migration
// package.json updates
{
"engines": {
"node": ">=21.0.0"
},
"type": "module", // ESM par défaut recommandé
"scripts": {
"dev": "node --watch --env-file=.env app.js",
"test": "node --test --test-reporter=spec",
"start": "node --env-file=.env.production app.js"
}
}
// Configuration ESM optimisée
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const require = createRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Chargement conditionnel de modules
const config = process.env.NODE_ENV === 'production'
? await import('./config/production.js')
: await import('./config/development.js');
// Utilisation des nouvelles APIs
export default class ModernApp {
constructor() {
this.setupErrorHandling();
this.setupPerformanceMonitoring();
}
setupErrorHandling() {
// Gestion d'erreur améliorée Node.js 21
process.on('uncaughtException', (error, origin) => {
console.error(`Uncaught exception: ${error}\nOrigin: ${origin}`);
this.gracefulShutdown();
});
process.on('unhandledRejection', (reason, promise) => {
console.error(`Unhandled rejection at: ${promise}\nReason: ${reason}`);
});
process.on('warning', (warning) => {
console.warn(`Warning: ${warning.name} - ${warning.message}`);
});
}
async gracefulShutdown() {
console.log('Starting graceful shutdown...');
// Fermeture des connexions
await this.closeConnections();
// Attente des tâches en cours
await this.waitForPendingTasks();
process.exit(0);
}
}
Bonnes Pratiques Node.js 21
- WebStreams : Préférez les WebStreams aux streams traditionnels
- Web Crypto API : Utilisez pour toutes les opérations cryptographiques
- Test runner natif : Remplacez Jest/Mocha pour les projets simples
- Watch mode : Intégrez le watch mode natif dans vos workflows
- ESM first : Adoptez ESM comme standard
Conclusion
Node.js 21 représente une évolution majeure vers les standards web modernes, avec des améliorations significatives de performance et de sécurité. Cette version pose les bases d'un écosystème Node.js plus moderne et efficace.