Moment.jsからDay.jsに移行したらタイムゾーン変換が遅すぎて転げ回った話

# はじめに
タイトル通り、本記事は約1年前に[Moment.js](https://momentjs.com/)から[Day.js](https://day.js.org/)へ移行した際のお話です。
今更なことは多々ありますが、その時に色々と学んだことがあったので備忘録と自戒の意味を込めて残しておきます。
ライブラリを移行するのは頻度としてはさほど多くないのですが、だからこそ気を付けなければならないことがありますね。

# 前提
そもそもの話として、Moment.jsについては2020年9月頃に**メンテナンスモード**[^1]へ移行しています。
ライブラリとしてはmomentオブジェクトがmutableであるとか、サイズが大きいのを削減出来ないとかの問題があったのですが構造的に解決が難しいとのこと。（[公式サイトより](https://momentjs.com/docs/#/-project-status/)）
そういうことでMoment.jsはもう他のライブラリ使いなよ～って言ってるのですが、そのライブラリの一つとしてDay.jsが挙げられています。実際に検索すると「Day.jsに移行しました！」みたいな記事も多いです。
Day.jsの利点としてはMoment.jsとAPIが似ていてほぼ同じ感覚で使えること、ファイルサイズがMoment.jsと比較してかなり小さいことがあるそうです。
他のライブラリも勿論あるのですが、なるべく時間を掛けずに移行するなら最有力候補かもしれませんね。

# 発生した問題
ということで自分もDay.jsへの移行を進めていきました。実際の使用感はMoment.jsとほぼ同じなのがとても良かったのですが、一部機能（最大・最小を求める、タイムゾーン関連等）はプラグインが別途必要になるのは注意です。

さて、ここからが本題ですが手がけているシステムの都合上タイムゾーン関連の変換処理がとても必要になっています。
例えば下記のような感じですね。（なんでこんなことしているのかは本題から外れるので割愛）
```javascript
//******** 移行前(Moment.js) ********
function convertFromJST (date) {
  const result = moment(date).tz('Asia/Tokyo').format('YYYY/MM/DD HH:mm:ss');
  return new Date(result);
};

//******** 移行後(Day.js) ********
function convertFromJST (date) {
  const result = dayjs(date).tz('Asia/Tokyo').format('YYYY/MM/DD HH:mm:ss');
  return new Date(result);
};
```
そしてこの`convertFromJST`関数を大量のデータに対して一気に実行するのですが・・・**なんと移行後はめっっちゃ重い。** Moment.jsの時は一瞬待ったかな？くらいだったのに、画面が秒単位で止まってて何事！？ってなりました。
もはや明らかに使えないレベルです。単体の処理としては何も問題ないので簡単に移行できたね～😆とか思ってたので割と焦って転げ回りました。

とはいえ、まずは落ち着いて調べてみるしかありません。詳細にパフォーマンス調査を行った方が居たので、その記事を見てみると<ins>なんとこのDay.jsのタイムゾーン関連のメソッドが超遅い（Moment.jsの40倍くらい遅い）</ins>という結果が出ていました。
[moment.js・day.js よりも速くて軽い cdate ライブラリ #moment.js - Qiita](https://qiita.com/kawanet/items/7ce867f5b1ad187c2111#%E3%83%99%E3%83%B3%E3%83%81%E3%83%9E%E3%83%BC%E3%82%AF%E7%B5%90%E6%9E%9C)

とんでもない罠ですね。ちなみに、[この問題はDay.jsでもとっくに報告されている](https://github.com/iamkun/dayjs/issues/1236)のですが、2024年4月現在でも修正される気配がありませんでした。
ネット上でもこの問題に触れている人は少ないので、気づかずに移行してしまった人もいるのではないでしょうか…

# 解決策
とにかく、このまま放置できないので解決するしかありません。
ライブラリ移行によって起きた問題なので切り戻す＝Moment.jsへ戻すのが一つの手ですが、本末転倒感が拭えないので最終手段とします。
今回は上に紹介されていた記事にあった[cdate](https://www.npmjs.com/package/cdate)というライブラリが超早いとのことだったので、~~Day.jsの遅さを教えていただいた恩も込めて~~タイムゾーン周りの処理のみをこのcdateに任せることにしました。
```javascript
//******** 移行後(cdate) ********
const cdateJST = cdate().tz('Asia/Tokyo').cdateFn();

function convertFromJST (date) {
  const result = cdateJST(date).format('YYYY/MM/DD HH:mm:ss');
  return new Date(result);
};
```
この`cdateFn()`を使ってプリセットを用意できるのがイケてますね。
実際この変更後によって問題を解消することが出来ました。何ならMoment.jsよりも早くなって感激です。fastest! 🍺

# 学んだこと
#### ○移行先のライブラリの問題を事前に把握する
<ins>あまりにも</ins>当たり前のことですが、事前に移行先のライブラリは詳細に調査しなければならないです。特に不具合などについては。
今回は既に移行した記事をよく見かけたこと、Moment.jsの公式サイトで紹介されていることなどを信用して深く調べずにやってしまったのが良くなかったです。
しかも今回はパフォーマンスの問題だったので、実際に移行していながらも仕様的に気付かない方も多かったと思われ、文句を言っている方が少なかったのも事前に検知できなかった要因かもしれません。まあ言い訳にしかならないので注意ですね。

ちなみにDay.jsはその他にもタイムゾーン関連の処理にはバグ（`.tz()`と`.startOf()`を組み合わせるとおかしくなる等）があるようです。大丈夫か？

#### ○ライブラリの移行を行いやすい構造にする
これの利点を大きく感じられたのが個人的には収穫でした。
要は「ライブラリを参照する処理は個々のファイルに書かず、ひとまとめにしておく」ということです。例えばこんな感じ。
<details open>
<summary>date.js （長いので見ない場合は折り畳んでね）</summary>

```javascript
// ※掲載用に雑に記述しています

import dayjs from 'dayjs';
import plugin_minMax from 'dayjs/plugin/minMax.js';

/* dayjsのプラグイン追加 */
dayjs.extend(plugin_minMax);

/**
 * 日時の差分取得
 */
function diff (target, compare, unit = 'milliseconds') {
  let result = dayjs(target).diff(compare, unit);

  return result;
}

/**
 * 日時の一致判定
 */
function isSame (target, compare, unit = 'milliseconds') {
  let result = dayjs(target).isSame(compare, unit);

  return result;
}

/**
 * 日時の比較判定
 */
function isAfter (target, compare, unit = 'milliseconds') {
  let result = dayjs(target).isAfter(compare, unit);

  return result;
}

/**
 * 日時の比較判定
 */
function isBefore (target, compare, unit = 'milliseconds') {
  let result = dayjs(target).isBefore(compare, unit);

  return result;
}

/**
 * 最も遅い日時の取得
 */
function max (values) {
  let dayjsArray = values.map((value) => { return dayjs(value); });

  let result = dayjsArray.length > 0 ? dayjs.max(dayjsArray).toDate() : null;

  return result;
}

/**
 * 最も早い日時の取得
 */
function min (values) {
  let dayjsArray = values.map((value) => { return dayjs(value); });

  let result = dayjsArray.length > 0 ? dayjs.min(dayjsArray).toDate() : null;

  return result;
}

export default {
  diff,
  isSame,
  isAfter,
  isBefore,
  max,
  min,
};

```

</details>

他のファイルではこの関数をインポートして使う形になります。
もし今回のようにライブラリに問題があったら一つのファイルのみを変えればOK。もし個々に色んなファイルでライブラリを参照していたら、修正しようにも手が付けられないか地獄の手作業かの2択です。
また、ラッピングしてあることでテストが容易になるという良い副産物もあります。

今回もこの構造にしていたことで、Day.js→cdateへの変更もスムーズにできました。分かる方が見れば当然のことだと思いますが、改めて大切ですね。

# 終わりに
ということでMoment.jsからDay.jsに移行した際のお話でした。
時には避けて通れないライブラリ移行、今後もあると思うので今回の教訓を生かしていきます。
もしこれから移行する方は是非参考にしてもらえればと思います。
しかしこういうライブラリの最新化とか移行とか、リスクの割に機能追加とかではないので工数も下りにくくてしんどいなぁ…

# 余談
何となく色んな日付操作系ライブラリのインストール数を比較してみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3690817/e4439b46-5f0d-3b55-a3ad-9419529c0873.png)
https://npmtrends.com/cdate-vs-date-fns-vs-dayjs-vs-luxon-vs-moment

未だにMoment.jsがギリギリトップで、やはり移行していないPJも多数あるんだろうなあと。~~ぶっちゃけMoment.jsでも動くし~~ただ頭打ちで少しずつ減ってはいますね。
逆に顕著に増えているのがDay.jsで、これまでの2番手だったdate-fnsを驚異の追い上げで団子状態になっています。
Luxonはぼちぼちって感じですが、好みって人は見かけますね。こだわりのある方向けってイメージ？
cdateは個人の方が開発しているということもあって知名度が低いのか、非常に少ないですね。とても優秀なのですが・・・

何もかんもJavaScriptの`Date`が驚異の使いにくさを見せているのが悪いのですが、使いやすいライブラリが出てくれるのは嬉しいですね。
今後も気を付けながらライブラリを使っていくことにします。

[^1]:これ以上の機能改善・追加やバグ修正等のアップデートを行わない（セキュリティ上の問題等を除く）状態のこと。