Introduction: Why is JavaScript Performance Important?
\nJavaScript (especially in the Node.js environment) has become the foundation of modern web development. The speed of code execution affects not only user experience but also server load. Slow applications lose their audience, and unoptimized requests can lead to costly capacity increases. In this article, we will cover basic yet effective performance optimization methods that are suitable even for beginners.
\nWe will look at three key areas: asynchronous programming, memory management, and profiling. All examples will be in Node.js.
\n\n1. Asynchrony: Avoiding Event Loop Blocking
\nNode.js operates in a single-threaded mode using the Event Loop. If you execute a synchronous heavy operation (e.g., reading a large file or a complex loop), the entire server "freezes" until it completes. To avoid this, use asynchronous methods.
\nBad Example (Blocking Code):
\nconst fs = require('fs');\nconst data = fs.readFileSync('/bigfile.txt', 'utf8'); // Blocks the thread\nconsole.log('File read');\n\nGood Example (Non-blocking):
\nconst fs = require('fs').promises;\nasync function readFile() {\n const data = await fs.readFile('/bigfile.txt', 'utf8');\n console.log('File read');\n}\nreadFile();\n\nUse async/await and promises for all I/O operations. This allows the Event Loop to handle other requests while waiting for a response from the database or file system.
Tip: Avoid Synchronous Methods in Production
\nMethods like fs.readFileSync or crypto.pbkdf2Sync are only acceptable during application startup. At runtime, use their asynchronous versions.
2. Optimizing Memory Management and Leaks
\nMemory leaks are a common cause of performance degradation in Node.js applications. They occur when objects remain in memory even though they are no longer needed.
\nMain Causes of Leaks:
\n- \n
- Global variables \n
- Uncleared timers (setInterval without clearInterval) \n
- Closed references in closures \n
- Event listeners that are not removed \n
Example of a Leak via Listeners:
\nconst EventEmitter = require('events');\nconst emitter = new EventEmitter();\n\nfunction processRequest() {\n emitter.on('data', (chunk) => {\n // Data processing\n });\n}\n// Each call to processRequest adds a new listener but does not remove the old one!\n\nSolution: Remove Listeners or Use once
\nemitter.once('data', (chunk) => {\n // Will execute only once and automatically be removed\n});\n\nIt is also useful to use utilities like heapdump for memory analysis. But for starters, it is enough to ensure that references to objects do not remain in the global scope.
3. Caching and Profiling
\nCaching helps avoid re-executing heavy computations. Profiling helps find bottlenecks in the code.
\nSimple Caching with Map:
\nconst cache = new Map();\n\nfunction expensiveCalculation(input) {\n if (cache.has(input)) {\n console.log('From cache');\n return cache.get(input);\n }\n const result = input * 1000 + Math.random(); // Simulating complex logic\n cache.set(input, result);\n return result;\n}\n\nconsole.log(expensiveCalculation(5)); // Compute\nconsole.log(expensiveCalculation(5)); // From cache\n\nFor more complex scenarios, use libraries like node-cache or Redis.
Profiling with Node.js Built-in Tool
\nRun the application with the --prof flag:
node --prof app.js\n\nAfter stopping the application, a file named isolate-*.log will appear in the folder. Convert it to a