In the past few years, deno and bun have been in full swing, and there is a trend that every update of deno and bun will be compared to Node, and the result of the comparison is always that Node lags behind.
This kind of comparison is not very familiar, just like a cell phone seller comparing with an iPhone, or a car seller comparing with a Tesla, and when comparing, sometimes you have to do the “thinner than a penny coin” routine.
Node isn’t behind anymore, but it did get a little stressed, so both versions 20 and 22 took a big leap forward and refused to be salty.
Because the Node website is so brief on version 22 features, I decided to come up with an article detailing the new features, so that those who are learning Node will know what tier Node is at now.
First of all, I’ll divide the new features into two categories: features that developers may use directly, and underlying updates that are relatively unnoticeable to developers. This article focuses on the former, and briefly introduces the latter. Let’s start with an overview:
Features that developers may use directly:
- Supports the introduction of ESM via
require()
- Run the script from
package.json
- Monitor mode (
--watch
) stabilization - Built-in WebSocket Client
- Default high water mark for increased flow
- File Pattern Matching Function
Developers are relatively unaware of the underlying updates:
V8 engine upgraded to version 12.4- The Maglev compiler is enabled by default
- Improved creation performance of
AbortSignal
The presentation begins next.
Support for importing ESMs via require()
Previously, we thought of CommonJS as separate from ESM.
For example, in CommonJS, we use and export modules with module.exports
and import modules with require()
:
// CommonJS
// math.js
function add(a, b) {
return a + b;
}
module.exports.add = add;
// useMath.js
const math = require('./math');
console.log(math.add(2, 3));
In ECMAScript Modules (ESM) ****, we use export
to export modules and import
to import them:
// ESM
// math.mjs
export function add(a, b) {
return a + b;
}
// useMath.js
import { add } from './math.mjs';
console.log(add(2, 3));
Node 22 supports a new way – importing ESMs with require()
:
// Node 22
// math.mjs
export function add(a, b) {
return a + b;
}
// useMath.js
const { add } = require('./mathModule.mjs');
console.log(add(2, 3));
The reason for this design is to provide a smooth transition for large projects and legacy systems, which can be difficult to migrate all the way to ESM quickly, and by allowing require()
to import into ESM, developers can migrate module-by-module instead of making changes to the entire project at once.
At present, this writing method is still an experimental feature, so there is a “threshold” for its use:
The start command requires the-experimental-require-module
parameter to be added, for example:node --experimental-require-module app.js
Module tagging: Ensure that the ESM module is available via"type": "module"
inpackage.json
or that the file extension is.mjs
.
Fully Synchronized: Only fully synchronized ESMs can be imported byrequire()
; any ESM containing the top-levelawait
cannot be loaded using this method.
Run the script from package.json
Suppose we have a script in package.json
:
"scripts": {
"test": "jest"
}
Before that, we have to rely on a package manager like npm or yanr to execute the commands, e.g. npm run test
.
Node 22 has added a new command line flag, --run
, which allows scripts defined in package.json
to be executed directly from the command line, and scripts can be run directly using commands like node --run test
.
At first I wondered if this was a pants-off-the-rocker move, because wherever there’s node, there’s usually npm, so what do I need node —run
for?
After thinking about it a bit, the main reason should still be to unify the runtime environment and improve performance. Different package managers may have small differences in how they handle scripts, and Node provides a standardized way of executing scripts, which helps to unify these behaviors; and executing scripts directly with node is faster than executing scripts through npm because it bypasses npm as an intermediate layer.
Monitor mode ( --watch
) stabilization
In version 19, Node introduced the —watch
directive to monitor the filesystem for changes and automatically reboot it. 22 makes this directive a stable feature.
To enable monitor mode, simply add the --watch
**** parameter when starting the Node application. Example:
node --watch app.js
For those of you who are using nodemon for auto-reboot, you can now switch to --watch
.
Built-in WebSocket Client
Previously, to develop a socket service with Node, you had to use third-party libraries like ws and socket.io. While third-party libraries have been helping developers for years, they’re still a bit inconvenient.
WebSocket is officially built into Node 22 and is a stable feature that no longer requires -experimental-websocket
to be enabled.
In addition to this, the WebScoket implementation follows the standards of the WebSocket API in browsers, which means that using WebSocket in Node will be very similar to using WebSocket in JavaScript, helping to reduce the cost of learning and improve code consistency.
Usage examples:
const socket = new WebSocket("ws://localhost:8080");
socket.addEventListener("open", (event) => {
socket.send("Hello Server!");
});
Increase the default High Water Mark for streams.
streams plays an important role in Node, it is necessary to read and write data to streams. The streams can set the highWaterMark
parameter to indicate the size of the buffer. highWaterMark
The bigger the size, the bigger the buffer is, the more memory it occupies, the less I/O operation it will do, the smaller highWaterMark
is, the other information is the opposite.
Usage is as follows:
const fs = require('fs');
const readStream = fs.createReadStream('example-large-file.txt', {
highWaterMark: 1024 * 1024
});
readStream.on('data', (chunk) => {
console.log(`Received chunk of size: ${chunk.length}`);
});
readStream.on('end', () => {
console.log('End of file has been reached.');
});
Although highWaterMark
is configurable, we usually use the default value. In previous versions, the default value of highWaterMark
was 16k, but since Node 22, the default value has been raised to 64k.
File pattern matching – glob and globSync
The Node 22 version adds the glob
and globSync
functions to the fs module, which are used to match file paths based on a specified pattern.
File pattern matching allows the developer to define a matching pattern to find a collection of file paths that match a specific rule. Pattern definitions typically include wildcards such as *
(which matches any character) and ?
(which matches a single character), as well as other pattern-specific characters.
glob function (asynchronous)
glob
function is an asynchronous function that does not block the Node.js event loop. This means that it does not stop the execution of other code while searching for files. glob
The basic usage of function is as follows:
const { glob } = require('fs');
glob('**/*.js', (err, files) => {
if (err) {
throw err;
}
console.log(files);
});
In this example, the glob
function is used to find files in all subdirectories ending with .js
. It accepts two arguments:
- The first argument is a string representing the file matching pattern.
The second argument is a callback function that will be called when the file search is complete. If the search is successful,err
will benull
andfiles
will contain an array with the paths of all matching files.
globSync function (synchronization)
globSync
is a synchronized version of glob
that blocks the event loop until all matching files have been found. This makes the code simpler, but can cause performance problems when dealing with large numbers of files or in applications that require high responsiveness. The basic usage is as follows:
const { globSync } = require('fs');
const files = globSync('**/*.js');
console.log(files);
This function returns an array of matched files directly and is suitable for scripts and simple applications where speed of execution is not a major concern.
Usage Scenarios
These two functions apply:
Automate the build process, such as automatically finding and working with JavaScript files in your project.
Development tools and scripts that require batch operations on files in the project directory.
Any application that needs to quickly sift through a large number of files to find a set that matches a specific pattern.
V8 engine upgraded to version 12.4
Starting with this section, we learn about the underlying updates that are relatively unnoticeable to developers, the first of which is that the V8 engine has been upgraded to version 12.4 with the following feature upgrades:
WebAssembly Garbage Collection: This feature will improve WebAssembly’s ability to manage memory.
Array.fromAsync: This new method allows the creation of arrays from asynchronous iterators.
Set Methods and Iterator Helper: Provides more built-in methods for Set operations and Iterator operations, enhancing the manipulation and flexibility of data structures.
The Maglev compiler is enabled by default
Maglev is a new compiler for V8, now enabled by default on supported architectures. It is optimized primarily for short lifecycle command line programs (CLI programs) performance, by improving the efficiency of JIT (compile on the fly). This will result in significant speed improvements for tools and scripts written by developers.
Improved creation performance of AbortSignal
In this update, Node has improved the efficiency of AbortSignal
instance creation. AbortSignal
is a mechanism used to interrupt an ongoing operation such as a network request or any long-running asynchronous task. By improving the efficiency of this process, it is possible to speed up any application that relies on this functionality, such as HTTP requests using fetch
or scenarios where interrupts are handled in the test runner.
AbortSignal
The way it works is managed through the AbortController
instance. AbortController
A signal
property and a abort()
method are provided. signal
The property returns a AbortSignal
object that can be passed to any API that accepts AbortSignal
(e.g. fetch
) to listen for cancel events. When the abort()
method is called, all operations associated with this controller will be canceled.
const controller = new AbortController();
const signal = controller.signal;
fetch(url, { signal })
.then(response => response.json())
.catch(err => {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Fetch error:', err);
}
});
controller.abort();