Node 22 is the new current!
Kevin Justal
CTO | Lead Full Stack Developer | Scrum Master | Tech Lead | Developer Consultant | CKA&CKS | Microsoft Azure & AWS Certified (x2)
Node.js version 22 was released in April 2024. This is now the new current.
I finally found time to catch up on the latest version and review each update in detail. While the new V8 engine is fascinating and adds many new functions, it's not the most exciting aspect. This release includes new features that will significantly impact my daily workflow. I have also created a git repository highlighting each feature. Let's dive into the details.
Support require()ing synchronous ESM graphs
Previously, it was impossible to require modules or use mjs modules in Node.js. For example, if we create the file test.mjs:
export const test = function () {
console.log("TEST");
};
And then require it in another file, such as index.js:
const { test } = require("./test.mjs");
test();
If you tried to run the previous code, you would have received this message:
With Node 22, it's now possible to run the following using the "--experimental-require-module" flag:
The same applies to modules. Create a package.json file for a module:
{
"type": "module",
"name": "test",
"version": "1.0.0",
"main": "index.js"
}
Then, in an index.js file located in the same folder as this package.json:
export const mtest = function () {
console.log("TEST");
};
You can now include this package as a dependency in your project and run it:
"module-test": "file:./module/module"
const { mtest } = require("module-test");
mtest();
Running package.json scripts
This is certainly the most impactful aspect of this release. I can't count the number of times I've run a script from a package.json. Instead of using the npm command:
npm run watch
Now, you can run the scripts from the package.json using the following command with the --run flag:
node --run watch
We can use Hyperfine, a command-line benchmarking tool, to compare the performance of both commands.
The difference in performance between the two commands is quite remarkable. Using the node command is 5.7 times faster than using the npm command.
Stream default High Water Mark
The high water mark is a parameter that sets a limit on the amount of data that can be stored in the internal stream buffer. When this limit is reached, Node stops reading more data until the buffer is emptied. This mechanism helps limit memory usage and prevents your application from crashing. It's important to note that this limit acts as a threshold rather than a hard cap! This parameter can be tricky to manage. If set too low, the program may slow down significantly when executing lengthy processes that require a lot of information from a stream. If set too high, you risk crashing your application with a Java heap limit exception. The default value of stream has been increased by default from 16kiB to 64KiB.
Let's create a function inspired by examples found in the Node documentation. This function will read a very long text file in chunks using a stream:
领英推荐
const fs = require("node:fs");
async function print(readable, highWaterMark) {
readable.setEncoding("utf8");
let data = "";
for await (const chunk of readable) {
data += chunk;
}
console.log(
`Everything has been read from the stream`
);
}
We will use the perf_hooks module to monitor performance and use getHeapStatistics to track memory heap usage.
async function print(readable, highWaterMark) {
const start = performance.now();
readable.setEncoding("utf8");
let data = "";
for await (const chunk of readable) {
data += chunk;
}
const end = performance.now();
const endMemory = getHeapStatistics().used_heap_size;
console.log(
`Everything has been read from the stream with ${highWaterMark} highWaterMark in ${
end - start
}ms with ${endMemory}`
);
}
We will run the function using both the previous default settings and the new version to compare their performance.
// Previous default => 16 * 1024
await print(
fs.createReadStream(`${__dirname}/file.txt`, {
highWaterMark: 16 * 1024,
}),
"16 * 1024"
).catch(console.error);
// Default => 64 * 1024
// With this value eveything is handle in one loop
await print(
fs.createReadStream(`${__dirname}/file.txt`, {
highWaterMark: 64 * 1024,
}),
"64 * 1024"
).catch(console.error);
As we can see, the new high water mark limit speeds up the process, but it also obviously consumes more memory.
Watch Mode
Finally, hot refresh is now officially part of Node.js. Previously experimental, it has been officially released in this version. To use it, simply include the --watch flag:
node --watch index.js
We will now officially say good bye to Nodemon.
Websocket Client
The new version of Node.js includes a built-in WebSocket client, eliminating the need for additional dependencies. To test this, we can create a WebSocket server server.js using the ws library:
const { WebSocketServer } = require("ws");
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", function connection(ws) {
ws.on("message", function message(data) {
console.log("received: %s", data);
});
ws.send("something");
});
And we can connect directly using a Node.js script without any additional dependencies. For example, let's create a file named client.js:
const socket = new WebSocket("ws://localhost:8080");
socket.addEventListener("open", () => {
socket.send("Hello Server!");
});
We can run both scripts:
node client.js
node server.js
You should see "Hello Server" in the console of server.js and "something" in the console of client.js.
Glob/GlobSync
Until now, I frequently used the npm package glob to list files in a directory according to a specific pattern. With this new release of Node, that dependency is no longer necessary. Glob and GlobSync have been implemented and can be imported from the fs module.
const { glob } = require("node:fs/promises");
const { globSync } = require("node:fs");
(async () => {
for await (const entry of glob("**/*.js")) console.log(entry);
})();
console.log(globSync("**/*.js"));
In the code above, I am listing all the JavaScript files within the project directory.
Improve performance of AbortSignal creation
For those unfamiliar with Node's AbortSignal API, this API allows you to interact with asynchronous operations. The performance of the AbortSignal with the fetch function has been improved in this release.
const mainFetchAborted = async () => {
const resp = await fetch("https://localhost:3000", {
signal: AbortSignal.timeout(2000),
});
const respBody = await resp.text();
console.log(respBody);
};
In the code above, if the call takes more than 2 seconds, it will time out and be aborted.
And that wraps up the highlights for Node.js 22!
Last Words
This release introduced many features that I now use almost daily. I'm now waiting October when this version reaches LTS status so I can use it in a production environment. I'm curious to see what Node 23 will bring. Let's wait and see!