別にしんどくないブログ

技術のことや読書メモを書いています

Node.js v22の主な変更点

nodejs logo
引用元: https://nodejs.org/en/about/branding

2024年4月24日にリリースされたNode.js v22の主な変更点を紹介します。

Node.js v22はLTS(長期サポート)のバージョンになります。10月の後半にLTSとしてサポートが始まります。

nodejs.org

記事内のサンプルコードはすべて以下のリポジトリにあります。

github.com

require()がESMをサポート

github.com

これまでできなかったCommonJS(CJS)のファイル内でrequire()を使ったESModule(ESM)の読み込みが可能になりました。
ただし、Top-level Awaitを含むファイルは読み込めない点に注意してください。
また、まだ実験的な機能のため実行時に--experimental-require-moduleフラグが必要になります。

node --experimental-require-module require.cjs

たとえば、以下のようなESMのファイルがあります。

// esm.js
export const foo = () => {
  return "bar";
};

そのESMのファイルをrequire()を使って読み込むことができます。

// require.cjs
const { foo } = require("./esm.js");

console.log(foo());
// bar

そもそもなぜ今までrequire()を使ってESMが読み込めなかったのかは以下のJoyeeの記事にまとめられています。

joyeecheung.github.io

Top-level Awaitを含む場合のみ非同期にしなければいけないのですが、そもそも仕様ではESMは無条件で非同期である必要がないとのことです。V8のコードを読んでいたときに発見したそうです。
そこで同期的に動くCJSでもTop-level Awaitを含まないESMファイルであればrequire()でも読み込めるように修正しました。
これまでrequire()でESMを読み込めなかった理由としては、コミュニティ内での議論が長引いたからとも書かれています。

まだ実験的な機能ですが、これが普通に使えるようになるとESM Onlyで配布しているnpm packageをCJSな環境でも読み込むことができるようになるかもしれません。Top-level Awaitさえ使われていなければという制約はあるもののNode.jsエコシステムの大きな前進ではないでしょうか。

V8 12.4 アップデートによる JavaScript の機能

github.com

JavaScriptエンジンであるV8が12.4にアップデートしました。
それにより新しいJavaScriptの機能が追加されています。
1つずつ見ていきましょう。

Array.fromAsync()

developer.mozilla.org

Array.fromAsyncはAsync IteratorやItarableなオブジェクトから新しい配列を作る関数です。
次のように非同期なGenerator関数の実行が終わったあとの結果の配列を作成します。Generator関数の結果を即時で配列にするわけではなく、実行結果を配列にするので以下のコードの例だとsleep(5000)の処理が10回実行されてはじめて配列の結果を得ることができます。

import { setTimeout as sleep } from "node:timers/promises";

async function* asyncIterable() {
  for (let i = 0; i < 10; i++) {
    await sleep(5000);
    yield i;
  }
}

const arr = await Array.fromAsync(asyncIterable());
console.log(arr);
/**
[
  0, 1, 2, 3, 4,
  5, 6, 7, 8, 9
]
*/

第二引数にmap関数を渡すことも可能です。以下は配列の値を2倍にするmap関数を指定する例です。

const arr = await Array.fromAsync(asyncIterable(), (val) => val * 2);
console.log(arr);
/**
[
   0,  2,  4,  6,  8,
  10, 12, 14, 16, 18
]
 */

Set methods

tc39.es

Setに新しいメソッドが増えました。

union()

developer.mozilla.org

union()Sebオブジェクトに別のSetオブジェクトの値を結合した新しいSebオブジェクトを作る関数です。 上述のリンク先のMDNの記事に掲載しているベン図が示すとおり和集合(OR)の結果を返します。

引用元: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/union

以下のように2つのSebオブジェクトの値が結合されています。

const a = new Set([1, 2, 3, 4, 5]);
const b = new Set([5, 6, 7, 8, 9]);

const c = a.union(b);
console.log(c);
// Set(9) { 1, 2, 3, 4, 5, 6, 7, 8, 9 }

intersection()

developer.mozilla.org

intersection()Sebオブジェクトと別のSetオブジェクトの重複する値だけを持つ新しいSebオブジェクトを作る関数です。 上述のリンク先のMDNの記事に掲載しているベン図示すとおり積集合(AND)の結果を返します。

引用元: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/intersection

const a = new Set([1, 2, 3, 4, 5]);
const b = new Set([2, 4, 6, 8]);

const c = a.intersection(b);
console.log(c);
// Set { 2, 4 }

difference()

developer.mozilla.org

difference()Sebオブジェクトに対して別のSetオブジェクトには含まれていない値のみを抽出して新しいSebオブジェクトを作る関数です。 上述のリンク先のMDNの記事に掲載しているベン図示すとおり対象差集合の結果を返します。

引用元: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/difference

以下のサンプルコードではabを比較しaだけが持つ値を抽出しています。

const a = new Set([1, 2, 3, 4, 5]);
const b = new Set([2, 4, 6, 8]);

const c = a.difference(b);
console.log(c);
// Set(3) { 1, 3, 5 }

symmetricDifference()

developer.mozilla.org

symmetricDifference()Sebオブジェクトと別のSetオブジェクトで重複しない値のみを抽出して新しいSetオブジェクトを作成する関数です。 上述のリンク先のMDNの記事に掲載しているベン図示すとおり対象差集合の結果を返します。

引用元: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference

以下のサンプルコードではabのどちらかだけが持つ値を抽出しています。

const a = new Set([1, 2, 3, 4, 5]);
const b = new Set([2, 4, 6, 8]);

const c = a.symmetricDifference(b);
console.log(c);
// Set(5) { 1, 3, 5, 6, 8 }

isSubsetOf()

developer.mozilla.org

isSubsetOf()Sebオブジェクトの値が別のSetオブジェクトに含まれているかどうかを判定する関数です。 上述のリンク先のMDNの記事に掲載しているベン図示すとおり包含関係にあるかどうかを判定します。

引用元: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/isSubsetOf

以下のサンプルコード以下のとおりです。

const a = new Set([2, 4, 6, 8]);
const b = new Set([0, 2, 4, 6, 8]);
const c = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9]);

console.log(a.isSubsetOf(c));
// true
console.log(b.isSubsetOf(c));
// false

isSupersetOf()

developer.mozilla.org

isSupersetOf()Sebオブジェクトの値が別のSetオブジェクトを含んでいるかどうかを判定します。先述のisSubsetOf()とは反対の関係性になります。
上述のリンク先のMDNの記事に掲載しているベン図示すとおり包含関係にあるかどうかを判定します。

引用元: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/isSupersetOf

以下のサンプルコード以下のとおりです。

const a = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9]);
const b = new Set([2, 4, 6, 8]);
const c = new Set([0, 2, 4, 6, 8]);

console.log(a.isSupersetOf(b));
// true
console.log(a.isSupersetOf(c));
// false

isDisjointFrom()

developer.mozilla.org

isDisjointFrom()は2つのSetオブジェクトの値に重複が1つもないか判定する関数です。
上述のリンク先のMDNの記事に掲載しているベン図示すとおり包含関係にあるかどうかを判定します。

引用元: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/isSupersetOf

以下のサンプルコード以下のとおりです。abは同じ値がないためtrueとなりますが、acは同じ値5を持つためfalseとなります。

const a = new Set([1, 2, 3, 4, 5]);
const b = new Set([6, 7, 8, 9]);
const c = new Set([5, 6, 7, 8, 9]);

console.log(a.isDisjointFrom(b));
// true
console.log(a.isDisjointFrom(c));
// false

Iterator Helpers

Iteratorに便利な関数が増えました。

tc39.es

2021年のtc39_studyのときから首を長くして待っていました。

各関数の紹介のためのサンプルコードには以下のGenerator関数を使います。

function* generator() {
  yield 1;
  yield 2;
  yield 3;
}

関数の数が多いのですべて紹介しませんが、いくつかピックアップして紹介します。他にも便利な関数がたくさんあるのでMDNなどで調べて試してみてください。

map()

developer.mozilla.org

map()は以下のようにArray.prototype.mapのように新しいIteratorオブジェクトを作成します。 以下の例ではmap()で値を2倍にしているので、yield 1;の結果が2になり、yield 2;の結果が4になり、yield 3;の結果が6になります。

const result = generator().map((i) => i * 2);
console.log(result.next());
// { value: 2, done: false }
console.log(result.next());
// { value: 4, done: false }
console.log(result.next());
// { value: 6, done: false }
console.log(result.next());
// { value: undefined, done: true }

filter()

developer.mozilla.org

filter()は以下のようにArray.prototype.filterのようにフィルタリング処理を加えた結果の新しいIteratorオブジェクトを作成します。 以下の例ではfilter()で奇数の値だけを抽出しています。

const result = generator().reduce((a, b) => a + b);
console.log(result);
// 6

reduce()

developer.mozilla.org

reduce()は以下のようにArray.prototype.reduceのようにコールバック関数の処理をもとに要素を単一の値に縮小にする関数です。 以下の例ではreduce()で合計値を算出しています。

const result = generator().reduce((a, b) => a + b);
console.log(result);
// 6

--runオプションによるpackage.jsonのscripts実行

github.com

npm runのように、nodeでも--runオプションをつけることでpackage.jsonscriptsに定義したコマンドを実行できるようになりました。

たとえば、以下のようにtestを実行できるようにpackage.jsonに定義されていたとします。

{
  "scripts": {
    "lint": "eslint **/*.js"
  }
}

これを以下のコマンドで実行できます。

node --run lint

watchモード(--watch)が安定版へ

github.com

--watchはNode.js v19から実験的に実装されていました。それがv22で安定版へと昇格しました。--watchについてはv19のときに紹介したので、ここでは簡単に紹介します。

shisama.hatenablog.com

実行時に以下のように--watchフラグを付与するとwatchモードとして起動します。

node index.js --watch

watchモードは実行中のプロセスを監視し、コードが変更されると自動的にプロセスを再起動します。

たとえば、以下のようにindex.jsがエントリーポイントとして実行されていたとします。index.jsとその依存関係にあるModule A, B, Cのいずれかが変更されると自動的にプロセスが再起動します。

モジュールの依存関係

ローカルで開発しているときにコードの修正後にわざわざコマンドを再実行しなくても良くなるので開発体験が良くなります。

WebSocketが安定版へ

github.com

Node.js v21で実験的に実装されたWebSocketが安定版へ昇格しました。

WebSocketはグローバルな変数として定義されているため、ブラウザのJavaScriptのようにNode.jsプログラム内のどこからでもimportなしで利用可能です。

// importしなくてもそのまま WebSocketを利用可能
const socket = new WebSocket("ws://localhost:8080");

fs.globとglobSyncの追加

github.com

fsモジュールにglobglobSyncが追加されました。まだ実験的な機能です。

以下のように**/*.jsとすると、その条件に一致するファイルのパスをすべて取得します。

import { glob } from "node:fs";

glob("**/*.js", (err, matches) => {
  if (err) throw err;
  console.log(matches);
});
/**
[
  'node22/esm.js',
  'node22/glob.js',
  'node22/test.js',
  ...
]
 */

上記は非同期に実行されるのでコールバック関数を渡す必要がありますが、以下のようにPromise版も提供されています。

import { glob } from "node:fs/promises";

for await (const entry of glob("**/*.js")) {
  console.log(entry);
}
/**
node22/esm.js
node22/glob.js
node22/test.js
...
 */

また、globSync()を使えば非同期ではなく同期的に実行することもできます。

import { globSync } from "node:fs";

const result = globSync("**/*.js");
console.log(result);
/**
[
  'node22/esm.js',
  'node22/glob.js',
  'node22/test.js',
  ...
]
 */

Stream の highWaterMarkのデフォルト値の変更

Streamは大きなデータを連続的に逐次処理したりするのに向いています。動画のストリーミング処理のように、大きなデータを一気に読むこむのではなく少しずつ読み込む処理に適しています。

highWaterMarkはStreamで処理を行うときの少しずつ読み込むデータの大きさの閾値です。

highWaterMarkについては以下の記事がわかりやすいです。

techblog.yahoo.co.jp

highWaterMarkは任意の値を設定することができStream処理のパフォーマンス改善を行うことができます。highWaterMarkを任意の値に変更することが可能です。上記の記事内にもあるように、Streamの処理のパフォーマンスを良くするにはhighWaterMarkの値を大きくすると良いでしょう。

そののhighWaterMarkのデフォルト値が今回16KiBから64KiBに変更されました。
それによりhighWaterMarkを指定しなくてもStream処理のパフォーマンスが上がることが期待されます。
ただし、highWaterMarkの値が上がるということは、その分一気に読み込むデータの量が大きくなります。環境に応じて適切な値を設定する必要があるかもしれません。デフォルトのhighWaterMarkの値を設定するためのsetDefaultHighWaterMark()を使うと良いでしょう。

setDefaultHighWaterMark(false, 1024 * 16); // 16KiB

ちなみにgetDefaultHighWaterMark()でデフォルトのhighWaterMarkの値も取得できます。
また、ReadableStreamではreadableHighWaterMarkWritableStreamではwritableHighWaterMarkでそれぞれのデフォルトのhighWaterMarkの値も取得できます。

これらを使い、Node.js v21とv22で値を比較してみました。

Node.js v21

getDefaultHighWaterMark: 16384
readableHighWaterMark: 65536
writableHighWaterMark: 16384
Node.js v22

getDefaultHighWaterMark: 65536
readableHighWaterMark: 65536
writableHighWaterMark: 65536

まとめ

今回紹介したNode.js v22はバージョンが偶数なので10月頃にLTSになる予定です。この記事の執筆時点では2024年10月29日にLTSになる予定です。
日程についての最新の情報は以下のリポジトリをご確認ください。

github.com

今回の個人的な目玉はrequire()でESMが読み込めるようになったことです。ESMでしか配布されていないnpm packageの読み込みがCJSな環境でもESM/CJSを意識せずに使えるようになると嬉しいです。

長くなってしまいましたが、最後までお読みいただきありがとうございました。不備や質問がございましたら、@shisama_までメンションするかブコメなどでコメントください。

参考記事