Node.js 24 Krypton
2025年10月28日、Node.js 24がCurrent LTSからActive LTSになり、Kryptonのコードネームで提供が開始されています。Node.js 22はMaintenance LTSに移行し、Node.js 20は来春でMaintenance LTSを終えます。
v22で動いているアプリを移行させるため、v24の更新内容をまとめました。なお、本記事は24.0で加えられた大きな変更点を扱っていますが、現状の24.11と大差はありません。
本記事で使用したnodeおよびV8のバージョンは以下です。
node -v
v24.11.1
node
> console.log(process.versions.v8);
13.6.233.10-node.28
V8エンジン v13.6
Node.js 24で最も注目すべき点は、V8エンジンが13.6にアップグレードされたことです。以降でV8エンジンの新機能を1つずつ見ていきます。
Float16Array
これは標準的な浮動小数点数(Float32ArrayやFloat64Array)よりも小さい16ビットの浮動小数点数を効率的に扱うための型付き配列(Typed Array)です。
// 比較用の高精度な値
const HIGH_PRECISION_VALUE = 0.123456789;
// 1. Float32Array (4バイト/要素)
const f32 = new Float32Array(1);
f32[0] = HIGH_PRECISION_VALUE;
// 2. Float16Array (2バイト/要素)
const f16 = new Float16Array(1);
f16[0] = HIGH_PRECISION_VALUE; // 代入時に16ビット精度に丸められる
console.log("--- メモリ消費量の比較 (1要素あたり) ---");
console.log(`Float32Array: ${f32.BYTES_PER_ELEMENT} バイト`);
console.log(`Float16Array: ${f16.BYTES_PER_ELEMENT} バイト (半減)`);
console.log("\n--- 精度の比較 ---");
console.log(`元の値: ${HIGH_PRECISION_VALUE}`);
console.log(`Float32で保持: ${f32[0]}`);
// 32ビットでも丸めは発生しますが、Float16よりは高精度
console.log(`Float16で保持: ${f16[0]}`);
// 3. 差分を確認
const precisionLoss = Math.abs(f32[0] - f16[0]);
console.log(`\nFloat32とFloat16の差分(精度低下): ${precisionLoss}`);
実行結果
--- メモリ消費量の比較 (1要素あたり) ---
Float32Array: 4 バイト
Float16Array: 2 バイト (半減)
--- 精度の比較 ---
元の値: 0.123456789
Float32で保持: 0.12345679104328156
Float16で保持: 0.12347412109375
Float32とFloat16の差分(精度低下): 0.000017330050468444824
Float16Arrayを使用することで、1要素あたり4バイトから2バイトに半減します。その代わり、Float32で保持された値とFloat16で保持された値を比較すると、Float16の方がより大きく丸められ、元の値との差(精度低下)が発生していることが確認できます。
機械学習やグラフィックス処理でメモリ効率を上げるために使用されると思われます。
明示的なリソース管理
ファイルやネットワーク接続といったリソースのクリーンアップ処理を自動化する仕組みがランタイムレベルでサポートされています。コードをシンプルにしてメモリリークをなくすことができます。
クリーンアップ処理にはSymbol.asyncDisposeとSymbol.disposeがあり、両者は同期/非同期かの違いです。これらを呼び出す処理を自動化する構文がusingとawait usingです。
| 項目 | Symbol.dispose | Symbol.asyncDispose |
|---|---|---|
| 種類 | 同期 (Synchronous) | 非同期 (Asynchronous) |
| 戻り値 | void (値を返さない) | Promise<void> (Promiseを返す) |
| 構文 | using 宣言 | await using 宣言 |
| リソース | ファイルハンドル、メモリバッファ、タイマーなど、即座に閉じられるリソース | ネットワーク接続、データベース接続、非同期I/Oなど、完了を待つ必要があるリソース |
従来のコード
import { open } from "fs/promises";
async function main() {
const file = await open("example.txt", "w");
try {
await file.writeFile("Hello, manual cleanup!\n");
console.log("ファイルへ書き込み完了");
} finally {
// 明示的にクローズが必要
await file.close();
console.log("ファイルを閉じました");
}
}
main();
新しい書き方
import { open } from "fs/promises";
async function main() {
// await using により自動的にファイルが閉じられる
await using file = await open("example.txt", "w");
await file.writeFile("Hello, explicit resource management!\n");
console.log("ファイルへ書き込み完了");
// スコープを抜けると file は自動的にclose()される
}
main();
await ugingはリソースの開放を自動で行いますが、エラー処理そのものは自動で行いません。そのため、エラー補足が必要であればtry-catchは必要です。ただし、例外がスローされても、リソースの解放は確実に行われます。
これはPythonのwithやC#のusingに似たリソース管理の強力な仕組みです。
RegExp.escape
RegExp.escapeは、文字列を正規表現内でリテラルとして安全に使用できるようにエスケープする静的メソッドです。正規表現パターンを動的に構築する際に、特殊文字(.など)の意図しない解釈を防ぐのに役立ちます。
例えば…
const sentence = "He has two dogs. I have one dog.";
const pattern = /dog./;
const newSentence = sentence.replace(pattern, "cat.");
console.log(newSentence);
// He has two cat.. I have one dog.
本来は末尾のdog.をcat.に置き換えたかったのですが、dog.の.が任意の文字を表すため、意図しない結果となっています。こういったケースでRegExp.escape()は役立ちます。
const sentence = "He has two dogs. I have one dog.";
const pattern = new RegExp(RegExp.escape("dog.")); // here!
const newSentence = sentence.replace(pattern, "cat.");
console.log(newSentence);
// He has two dogs. I have one cat.
WebAssembly Memory64
WebAssembly(Wasm)が扱えるメモリ空間の制約を大幅に緩和する機能です。従来のWebAssemblyのメモリモデルは、アドレス空間が32ビットに制限されており、扱えるメモリの最大サイズは約4GBでしたが、64ビットに対応したことで4GBを超えた巨大なメモリ空間をWebAssemblyが利用できるようになりました。大規模ゲームなどで恩恵がありそうです。
Error.isError
Error.isError() は、「与えられた値がErrorオブジェクトまたはその派生型かどうか」を判定する静的メソッドです。VMコンテキストやワーカーなどで生成されたエラーは、instanceof Errorで正しく判定できないという問題があったため、これに対処しています。
VMコンテキストとは、同じプロセス内で独立した小さなJavaScript実行環境を作るためのものです。日常的に使用する機能ではありませんが、vsモジュールによって提供されています。
const vm = require('node:vm');
const ctx = vm.createContext({});
try {
vm.runInContext(`throw new Error("oops")`, ctx);
} catch (err) {
console.log(err instanceof Error); // false(予期せぬ結果)
console.log(Error.isError(err)); // true(正しく判定できる)
}
セキュリティ
npm 11
Node.js 24にはnpm 11が同梱されており、パフォーマンスとセキュリティが強化されています。これにより、--ignore-scriptsフラグがすべてのライフサイクルスクリプトに適用されるようになりました。
npmパッケージはインストール時に自動で実行されるnpm installやnpm updateなどのスクリプトがありますが、悪意あるパッケージがこれらのスクリプトを利用してマルウェアをインストールさせようとすることがあります。--ignore-scriptsはこのようなスクリプト実行を防ぎ、セキュリティを向上させることができます。CI/CDではセキュリティ対策、およびビルド時間の短縮で推奨されています。
パーミッション
パーミッションを扱う--experimental-permissionが正式版となり、--permissionフラグとなりました。この機能を使うと、アプリケーションが不要なリソースにアクセスするのを防ぎ、セキュリティリスクを軽減できます。
| パーミッション名 | 説明 |
|---|---|
--allow-fs-read | 特定のファイルやディレクトリからの読み取り操作を許可します |
--allow-fs-write | 特定のファイルやディレクトリへの書き込み操作を許可します |
--allow-child-process | child_processを使った子プロセスの生成を許可します |
--allow-worker | worker_threadsを使ったワーカーの生成を許可します |
--allow-net | ネットワークソケット(TCP/UDP)へのアクセスを許可します。ホストやポートで制限可能です |
例えば、以下を実行するとアプリケーションはtmp/dataからのみ読み取りができ、他のすべてのファイルシステムへのアクセスは拒否されます。
node --permission --allow-fs-read=/tmp/data app.js
child_processの引数
child_process.spawn()とchild_process.execFile()にコマンドの引数を渡す方法が変更されました。単一の文字列が禁止され、文字列の配列に厳格化されています。
const { spawn } = require('child_process');
// v24以前(禁止)
spawn('echo', ['hello', 'world'], { shell: true });
// v24以降(shell: trueの場合)
spawn('echo hello world', [], { shell: true });
// v24以降(shell: falseの場合)
spawn('echo', ['hello', 'world']);
これは以前から非推奨だったため、一貫性が向上したと言えます。
パフォーマンスの向上
Undici 7.0
Undiciは、Node.jsで使われている高性能なHTTP/1.1およびHTTP/2クライアントです。Web標準のFetch APIの実装で利用されています。version 7.0にアップグレードされ、以下が改善されました。
- 接続プーリングの強化 - HTTPリクエストの再利用効率が向上し、特に多数の外部APIコールを行うアプリケーションでリクエストが最大30%高速化するそうです
- HTTP/2の改善 - HTTP/2プロトコルに関する安定性や性能が向上しています
- Fetch APIの準拠強化 - Web標準のFetch APIの仕様への準拠が強化され、外部のポリフィルへの依存が減り、より標準的かつ効率的なネットワーク操作が可能になりました
AsyncLocalStorage
従来の非同期操作ではAsyncLocalStorageでバグが発生しやすかったようです。例えば、非同期コンテキストが失われたためにセッションデータが未定義を返したり、間違ったユーザIDを返すケースが見られました。
Node.js 24では内部実装がAsyncContextFrameを使用するようになったことで、パフォーマンスや信頼性が向上しています。開発者はコードの変更を行うことなく、これらの改善点を享受できます。
ネイティブのWebSocketクライアント
グローバルスコープにWebSocketクラスが追加されています。これはブラウザ環境で提供されている標準のWebSocket APIと同じインターフェイスを持っています。従来は、wsのようなモジュールのインポートが必要でしたが、これらを削除してnew WebSocket()が利用できるということです。
const socket = new WebSocket('ws://localhost:8080');
socket.addEventListener('open', event => {
console.log('WebSocket connection established!');
socket.send('Hello Server!');
});
socket.addEventListener('message', event => {
console.log('Message from server: ', event.data);
});
socket.addEventListener('close', event => {
console.log('WebSocket connection closed:', event.code, event.reason);
});
socket.addEventListener('error', error => {
console.error('WebSocket error:', error);
});
開発者エクスペリエンス
TypeScriptのサポート
v22から--experimental-strip-typeフラグをつけると、構成なしでTypeScriptが実行できていました。型などの消去可能なものに限りますが、v24からはフラグなしで可能になっています。
interface Point {
x: number;
y: number;
}
const coordinate: Point = {
x: 10,
y: 20,
};
// 座標: (10, 20)
console.log(`座標: (${coordinate.x}, ${coordinate.y})`);
上記はnode index.tsで実行できます。ただし、以下は不可です。
enum Fruit {
Apple,
Banana,
Orange,
}
const favoriteFruit: Fruit = Fruit.Banana;
// SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]:
// TypeScript enum is not supported in strip-only mode
console.log(`お気に入りの果物: ${favoriteFruit}`);
これは前述のように消去不可なenumを使用しているためです。
--experimental-transform-typesをつけると、JavaScriptへ変換して実行ができます。
node --experimental-transform-types index.ts
お気に入りの果物: 1
DenoのようにTypeScriptをファーストクラスとしてサポートしているわけではないため、引き続き構成ファイルのセットアップやトランスパイルは必要です。
URLPattern
URLPatternがグローバルオブジェクトに追加されました。これはURLと名前は似ていますが、目的や機能は大きく異なります。URLはURLの解析に使用されますが、URLPatternはURLパターンマッチングと抽出が主な目的です。
// /users/の後にIDが続くURLパターン
const pattern = new URLPattern({ pathname: "/users/:id" });
// 1. 一致のテスト
console.log(pattern.test("https://example.com/users/123")); // true
// 2. 一致と値の抽出
const match = pattern.exec("https://example.com/users/123");
console.log(match.pathname.groups.id); // "123" (IDを抽出)
ライブラリに依存することなく、カスタムルーティングを作成することができるようになります。
組み込みのテストランナーの強化
Node.js 22以前の組み込みのテストランナーは、ネストされたテストを記述する際に親のtest関数内に明示的にawaitを使用しなければいけませんでした。これを記述しないと、サブテストが完了する前に親テストが完了したと見なされ、意図しないテストの失敗が発生します。
Node.js 24では、すべてのサブテストが完了するまで自動的に待機するようになりました。これにより、明示的な await を記述する必要がなくなりました。
import { test } from 'node:test';
// (1) Node.js 24以前
test('親テスト', async (t) => {
// 以前は、サブテストの完了を保証するために`await`が必要
await t.test('サブテスト 1', () => { /* ... */ });
await t.test('サブテスト 2', () => { /* ... */ });
});
// (2) Node.js 24以降
test('親テスト', (t) => {
// Node.js 24では`await`は不要
t.test('サブテスト 1', () => { /* ... */ });
t.test('サブテスト 2', () => { /* ... */ });
});
ECMAScript 2025の新機能
以下はECMAScript 2025の新機能の一部ですが、ポリフィルなしで使用できるようになっていました。実際には、v23から導入されていたようですが、変更履歴には記載されていないため、おまけとして記載しておきます。
非推奨API
非推奨のAPIと代替手段を記述します。
url.parser- WHATWG URL APIが推奨されますSlowBuffer-Buffer.allocUnsafeSlow()を使用しますnewなしのREPLServerクラスのインスタンス化 -newキーワードを使用しますnewなしのREPL/Zlib関連クラスのインスタンス化 -newキーワードを使用しますtls.createSecurePair(廃止) -tls.TLSSocketを使用します
既知の問題
Node.js 24.x系列において、Buffer.allocUnsafeが誤ってゼロ初期化されたバッファを返す不具合が確認されています。Buffer.allocUnsafe() は、Node.jsで指定したサイズの新しいバッファを初期化せずに(つまり、ゼロ埋めせずに)作成するためのメソッドです。
この問題点は24.11.1で以前の仕様に戻ったようです。