Node.js vs Browser difference
Browser:
Interactive Web Applications
DOM, Window…
Need to support older versions of browsers
ES modules
Node:
Server applications
File system APIs, networking, cryptography…
Single version of Node on the server
CommonJS + ES modules
REPL
Read Eval Print Loop
Debugging mode
To enter it, write “node”.
Global variables
global
console
performance
Buffer
AbortController
queueMicrotask
WebAssembly
setTimeout
setInterval
setImmediate
clearTimeout
clearInterval
clearImmediate
URL
URLSearchParams
MessageChannel
MessageEvent
MessagePort
Event
EventTarget
TextDecoder
TextEncoder
Modules variables
__dirname
__filename
exports - это алиас для module.exports
module
require()
С10K problem
In multi-threaded servers, for example on Java, 1 thread (1 thread served 1 request to the server) used to take up approximately 1 MB, 10,000 threads - 10 GB of memory
This problem existed before, but now there are difficulties with processing, for example, a million requests, since the server must have 1 TB of RAM for this
How many threads in Node.js?
1 thread and a huge thread pool
Non blocking I/O
The main thread is not blocked by input and output
The server will continue to serve requests
We are working with asynchronous code
It’s because of libuv
Event Loop in Node.js
1) First, a request is made
2) Starts to be processed by our V8
3) Passes through Node bindings and turns into C/C++ if necessary
4) Then gets into the Event Queue if it is an asynchronous operation
5) If the code is synchronous, it goes straight to the Callstack
6) Asynchronous operations that got into the Callstack, if they block the thread, then they go to the Worker threads, and from there back to the event queue, but only as a callback
PHASES:
0) Synchronous code is executed
1) timers - looks if there are timers that should complete
2) pending callbacks - callbacks from system operations
3) idle, prepare - internal phase of libuv implementation, we cannot influence it in any way
4) poll - processing i/o events (reading a file, writing, etc.), does not read all requests, but only for some time, and then again returns to this point in a cycle
5) check - here only setImmediate
6) close callbacks - all close callbacks will be processed, for example, the connection with sockets was interrupted.
7) exit check
!!! Between each phase, promises are called, and process.nextTick()
setImmediate
Schedules a callback function to be executed after the current phase of the event loop, specifically after I/O events have been processed. It’s used for asynchronous execution and is generally faster than setTimeout() with a delay of 0 when used within the same context according to a Node.js blog post.
How to measure performance in Node?
1) If you need to measure a piece of code:
First, we put marks inside the function or code:
performance.mark(“start”)
performance.mark(“end”)
Then we put a measurement mark
performance.measure(“some name”, “start”, “end”)
After that, we import the performance hook:
const perf_hooks = require(‘perf_hooks’);
Create a performance observer from the hook:
const performanceObserver = new perf_hooks.PerformanceObserver((items, observer) => {
const entry = items.getEntriesByName(“name of our measurement mark”)[0];
console.log(777, “entry: “, entry);
observer.disconnect();
})
Call the observer:
performanceObserver.observe({ entryTypes: [“measure”] });
2) If we need to measure the whole function, we can do without placing labels, and do it like this:
Wrap our function in timerify
slow = perf_hooks.performance.timerify(slow);
Call the observer with entryType: function
performanceObserver.observe({ entryTypes: [“function”] });
worker_threads
Worker Threads is an analogue of WebWorker from the browser
1) For the worker, you need to create a separate file
2) Import const { parentPort, workerData } = require(“worker_threads”)
3) parentPort.postMessage(compute(workerData)) - compute is our random function
4) In the file where we use the worker, import:
const { Worker } = require(“worker_threads”)
5) And do something like this:
const compute = (array) => {
return new Promise((resolve, reject) => {
const worker = new Worker(“./worker.js”, {
workerData: {array}
})
worker.on(“message”, (msg) => resolve(msg))
worker.on(“error”, (err) => reject(err.name))
worker.on(“exit”, () => console.log(“end”))
})
}
Important: It is a bad idea to use a new worker for each new request, this way you can get zadosed
spawn
When to use:
When you need to run a command that may run for a long time and produce a lot of output.
How it works:
spawn runs a command and lets you read its output as it runs. This is very useful for working with large amounts of data or long-running processes, since it does not limit the amount of output.
Example:
When you want to run a command and process its output line by line, like ping or tail -f.
const { spawn } = require(‘child_process’);
const child = spawn(‘ls’, [‘-lh’, ‘/usr’]); // Run the command ‘ls -lh /usr’
child.stdout.on(‘data’, (data) => {
console.log(stdout: ${data});
});
child.stderr.on(‘data’, (data) => {
console.error(stderr: ${data});
});
child.on(‘close’, (code) => {
console.log(child process exited with code ${code});
});
exec
When to use:
When you need to run a command and get all of its output at once, after it has finished.
How it works:
exec runs a command and stores all of its output (stdout and stderr) in a buffer. After the command has finished, you get all of the output at once. This is useful for short commands with a small amount of output.
Example:
When you need to run a command and get the entire output, such as cat to read a small file or ls to list a short file.
const { exec } = require(‘child_process’);
exec(‘ls -lh /usr’, (error, stdout, stderr) => {
if (error) {
console.error(exec error: ${error});
return;
}
console.log(stdout: ${stdout});
console.error(stderr: ${stderr});
});
fork
Creates a separate process
The main differences and advantages of fork over spawn and exec:
IPC (Inter-Process Communication):
fork automatically establishes a channel for inter-process communication (IPC) between the parent and child processes. This simplifies the exchange of messages.
spawn can also be used to create child processes, but setting up an IPC channel and exchanging messages requires additional effort and code.
Optimization for Node.js:
fork is specifically optimized for starting new Node.js processes. It automatically inherits parent environment variables and allows easy management of Node.js script execution.
spawn is more versatile and is used to run any commands or scripts. Although it can start Node.js processes, fork does this more efficiently and with less effort for the developer.
What is the difference between fork, spawn and exec from worker_threads?
It is always better to use woker, as it is a modern approach. Previously, Node only had fork. The performance of woker when transferring large files is much higher. All new libraries are rewritten from fork to worker threads
fork, spawn and exec:
Create separate processes with full isolation.
Used to execute external commands and scripts, for example, written not in JS.
Processes do not share memory, which makes them more secure, but less efficient in data exchange.
worker_threads:
Create threads within a single process.
Used for parallel computing in JavaScript with fast data exchange.
Threads share memory, which allows for efficient and fast data exchange, but requires caution to prevent problems with access to shared memory.