【Node.js】Node 26 完全ガイド — Temporal API と Iterator.concat

「Temporal」の文字と時計・地球を配したNode.js 26のアイキャッチ画像

Node.js 26で標準搭載されたTemporal APIとMap.getOrInsert・Iterator.concatの使い方を、v26.8.2で実行した結果とともにまとめます。LTS昇格や削除されたAPIの確認点も分かります。

目次

この記事が解決する悩み

  • Temporal APIを実務で使えるレベルなのか知りたい
  • LTSに上がる前に何を確認しておくべきか知りたい

対象読者: Node.jsでサーバーやツールを書いている人
前提知識: Node.js 22以降を使っていること

結論

Node.js 26は2026年5月5日に出たCurrent版で、2026年10月にLTSへ昇格します。いちばん大きな変化はTemporal APIがフラグなしで使えるようになったことで、日付とタイムゾーンの扱いがかなり楽になりますよ。

まず動かして確かめます。手元のNode.js 26.8.2で実行した結果がこちらです。

// node26-check.mjs
console.log('node', process.version, 'v8', process.versions.v8);

const today = Temporal.Now.plainDateISO('Asia/Tokyo');
const closing = today.with({ day: today.daysInMonth });

console.log('today  :', today.toString());
console.log('closing:', closing.toString());
console.log('rest   :', today.until(closing).toString());

const map = new Map();
map.getOrInsertComputed('cache-key', () => 'first call only');
console.log('upsert :', map.getOrInsertComputed('cache-key', () => 'never used'));

console.log('concat :', [...Iterator.concat([1, 2], new Set(['x']))].join(','));

実行結果です。

$ node --version
v26.8.2

$ node node26-check.mjs
node v26.8.2 v8 14.6.202.34-node.28
today  : 2026-09-15
closing: 2026-09-30
rest   : P15D
upsert : first call only
concat : 1,2,x

この3つが動けば、問題ありません。あとは既存コードで消されたAPIを踏んでいないかの確認だけで、移行はほぼ終わります。

Node.js 26の位置づけ

リリースノートのハイライトは4点です。Temporal APIのデフォルト有効化、V8が14.6へ更新、Undiciが8系へ、そして非推奨APIの削除がまとめて入りました。

地味に重要なのがリリースサイクルの話です。Node.js 26は旧スケジュール最後のリリースになります。Node.js 27以降は年1回のリリースに変わり、番号もカレンダー年に揃い、全バージョンがLTSになる予定です。つまり偶数系がLTSという今の常識は、26でいったん終わりです。

項目 Node.js 26 備考
リリース 2026年5月5日 Current
LTS昇格 2026年10月 保守は2027年10月から
V8 14.6.202.34 Chromium 146相当
Undici 8.x fetch()の中身
EOL 2029年4月 LTSは30か月

Temporal APIが標準で使えるようになった

これまでTemporalはフラグ付きの実験機能でした。Node.js 24.16.0で確認するとtypeof Temporalはundefinedです。Node.js 26では同じ式がobjectを返します。フラグもポリフィルも不要になったわけです。

Dateを置き換える理由

旧Dateの問題は挙動が仕様に染み付いていることです。月が0始まり、オブジェクトが可変、タイムゾーンが実行環境依存、文字列パースが曖昧。Temporalはこの4つを別々の型に切り分けました。

  • Temporal.PlainDate : 日付だけ。時刻もタイムゾーンも持たない
  • Temporal.PlainDateTime : 日時だけ。タイムゾーンは持たない
  • Temporal.ZonedDateTime : 日時とタイムゾーンをセットで持つ
  • Temporal.Instant : エポックからの絶対時刻だけ
  • Temporal.Duration : 期間

型が分かれているので、PlainDateにタイムゾーン変換を呼ぼうとすると実行前に弾かれます。旧Dateでありがちだった「実はUTCだった」が起きにくい設計ですね。

日付計算はカレンダー単位で動く

月末の扱いが旧Dateより素直です。1月31日に1か月足すと2月28日に丸まります。旧Dateで同じことをすると、月の繰り上がりを自前で処理することになりますからね。

const jan31 = Temporal.PlainDate.from('2026-01-31');

console.log(jan31.add({ months: 1 }).toString());
console.log(jan31.daysInMonth);
console.log(jan31.until('2027-01-01', { largestUnit: 'month' }).toString());
2026-02-28
31
P11M1D

untilのlargestUnitをmonthにすると月数で、dayにすると日数で返ってきます。単位を自分で決められるのが便利ですよ。

タイムゾーンとサマータイム

2026年3月8日のアメリカ東部はサマータイム開始日で、現地時間の午前2時が存在しません。旧Dateだとこの計算を自分で補正する羽目になります。

const before = Temporal.PlainDateTime
  .from('2026-03-08T01:30')
  .toZonedDateTime('America/New_York');

const after = before.add({ hours: 1 });

console.log(before.toString());
console.log(after.toString());
console.log(before.until(after, { largestUnit: 'hour' }).toString());
2026-03-08T01:30:00-05:00[America/New_York]
2026-03-08T03:30:00-04:00[America/New_York]
PT1H

1時間足したのに表示は2時間進んでいます。オフセットが-05:00から-04:00に変わったので、実時間はきっちり1時間です。この挙動が標準で手に入るのは大きいですね。

V8 14.6で入った新メソッド

V8の更新に合わせて、TC39の2つのプロポーザルがそのまま使えるようになりました。

Map.getOrInsert / getOrInsertComputed

「無ければ作って入れる」を1回のメソッド呼び出しで書けます。集計コードの定番パターンが2行減りますよ。

const rows = [
  { path: '/api/users', ms: 120 },
  { path: '/api/users', ms: 80 },
  { path: '/api/posts', ms: 300 },
];

const stats = new Map();

for (const row of rows) {
  const s = stats.getOrInsertComputed(row.path, () => ({ count: 0, total: 0 }));
  s.count += 1;
  s.total += row.ms;
}

for (const [path, s] of stats) {
  console.log(path, 'n =', s.count, 'avg =', s.total / s.count);
}
/api/users n = 2 avg = 100
/api/posts n = 1 avg = 300

getOrInsertのほうは値をその場で渡す形です。第2引数は必ず評価されるので、生成コストが高いオブジェクトにはgetOrInsertComputedを使います。実際に効くのはこの違いで、前者は毎回オブジェクトを作って捨てることになります。

const cache = new Map();

// 生成されるのは最初の1回だけ
const first = cache.getOrInsertComputed('expensive', () => ({ heavy: true }));
const second = cache.getOrInsertComputed('expensive', () => ({ heavy: true }));

console.log(first === second, cache.size);
true 1

WeakMapにも同名のメソッドが入っています。オブジェクトをキーにしたキャッシュをGCに任せられるので、リクエスト単位のメモ化なんかはこれで済みます。

Iterator.concat

複数のイテラブルを1本のイテレータにまとめます。配列に変換しないので、無限ジェネレータとも組み合わせられます。

function* naturals() {
  let i = 0;

  while (true) {
    yield i;
    i += 1;
  }
}

const head = [...Iterator.concat(naturals()).take(5)];

console.log(head.join(','));
console.log([...Iterator.concat([1, 2], new Set(['x']), naturals()).take(4)].join(','));
0,1,2,3,4
1,2,x,0

1つ注意点があります。非同期イテラブルは渡せません。Iterator.concat(asyncGenerator)はTypeErrorになります。async側はfor awaitで回してください。

実践ユースケース

ページネーションを遅延連結する

APIをページ単位で叩くコードは、全件配列に詰め込むとメモリを無駄に食います。必要な分だけ引ける形にしておくと、一覧画面の初期表示が軽くなりますよ。

async function* fetchPages(path, pages) {
  for (let page = 1; page <= pages; page += 1) {
    const res = await fetch(`${path}?page=${page}`);
    const json = await res.json();

    yield* json.items;
  }
}

let seen = 0;

for await (const item of fetchPages('/api/items', 5)) {
  console.log(item.id);

  seen += 1;
  if (seen >= 10) {
    break;
  }
}

上の例は非同期なのでfor awaitです。同期のジェネレータならIterator.concat(pageA(), pageB(), pageC())でつなげます。ストリームの前段で複数ソースを1本にまとめたいときに向いていますね。

予約時刻を複数タイムゾーンで表示する

海外チームと日程を調整するときのやつです。1つのZonedDateTimeから必要なタイムゾーンだけ取り出せます。

const booking = Temporal.ZonedDateTime.from({
  timeZone: 'Asia/Tokyo',
  year: 2026, month: 9, day: 18, hour: 10, minute: 30,
});

console.log(booking.toString());
console.log(booking.withTimeZone('UTC').toString());
console.log(booking.withTimeZone('America/New_York').toString());

console.log(booking.toLocaleString('ja-JP', { dateStyle: 'full', timeStyle: 'short' }));
2026-09-18T10:30:00+09:00[Asia/Tokyo]
2026-09-18T01:30:00+00:00[UTC]
2026-09-17T21:30:00-04:00[America/New_York]
2026/9/18金曜日 10:30

toLocaleStringがIntlのフォーマットをそのまま使えるのも助かります。旧Dateと行き来したいときはエポックミリ秒を経由します。Temporal.InstantにtoDate()は無いので、そこだけは気をつけてください。

const legacy = new Date(booking.epochMilliseconds);
console.log(legacy.toISOString());

const back = Temporal.Instant
  .fromEpochMilliseconds(legacy.getTime())
  .toZonedDateTimeISO('Asia/Tokyo');

console.log(back.toString());
2026-09-18T01:30:00.000Z
2026-09-18T10:30:00+09:00[Asia/Tokyo]

署名に文脈を埋め込む

Node.js 26のcryptoには2つ入りました。Ed25519などの生鍵フォーマット対応と、署名にcontextを渡すオプションです。生鍵はraw-publicとraw-privateで32バイトのまま扱えます。

import { generateKeyPairSync, createPrivateKey, createPublicKey, sign, verify } from 'node:crypto';

const { publicKey, privateKey } = generateKeyPairSync('ed25519');

const rawPub = publicKey.export({ format: 'raw-public' });
const rawPriv = privateKey.export({ format: 'raw-private' });

console.log(rawPub.length, rawPriv.length);

const pub = createPublicKey({ key: rawPub, format: 'raw-public', asymmetricKeyType: 'ed25519' });
const priv = createPrivateKey({ key: rawPriv, format: 'raw-private', asymmetricKeyType: 'ed25519' });

const msg = Buffer.from('hello node 26');
const ctx = Buffer.from('engineer-notes:v1');
const sig = sign(null, msg, { key: priv, context: ctx });

console.log('ok        :', verify(null, msg, { key: pub, context: ctx }, sig));
console.log('wrong ctx :', verify(null, msg, { key: pub, context: Buffer.from('v2') }, sig));
32 32
ok        : true
wrong ctx : false

contextはBufferかTypedArrayで渡します。文字列を渡すとERR_INVALID_ARG_TYPEで落ちるので、そこはハマりどころですね。同じ鍵で用途ごとに署名を分離できるので、トークン種別のドメイン分離に使えます。

Node.js 26で消えたAPI

移行でいちばん痛いのは削除されたAPIです。Node.js 26.8.2で実際に確認した結果をまとめます。

削除されたもの 代替
res.writeHeader() res.writeHead()
node:_stream_wrap ほか _stream_* の6モジュール 公開のstream API
--experimental-transform-types 型注釈の実行時変換は不可
module.register() 実行時非推奨。registerHooks()へ

自分のコードが踏んでいないかは、これで一発です。

import http from 'node:http';

console.log('writeHeader:', typeof http.ServerResponse.prototype.writeHeader);
console.log('writeHead  :', typeof http.ServerResponse.prototype.writeHead);

for (const name of ['_stream_wrap', '_stream_readable']) {
  try {
    await import(`node:${name}`);
    console.log(name, ': loaded');
  } catch (err) {
    console.log(name, ':', err.code);
  }
}
writeHeader: undefined
writeHead  : function
_stream_wrap : ERR_UNKNOWN_BUILTIN_MODULE
_stream_readable : ERR_UNKNOWN_BUILTIN_MODULE

ログにERR_UNKNOWN_BUILTIN_MODULEが出ていたら、その依存パッケージが古い可能性が高いですね。npm lsで該当箇所を辿って、メジャー更新できるか確認してみてください。

【広告】お名前.comならドメイン取得が格安。

まとめ

Node.js 26は派手な新機能は少ないですが、Temporalがフラグなしで入ったことで実務の書き方が変わります。日付計算を自前で補正していたコードは、かなり削れるはずです。

機能 Node 24 LTS Node 26
Temporal API フラグ付き デフォルト有効
Map.getOrInsert なし あり
Iterator.concat なし あり
Ed25519生鍵フォーマット なし あり
Undici 7系 8系
V8 13.6 14.6

本番投入のタイミングですが、2026年10月のLTS昇格を待つのが無難です。今すぐ試したいならバージョンマネージャで26系を入れて、まず日付まわりのテストを通してみてください。Temporalで書き直したコードは、そのまま残せますよ。

日付とタイムゾーンのバグは見つけにくいので、テストが薄い部分から順にTemporalへ寄せていくのがおすすめです。

検証環境

OS Windows 11 (build 10.0.26200)
CPU AMD Ryzen 9 PRO 8945HS w/ Radeon 780M Graphics
メモリ 28GB
言語/ツール Node.js 26.8.2(公式のポータブル配布を一時ディレクトリに展開)
使用コマンド node test.mjs
測定日 2026-09-17

上記の環境で実際に実行して確認した結果です。環境が異なる場合は挙動が変わることがあります。

Node.js 26.8.2 で Temporal API・Map.getOrInsert・Iterator.concat を実際に呼び出した結果
Node.js 26.8.2 で Temporal API・Map.getOrInsert・Iterator.concat を実際に呼び出した結果

参考

本文の記述は以下の一次情報を確認して書いています。

あわせて読みたい

【広告】このサイトはConoHa WINGで運営しています。安定した高速サーバーで快適にブログを書けています。いつもありがとう!!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次