Temporalのpolyfillをゼロから実装した

に公開

自作した理由は単純で、既存のpolyfillに満足できなかったからです。

要約

https://npmx.dev/package/temporal-polyfill-lite

  • 軽量なTemporalのpolyfillであるtemporal-polyfill-liteを実装しました。
  • 最終版(最新)の仕様を実装しており、TypeScript公式の型定義とも互換性があります。
  • 大半の(グレゴリオ暦しか使わない)開発者にとっては、2026年4月現在これが一番バンドルサイズの小さいpolyfillとなっています。

Temporalの現在地

Temporalはこの3月にstage 4に到達しました。Firefoxは去年5月に、Chromeは今年の1月にTemporal対応のバージョンをリリースしています。Node.jsは今年5月にリリース予定のv26でTemporalに対応する予定です[1]。Safariの実装は遅れていますが、実装は進行中なので時間の問題でしょう[2]

今年はTemporal元年なのでどんどん使っていこう……と言いたいところですが、今のところpolyfillが必要です。最近まで、Temporalのpolyfillはこの世界に@js-temporal/polyfilltemporal-polyfillの2つしかありませんでした。

https://npmx.dev/package/@js-temporal/polyfill

https://npmx.dev/package/temporal-polyfill

既存のpolyfill

@js-temporal/polyfill

@js-temporal/polyfillは、Temporalの公式リポジトリにあるリファレンスpolyfillをベースにしており、Temporalのchampion複数人によってメンテナンスされています。元のリファレンス実装は仕様を検証するためのものであってプロダクションで使われることは想定していないので、実運用上の問題を解消するため@js-temporal/polyfillの側でパフォーマンスの改善などが行われています。

問題は、minify後で160kB、gzipしても45kBと、とにかくバンドルサイズが大きいということです。参考までに、かつての日時ライブラリの代表格であるMoment.jsはminify後で60kB、gzip後で18.5kBです。Moment.jsに限らず他の日時ライブラリと比較しても、これはフロントエンドのプロジェクトにとっては大きすぎます。元のコードはバンドルサイズを度外視して仕様をそのまま書き起こしたもので、パフォーマンス改善は後付けでも可能ですが、バンドルサイズの削減は困難ということでしょう。

しかも、championの人たちはリファレンスpolyfillのバグ修正や仕様変更の追従といった作業を非常に積極的に行っていたものの、@js-temporal/polyfillの側は10ヶ月ほど更新が滞っていました。最近は@js-temporal/polyfillの側の更新も再開していますが(例: js-temporal/temporal-polyfill#361)、まだリリースはされていません。念のため言っておくと、championの人たちを責める気は一切ありません。公式のリファレンス実装と下流のバージョンの両方をメンテナンスするのは大変な作業ですし、Temporalのchampionの人たちはTemporalをstage 4に上げるための莫大な作業に忙殺されていました。Temporalの進展を2年間ウォッチしてきた身として、Temporalの標準化がどれだけ大変だったかは誰よりも理解しているつもりです。

temporal-polyfill

もう1つの実装であるtemporal-polyfillはminify後で57kB、gzip後で20kBと、かなり軽量です。あまり「小さい」と言えるサイズではありませんが、Temporal自体が1万行以上の仕様を持つ巨大なプロポーザルであることを考えると、これくらいになるのは仕方ないと思います。残念ながら、@js-temporal/polyfillと同様にtemporal-polyfillも古い仕様から更新されておらず、最終的な仕様では解決済みの仕様バグや混乱を招く挙動などが残ったままになっています。つい最近更新作業用のissueができたものの、まだ未着手の状態です。

https://github.com/fullcalendar/temporal-polyfill/issues/94

仕様バグといっても極めて稀なエッジケースでしか発生しないものがある一方、普通にコードを書いていたら遭遇するようなものもあります。

// 通常のサマータイム:
Temporal.ZonedDateTime.from(
  "2025-11-02T01:01:00-07:00[America/Vancouver]",
).until("2025-11-02T01:00:00-08:00[America/Vancouver]", {
  largestUnit: "days",
});
// 両方のpolyfillで`RangeError`になるが、
// 仕様ではPT59M(59分)になるべき

Temporal.PlainYearMonth.from("2024-02").subtract(
  { years: 1 },
  { overflow: "reject" },
);
// どちらのpolyfillでも`RangeError`になるが、
// 仕様では2023-02(2023年2月)になるべき

特に、各種の暦に対するera(時代・元号)の定義が決まったのは比較的最近なので、どちらのpolyfillも追従できていません。iso8601以外の暦を扱う人にとっては非常に困ります。

Temporal.PlainDate.from("2026-04-01[u-ca=gregory]").era;
// どちらのpolyfillでも"gregory"だが、仕様では"ce"になるべき

Temporal.PlainDate.from("2026-04-01[u-ca=hebrew]").era;
// どちらのpolyfillでも`undefined`になるが、仕様では"am"になるべき

また、temporal-polyfillには特にタイムゾーンの扱いや日時の計算に関するバグがいくつかあります。バグ自体は仕方ないとはいえ、バグが修正されない状態だとTemporalの導入に支障を来たしかねません。私は日時やタイムゾーンの計算の信頼性こそがTemporalの存在意義だと思っていますが、その部分でpolyfillにバグが残ったままなのは実用上非常に困ります。もちろんメンテナを責める気は一切ないのですが(そもそもTemporalのpolyfillをメンテナンスすること自体がめちゃくちゃ大変だし、善意でプロジェクトをやっているメンテナを責めるのはOSSで最も悪い行為だと思っています)。

polyfillの実装が大変な理由

一般論として、polyfillを書くにはECMAScriptの専門知識が必要ですが、実装がめちゃくちゃ大変というわけではありません。とにかく仕様を読んでその通りに実装するだけです。実際、手頃なプロポーザルのpolyfillを実装するのはECMAScriptに興味のある人の入門としてちょうどいいくらいの難易度だと思います。

Temporalは話が別です。Temporalの仕様は1万行以上あり、ECMA-262(JavaScriptのコア部分の仕様)が5.3万行、ECMA-402(国際化APIの仕様)が1万行であることから分かるように、ECMAScript全体と比肩するレベルでTemporalは巨大です。もちろんこれはECMAScriptの歴史でも最大級の仕様追加ですし、ここまで巨大な仕様となると実装はおろか理解するだけでも大変です。しかも、普通のpolyfill実装者に要求されるような知識だけでなく、タイムゾーン、暦、Intl.DateTimeFormatといったECMA-402や国際化に関する専門知識すら必要です。

選択肢

フロントエンドでは唯一の選択肢のtemporal-polyfillは様々なバグを抱えていて、バグ修正に動いている人は文字通り誰もいませんでした。あれだけ巨大な仕様のpolyfillのバグを修正できる開発者なんて滅多にいないだろうし、メンテナの人も忙しそうだったので、仕方ないといえば仕方なかったと思います。

私はweb技術が好きな一介の大学院生であって特に標準化団体やブラウザベンダに属していたりはしませんが、たまたまTemporalや国際化(i18n)に関する多少の知識とECMAScriptの仕様を読む最低限の能力があり、JavaScriptのエコシステムに貢献したいという熱意もありました。比較的自然な帰結として、私はある種の義務感から、Temporalの導入を妨げかねないpolyfillの問題をどうにかしようと考えるようになりました。

最初はtemporal-polyfillのバグを修正しようと考え、実際にタイムゾーンのバグの一部を修正するプルリクエストを1つ送りましたが、残念ながら半年経った今も音沙汰がありません。他のタイムゾーン関連のバグも修正する気はあったのですが、元のコードの設計をそのままにして修正したり、逆に設計についてメンテナの人と議論したりするには能力が足りませんでした。日時演算のバグに至っては、プロジェクトの構造が仕様書と違っていたこともあって、仕様と照合してバグの原因箇所を調べるのが私には困難でした。

temporal-polyfillをフォークして大幅に整理することも考えましたが、最終的により「頭のおかしい」解決策、すなわちpolyfillのゼロからの実装を決めました。temporal-polyfillをいじるのが自分の能力を超えていたという理由もありますが、これはTemporalを追う中でずっと考えていたこと、すなわち「少数の開発者しか使わない非グレゴリオ暦のサポートを削った場合、Temporalのpolyfillをどこまで小さくできるのか」という疑問に答えを出す機会でもありました。

要件と設計

temporal-polyfill-liteは既存のpolyfillと異なる要件・設計の元に実装されています。

プロジェクト構造

temporal-polyfill-liteでは、Temporalの仕様をほぼ逐語的に実装しています。

Temporalの仕様は(私を含む)たくさんの人のレビュー・検証を受けてきた疑似コードです。仕様をそのまま忠実に実装することでバグを減らせるだけでなく、リファレンスpolyfillと付き合わせてデバッグすることも容易になります。また、仕様がリファクタリングされればそれをそのまま適用することでpolyfillのコードを整理できますし、逆に私がpolyfillのコードを整理できれば、公式リポジトリに同様のプルリクエストを送って仕様の整理に貢献することすら可能です。

グローバル空間の破壊に対する防御策

String.prototype.replaceArray.prototype.mapといった組み込みメソッドが破壊されるとtemporal-polyfill-liteは動かなくなります。これは厳密には仕様違反ですが、現実的に問題になることは稀でしょう。もし組み込みメソッドが削除されたり変更されたりすれば、polyfill以前にアプリケーション全体が壊れます。Node.jsやDenoの標準ライブラリや@js-temporal/polyfillがやっているように、primordialsと呼ばれるテクニックを使えばそういったグローバル空間の破壊に抗うことができますが、temporal-polyfill-liteではパフォーマンスとバンドルサイズを犠牲にする割に得られるメリットが少なすぎるという理由で導入していません。

参考記事:

https://zenn.dev/pixiv/articles/2f6511742d7907

https://43081j.com/2026/03/three-pillars-of-javascript-bloat

バンドルサイズ

temporal-polyfill-liteは徹頭徹尾バンドルサイズの縮小を追求して開発しており、そのために様々なテクニックを適用しています。

巨大な整数の内部表現

Temporalはナノ秒単位の日時・期間をサポートしています。仕様上の"epoch nanoseconds"(UNIXエポックからの経過ナノ秒)や"time duration"(ナノ秒換算での期間)といった値は、JavaScriptの安全な整数の最大値(53ビット)どころか64ビット整数すら超える可能性があります。temporal-polyfill-liteでは、これらの巨大な整数をBigIntではなく、8.64e13[3]で割った商と余りからなる長さ2の配列として内部で保持しています。この賢いアイデアはtemporal-polyfillから拝借しました。

対応ブラウザ

temporal-polyfill-liteは"Baseline 2020"の機能しか使っておらず、そのため2020年以降のブラウザでそのまま動きます。内部でBigIntを使っていないので、temporal-polyfill-lite内部で使っている標準APIのpolyfillを読み込んだ上でtemporal-polyfill-lite自身をトランスパイルすれば、2020年以前のブラウザでも動かすことができます(BigIntは原理的にトランスパイルやpolyfillが不可能)。

タイムゾーン

temporal-polyfill-liteは全てのタイムゾーンをサポートしています。というかタイムゾーンをサポートしないTemporalのpolyfillはもはや使う意味がないと思います。

デフォルトではiso8601gregoryのみをサポートすることでバンドルサイズを最小限に保っています。なお、gregoryのサポートはTemporal.PlainMonthDayTemporal.PlainYearMonthIntl.DateTimeFormattoLocaleStringで正しくフォーマットしようとすると必要になります(この2つのクラスはちょっと挙動が特殊なので)。

Temporal.PlainYearMonth.from("2026-04").toLocaleString("ja", {
  dateStyle: "long",
});
// 非常に分かりにくい「暦の不一致」エラーが発生する

Temporal.PlainYearMonth.from("2026-04").toLocaleString("ja", {
  calendar: "iso8601",
  dateStyle: "long",
});
// ChromeやFirefoxだと"2026 4月"になるが、日本語として一応理解できるとはいえ
// 普通欲しい形式ではない(そもそも「年」が抜けている)
// Node.jsのv25だと"2026 "というバグった結果が返ってくる

Temporal.PlainYearMonth.from("2026-04-01[u-ca=gregory]").toLocaleString(
  "ja",
  { dateStyle: "long" },
);
// "2026年4月"(想定通りのフォーマットになる)

実装

本当に長い戦いでした。

このプロジェクトを始めたのは2025年6月ですが、その後の半年間は時間と精神的余裕が足りず、あまり進みませんでした。2025年12月にプロジェクトを再始動してゼロから始め(コードはかなり再利用しましたが)、100%動く最初のリリースを2026年1月下旬に行うことができました。再始動から最初のリリースまで7週間かかった計算です。

Temporalのchampionや各ブラウザ・JavaScriptエンジンの実装者の莫大な努力のおかげで、Temporalのtest262におけるカバレッジはほぼ100%となっていました。championたちが開発したtemporal-test262-runnerという高速なテストランナーのおかげでtest262の実行時間が大幅に短縮され、開発中のイテレーションを高速で回せました。さらに、TemporalのリポジトリにはTemporal実装を十万・百万単位のテストケースでテストする包括的なテストスクリプトが準備されていました。これらのツールやテストケースのおかげで、私はpolyfillのバグをいくつも発見・修正することができました。

ところで、私は他のpolyfillの開発者と違い、ほとんど確定した状態の仕様を元にpolyfillを実装しました。つまり私は2024年6月の仕様削減のような巨大なnormative changeを実装に反映させる苦労を経験していません。Temporalの仕様が洗練・変更されるのに合わせて長年polyfillをメンテナンスしてきた先駆者のみなさんに敬意を表したいと思います。

polyfillの実装時の副産物として、仕様のバグや誤りをいくつか発見したり、仕様を改善するプルリクエストを送ったりすることができました。Temporalの仕様の改善に微力ながら貢献できたことを嬉しく思います。

なお、厳格な仕様書と包括的なテストスイートがある以上、Claude CodeやCodexに頼めば数日、もしかしたら数時間でTemporalのpolyfillを完成させられたかもしれません。人間がコードを書くのは時代遅れだという風潮があるのも承知しています。それでも私は全てのコードを手で書きましたし、そのことを後悔していません。実装作業は確かに相当な労力を要しましたが、これは自分にとっては「望ましい困難」でしたし、自分で手を動かすことで巨大な仕様を隅々まで理解することができました。カナダで100年前に一度だけ起きたサマータイム関連の特殊なエッジケースや浮動小数点の非直感的な挙動など、実装レベルの細かすぎる問題を理解・対処することはコーディングエージェントに丸投げしたい退屈な作業ではなく、むしろそれこそが私にとっては至上の喜びでした。

とはいえ、たとえば深い理解を得るための伴走者となってもらうなど、DIY的なプログラミングの楽しみを損なわずにコーディングエージェントを活用する余地はもっと存在した気もします。

タイムゾーンの処理

JavaScriptエンジンの持つタイムゾーンDBにアクセスする唯一の方法は、timeZoneオプションを指定したIntl.DateTimeFormatの出力をパースすることです。一般論として、Intl.DateTimeFormatの出力はエンジンやバージョンによって差があるので、その結果をパースするのは推奨されないのですが、他に方法がないので仕方ありません。幸いformatToPartsメソッドは出力を構造化された情報として返すので、フォーマットが違っても比較的安定してパースすることができます。

new Intl.DateTimeFormat("en", {
  year: "numeric",
  month: "numeric",
  day: "numeric",
  hour: "numeric",
  minute: "numeric",
  second: "numeric",
  hour12: false,
  timeZone: "Asia/Tokyo",
}).formatToParts(0);
// 結果:
[
  { type: "month", value: "1" },
  { type: "literal", value: "/" },
  { type: "day", value: "1" },
  { type: "literal", value: "/" },
  { type: "year", value: "1970" },
  { type: "literal", value: ", " },
  { type: "hour", value: "09" },
  { type: "literal", value: ":" },
  { type: "minute", value: "00" },
  { type: "literal", value: ":" },
  { type: "second", value: "00" },
];

フォーマット結果の日時とタイムスタンプの差を計算することで、UTCオフセットを求めることができます。LuxonやDay.jsのtzプラグインや@date-fns/tzといった日時ライブラリや、他のTemporalのpolyfillも、似たような方法を使ってタイムゾーンの計算を行っています。

function getOffsetMilliseconds(epochSeconds: number, timeZone: string) {
  const parts = new Intl.DateTimeFormat("en", {
    year: "numeric",
    month: "numeric",
    day: "numeric",
    hour: "numeric",
    minute: "numeric",
    second: "numeric",
    hour12: false,
    timeZone,
  }).formatToParts(epochSeconds * 1000);
  const units = ["year", "month", "day", "hour", "minute", "second"];
  const [localYear, localMonth, localDay, localHour, localMinute, localSecond] =
    units.map((unit) => parseInt(parts.find((p) => p.type === unit)!.value));
  return (
    Date.UTC(
      localYear,
      localMonth - 1,
      localDay,
      localHour,
      localMinute,
      localSecond,
    ) -
    epochSeconds * 1000
  );
}
getOffsetMilliseconds(0, "Europe/London"); // 3600000 (UTC+1)
getOffsetMilliseconds(0, "America/New_York"); // -18000000 (UTC-5)
getOffsetMilliseconds(0, "Asia/Tokyo"); // 32400000 (UTC+9)
getOffsetMilliseconds(0, "Africa/Monrovia"); // -2670000 (UTC-0:44:30)

Intl.DateTimeFormatの処理は遅いので、このpolyfillではLRUキャッシュを使っていますが、現状のキャッシュ戦略は理想からはほど遠いです。多くの国・地域ではサマータイムを実施していないし、実施しているタイムゾーンでもオフセットは年に2回しか変化しないし、その他の政治的理由によるオフセット変更はさらに稀です。このように、UTCオフセットが変わる頻度は非常に低いにもかかわらず、このpolyfillでは毎秒ごとにUTCオフセットをキャッシュしています。この点は今後改善したいと思っています。

https://github.com/fabon-f/temporal-polyfill-lite/issues/26

バンドルサイズ削減

このpolyfillの目標の1つはバンドルサイズの削減でした。まず圧縮前・圧縮後のバンドルサイズを測定するスクリプトを書き、実際の変更が本当にバンドルサイズに影響するか確認できるようにしました。測定していないものを最適化することはできません。

一般的なライブラリやアプリケーションのminify後のJavaScriptコードを眺めれば、長いプロパティ名がそのまま残っていることに気付くはずです。これは、JavaScriptのminifierがプロパティ名をmangle(短縮)すれば簡単にコードを破壊してしまうからです。パブリックなAPIに露出しているプロパティ・メソッド名はもちろん、内部のプロパティでもブラケット演算子でアクセスしているものはmangleできません。

// 元コード
const foo = { fooBarBaz: 0 };
const prop = "fooBarBaz";
Object.keys(foo); // ["fooBarBaz"]
foo[prop]; // 0

// プロパティをmangleした後:
const foo = { a: 0 };
const prop = "fooBarBaz";
Object.keys(foo); // ["a"]
foo[prop]; // undefined

最初はpolyfill内でオブジェクトではなくタプル(固定長の配列)を使ってこの問題に対処しようとしましたが、この方法ではメンテナンス性が大幅に犠牲になってしまいます。いくらTypeScriptのLSPがあっても、isoDateTime[0][1]isoDateTime.date.monthと同じ意味だと理解するのは困難です。結局、プロジェクトを再始動するタイミングでプロパティのmangle処理を導入することにしました。

mangleしたくないプロパティ名を長いリストにして管理する代わりに、「短縮可能な内部プロパティ名は$_から始める」というシンプルな命名規則を導入しました(例: $isoDate)。これによって、mangleしてほしいプロパティ名だけをビルド時にmangleすることができます。このアイデアは私が考えたものではなく、以下の記事からそのまま借りました。

https://zenn.dev/mizchi/articles/mangle-best-practice

また、可能な範囲で文(statement)ではなく式(expression)を利用するようにしました。式は文と比べて様々なminify処理がしやすいという特徴があります。たとえば、複数の関数呼び出しはコンマ演算子で1つの式に結合でき、内部に式しか含まないif文は条件演算子(三項演算子)や論理演算子で書き換えることができます。

if (foo) {
  bar();
  baz();
}

// minify後:
foo && (bar(), baz());

throw文を内部でthrowする関数の呼び出しに置き換えるのは意味不明に見えますが、これも文から式への置き換えの一環で、これだけで全体のバンドルサイズがかなり縮みました。また、変数宣言も文の一種なので、可能な限り中間変数を削除することでminify後のサイズを減らすことができました。

// before:
function aaaaa(foo) {
  if (foo) {
    throw new RangeError("foo");
  }
  const bar = baz();
  return qux(bar);
}
// 上のコードはminify後にこうなる:
function aaaaa(e) {
  if (e) throw RangeError("foo");
  let t = baz();
  return qux(t);
}

// after:
function throwRangeError(message) {
  throw RangeError(message);
}
function aaaaa(foo) {
  if (foo) {
    throwRangeError("foo");
  }
  return qux(baz());
}
// 上のコードはminify後にかなり短くなる:
function aaaaa(e) {
  // 本番ビルドでは`throwRangeError`もmangleされる
  return (e && throwRangeError("foo"), qux(baz()));
}

様々な試行錯誤の末、バンドルサイズは初回リリース時と比較してminify後を基準として15%(60kBから52kB)、gzip後で10%(19.7kBから18kB)も減りました。

非グレゴリオ暦のサポート

バンドルサイズ削減に数週間を費やし、最適化が一段落した結果、明らかに大変で後回しにしていたタスクと向き合わざるを得なくなりました。つまり、非グレゴリオ暦のサポートです。todoリストにはずっと入っていたものの、グレゴリオ暦しか使わない大半の開発者向けのバンドルサイズを維持したままサポートを追加する具体的な方針が思いついていませんでした。

https://github.com/fabon-f/temporal-polyfill-lite/issues/6

Rustのfeature flagのようなものがJavaScriptにあれば良かったのですが、残念ながらありません。いろいろ考えた末、モジュールの関数を条件によって分岐させるためにcustom conditionを利用することにしました。幸い、このプロジェクトで使っているツールチェイン(Node.js、TypeScriptコンパイラ、Rolldown)は全てcustom conditionに対応していました。

package.jsonはこんな感じになっています:

{
  "exports": {
    ".": "./dist/index.js",
    "./calendars-full": "./dist/calendars/index.js"
  },
  "imports": {
    "#calendricalCalculations": {
      "nonIsoCalendars": "./src/internal/calendars/all.ts",
      "default": "./src/internal/calendars/basic.ts"
    }
  }
}

このパッケージには2つのエントリポイントがあります。通常版はcustom conditionのnonIsoCalendarsが無効の状態で、非グレゴリオ暦に対応した/calendars-fullnonIsoCalendarsが有効の状態でビルドしたものです。all.tsbasic.tsは基本的に同じ関数をエクスポートしていますが、対応している暦が異なります。詳しい実装が気になる人は以下のプルリクを参照してください:

https://github.com/fabon-f/temporal-polyfill-lite/pull/7

暦の計算

JavaScriptエンジンはグレゴリオ暦以外の日付を計算することができますが、その結果に直接アクセスできるAPIはありません(というより、Temporalこそが暦の計算に直接アクセスするためのAPIです)。しかし、グレゴリオ暦だけ相手にすればよかったタイムゾーンのときと異なり、グレゴリオ暦以外に対するIntl.DateTimeFormatの出力をパースするのは格段に厄介です。なぜなら、グレゴリオ暦以外に対するIntl.DateTimeFormatの出力は遥かに一貫性に欠けるからです。たとえば、スマートフォン向けのビルドではグレゴリオ暦以外の暦に対する各言語のデータが削減されていたりします。

https://github.com/js-temporal/temporal-polyfill/issues/284

代替手段として、temporal-polyfill-liteではルールベースの暦の計算を全て自前で計算することにしました。中でもユダヤ暦(hebrew)は純粋な計算で求められる暦ではあるものの非常に複雑なので、ICU4Xの計算結果と比較するテストによってバグがないことを確認しています。純粋なルールベースではない、つまり天文シミュレーションや歴史上の暦データが必要な暦(islamic-umalqurapersianchineseおよびdangi)についてはどうしようもないので、Intl.DateTimeFormatの結果を頑張ってパースして取得しています。

結果

temporal-polyfill-liteは今のところ最終版の仕様に従っている唯一のpolyfillであり、またTypeScriptの公式の型定義と互換性のある唯一のpolyfillであり、最もバグの少ないpolyfill実装でもあります。とはいえ、これらの特徴は今すぐTemporalを実践投入したい人(たとえば私)にとっては重要ですが、他のpolyfillもそのうち(いつかは分かりませんが)更新・修正されることを考えると、長期的には優位点にならないでしょう。

むしろ、temporal-polyfill-liteの強みはバンドルサイズにあります。以下の表は2026年4月時点でのバンドルサイズをまとめたものです。なお、この結果は将来的に、特にtemporal-polyfill@js-temporal/polyfillが最終版の仕様を反映したタイミングで変わる可能性があります(最新のデータは比較用リポジトリを参照してください)。現状には基本的には満足していますが、calendars-fullバージョンはもう少しバンドルサイズを減らす余地があるかもしれません。

minified gzip brotli zstd
ネイティブ実装 0 kB 0 kB 0 kB 0 kB
temporal-polyfill-lite 52.1 kB 17.9 kB 15.9 kB 18.6 kB
temporal-polyfill-lite/calendars-full 63.6 kB 22.2 kB 19.6 kB 23.2 kB
temporal-polyfill 57.0 kB 20.3 kB 18.2 kB 21.2 kB
@js-temporal/polyfill 159.7 kB 45.3 kB 39.3 kB 47.2 kB

また、実行時のパフォーマンスはまだ測定・最適化を行っていないので、バンドルサイズを極端に大きくしない範囲で高速化する余地がありそうです。

どのpolyfillを使うべきか?

これは宣伝記事なので中立性も何もないのですが、可能な限り客観的かつ正直に書くよう努めます。

バックエンド

Temporalがあれば日時を堅牢な形で扱うことができるので、バックエンドのプロジェクトにTemporalを導入するのは合理的かつ有望だと思います。

バックエンドではバンドルサイズを過度に気にする必要がない一方、実装の安定性が重要になります。temporal-polyfill-liteは現段階でも十分安心して利用できるpolyfillだと私は信じていますが、最新の仕様への更新作業さえ終われば、半公式という安心感を取って@js-temporal/polyfillを選ぶのもアリだと思います。どちらのpolyfillもバグは非常に少ないです。

いずれにせよ、TemporalをサポートするNode.jsのv26は今年の10月にLTSになるので、そうなればバックエンドのプロジェクトでpolyfillを使う必要は完全になくなります。

フロントエンド

正直、TemporalがBaseline Newly availableにすら入っていない現段階では、フロントエンドのプロジェクトでTemporalを使うのは時期尚早かもしれません。いかんせんpolyfillのサイズがお世辞にも小さいとは言えないので。とはいえ、もう仕様は完全に確定しているので、今から試すことはできます。特にAstroのようなSSGフレームワークを使っているプロジェクトでは、polyfillをインストールしてもバンドルサイズが増える心配がないのでオススメです。

gzip後で20kB弱なので「polyfillを使ってでも今すぐTemporalを使うべき」とは言いづらいですが、それでも使いたい場合、最新の仕様に準拠していてバグがほぼなく最もバンドルサイズが小さいtemporal-polyfill-liteが、少なくとも現状では一番マシだと思います。バグ修正と更新が終わればtemporal-polyfillも選択肢に入るので、導入するタイミングでバンドルサイズやバグの数やその他の事情を考慮して適宜選ぶといいのではないでしょうか。

おわりに

Temporalの最後の2年間を通じて、仕様や実装の動向を見守りながらずっと「Temporalはいつ本番投入できるようになるのか?」ということを考えていました。特にpolyfillの状況がずっと懸念事項だったのですが、今は自信を持って「望むなら今すぐにも可能」と答えられます。TemporalをサポートするブラウザやJavaScriptランタイムが広く普及するまでの間(長くても数年間)の一時的な措置として、temporal-polyfill-liteは必要な役割を十二分に果たしてくれるはずです。

使っていてバグや疑わしい挙動を見つけたら、気軽にGitHubにバグレポートを送ってください。可能な限り速やかに調査・修正します。

最後に、TemporalというECMAScript史上例を見ない偉大なプロジェクトにささやかとはいえ貢献できたことを嬉しく思います。web標準万歳、ECMAScript万歳!

脚注
  1. 元は4月22日リリースの予定でしたが、Temporal実装などに起因するビルドの問題で5月4日に延期されました(Temporalの実装のせいだけではないですが)。V8はTemporalを実装するにあたってV8史上初めてICU4Xやtemporal_rsといったRustの依存関係を組み込んでいます(参考: nodejs/node#58730)。もちろんNode.js側でもビルドパイプラインにRustのツールチェーンをインストールする必要があり、かなり大変そうでした。 ↩︎

  2. 時間の問題だという点には私も同意しますが、「SafariはもうすぐTemporalを実装・リリースするだろう」と断言するのは無責任でしょう。特にICU4Cでの暦計算の問題などリリースにあたっての懸念事項がいくつかありますし、そもそもプルリクエストが月単位で音沙汰がないなど実装作業の進捗があまり思わしくなく、メインの実装担当であるIgaliaとAppleの間のコミュニケーションが上手くいってないとかTemporal実装の優先度が低いとかの可能性すら疑っています。……というのが最近までの私の見解でしたが、数日前からAppleの人がTemporalをガリガリ実装している様子を発見したので、意外と早く実装されるかもしれません。過度に期待せずに動向を注視しておきます。 ↩︎

  3. 8.64e13は1日をナノ秒で換算した値です。この値を選んだ理由は、第一に商と余りの両方がJavaScriptの整数の範囲に余裕をもって収まるので桁溢れバグを心配せずに済むから、第二にTemporalの中の丸め処理で除数となりうる数が基本的に8.64e13の約数で完結し、余りの部分だけの計算で完結するからです。 ↩︎

Discussion