いまさらdate-fnsとdayjsを比較
評価基準
以下の2つです。書き味は好みによるので比べません。(筆者はdate-fnsが好き)
- webpackによるbundleサイズ
- node@22.14.0環境での実行速度
評価対象
- format
- parse
- add
- date-fnsがdayjsと同じ機能量になるようにしたときの比較
留意事項
format
関数 は使うべきではない(個人的見解)
date-fnsの 後述しますが、date-fnsの format
のbundleサイズは大きいです。
大抵の場合は lightFormat
で十分なので format
は使わなくていいと思います。
後述の内容を一部抜粋するとdayjsのformat、date-fnsのformat、date-fnsのlightFormatのbundleサイズと実行速度の比較結果を載せます。
dayjs(format) | date-fns(format) | date-fns(lightFormat) | |
---|---|---|---|
bundleサイズ(webpack) | 2.94KB | 5.37KB | 886B |
実行速度 | 2.52 µs/ite | 3.21 µs/iter | 1.58 µs/iter |
dayjsはappendixにあるようにいくつかのメソッドはtreeshakingが効かずに強制的に同梱されてしまうのですが、date-fnsの format
はそれ単体でそのdayjsと張り合えるほどの大きさがあります。
また、実行速度も lightFormat
と比べると倍遅いです。
format
ではなく lightFormat
を使うようにしましょう。
lightFormat
はlocaleに対応してませんが、それは intlFormat
でいいと思います。
ベンチマークの値
mitataのQuick Startのままで、調整したコードではないです。
GCの記載がmitataにありましたが未タッチです
評価(format)
date-fnsは複数のフォーマット用関数があるのでいくつか比較に採用してます。
採用理由
- format
- 重くて遅いので
- lightFormat
- 軽くて速いので
- intlFormat
dayjs(format) | date-fns(format) | date-fns(lightFormat) | date-fns(intlFormat) | |
---|---|---|---|---|
bundleサイズ | 2.94KB | 5.37KB | 886B | 765B |
実行速度 | 2.52 µs/ite | 3.21 µs/iter | 1.58 µs/iter | 39.04 µs/iter |
考察
- date-fns(intlFormat)はめちゃくちゃ遅いですね。
- localeを指定できないせいか、dayjs(format)よりdate-fns(lightFormat)の方が速いですね
- やはりdate-fns(format)は大きい、そして速くない
評価(parse)
dayjs("2022-01-01T00:00:00.000+09:00")
と parseISO("2022-01-01T00:00:00.000+09:00")
とついでに new Date("2022-01-01T00:00:00.000+09:00")
の比較をしました。
dayjs | date-fns | new Date | |
---|---|---|---|
bundleサイズ | 2.94KB | 1.3KB | 94B |
実行速度 | 1.43 µs/iter | 1.17 µs/iter | 228.22 ns/iter |
考察
- bundleサイズは必然的にdate-fnsの方が小さいです。
- dayjsのコンストラクタはstring以外も取れるせいかdate-fnsと比べて遅いですね。(とはいえほとんど変わりませんが)
- ISOをparseしたいならnew Dateがいいです。
評価(add)
dayjsの add
はミリ秒から年まで(Quarterはプラグインが必要なので除く)に対応しているため、date-fnsの方もミリ秒から年の関数を使って各単位で1を足す処理を比較しました。
dayjs | date-fns | |
---|---|---|
bundleサイズ | 2.94KB | 621B |
実行速度 | 7.86 µs/iter | 1.58 µs/iter |
考察
- bundleサイズは依然date-fnsの方が小さいです。
- 実行速度はやはり単位によって条件分岐してるせいかdayjsの方が遅いですね。
評価(date-fnsがdayjsと同じ機能量になるようにしたときの比較)
dayjsに強制的に同梱されているメソッドをdate-fnsで同様の関数を使って実装します。
いくつかdayjsにあってdate-fnsにないものがあったので、対応を以下にまとめました。
- valueOfはdate-fnsにないので
Date.valueOf
で代用 - utcOffsetは実装を見ると
Date.getTimezoneOffset
だったので、それで代用 - localeはdate-fnsで対応する場合
format
を使う必要があり、bundleサイズの面でdate-fnsの負け確になって面白くないので割愛 - toDateは実装を見ると
new Date(this.valueOf())
だったので、それで代用 - toJSONは実装を見るとほぼ
Date.toISOString
だったのでformatISO
で対応 - toStringは実装を見ると
Date.toUTCString
だったので、それで代用
※代用は「bundleサイズ的にdate-fnsが有利になるが実行速度比較用に一応実行する」ことを意味しています。
※対応は「bundleサイズ、実行速度ともに十分比較として意味を持つと考えられる」ことを意味しています。
dayjs | date-fns | |
---|---|---|
bundleサイズ | 2.94KB | 4.49KB |
実行速度 | 89.20 µs/iter | 22.66 µs/iter |
考察
- bundleサイズはdayjsの方が少ないです。
- build後のコードを見るとdate-fnsは割と人間に読みやすい文字列が残っていたのに対し、dayjsはmanglingされて読みづらいコードになってました。
- 実行速度はdate-fnsの方が速いです。
- おそらくdayjsはbundleサイズを減らすための工夫を実行速度を犠牲にして行っているのだと思います。
結論
実行速度を数μsでも削りたい人はdate-fns。
bundleサイズを数KBでも削りたい人は、日付操作が多いならdayjs、そうでもないならdate-fnsかなと思います。
どちらでもない人は好みで選ぶとよさそうです。
Appendix
date-fnsをnamespaceインポート
date-fnsを個別にインポートするのも面倒だと思うのでnamespaceしたい人が多いと思います。
そういう方には以下がおすすめです。
dayjsのtreeshakingが効かないメソッド
- parse
- init
- isSame
- isAfter
- isBefore
- unix
- valueOf
- startOf
- endOf
- set
- get
- add
- subtract
- format
- utcOffset
- diff
- daysInMonth
- locale
- clone
- toDate
- toJSON
- toISOString
- toString
参考(npmパッケージに含まれているdayjs.min.js)
function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs=e()}
this,
() => {
var t = 1e3,
e = 6e4,
n = 36e5,
r = "millisecond",
i = "second",
s = "minute",
u = "hour",
a = "day",
o = "week",
c = "month",
f = "quarter",
h = "year",
d = "date",
l = "Invalid Date",
$ =
/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,
y =
/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,
M = {
name: "en",
weekdays:
"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
months:
"January_February_March_April_May_June_July_August_September_October_November_December".split(
"_",
),
ordinal: (t) => {
var e = ["th", "st", "nd", "rd"],
n = t % 100;
return "[" + t + (e[(n - 20) % 10] || e[n] || e[0]) + "]";
},
},
m = (t, e, n) => {
var r = String(t);
return !r || r.length >= e
? t
: "" + Array(e + 1 - r.length).join(n) + t;
},
v = {
s: m,
z: (t) => {
var e = -t.utcOffset(),
n = Math.abs(e),
r = Math.floor(n / 60),
i = n % 60;
return (e <= 0 ? "+" : "-") + m(r, 2, "0") + ":" + m(i, 2, "0");
},
m: function t(e, n) {
if (e.date() < n.date()) return -t(n, e);
var r = 12 * (n.year() - e.year()) + (n.month() - e.month()),
i = e.clone().add(r, c),
s = n - i < 0,
u = e.clone().add(r + (s ? -1 : 1), c);
return +(-(r + (n - i) / (s ? i - u : u - i)) || 0);
},
a: (t) => (t < 0 ? Math.ceil(t) || 0 : Math.floor(t)),
p: (t) =>
({ M: c, y: h, w: o, d: a, D: d, h: u, m: s, s: i, ms: r, Q: f })[
t
] ||
String(t || "")
.toLowerCase()
.replace(/s$/, ""),
u: (t) => void 0 === t,
},
g = "en",
D = {};
D[g] = M;
var p = "$isDayjsObject",
S = (t) => t instanceof _ || !(!t || !t[p]),
w = function t(e, n, r) {
var i;
if (!e) return g;
if ("string" == typeof e) {
var s = e.toLowerCase();
D[s] && (i = s), n && ((D[s] = n), (i = s));
var u = e.split("-");
if (!i && u.length > 1) return t(u[0]);
} else {
var a = e.name;
(D[a] = e), (i = a);
}
return !r && i && (g = i), i || (!r && g);
},
O = (t, e) => {
if (S(t)) return t.clone();
var n = "object" == typeof e ? e : {};
return (n.date = t), (n.args = arguments), new _(n);
},
b = v;
(b.l = w),
(b.i = S),
(b.w = (t, e) =>
O(t, { locale: e.$L, utc: e.$u, x: e.$x, $offset: e.$offset }));
var _ = (() => {
function M(t) {
(this.$L = w(t.locale, null, !0)),
this.parse(t),
(this.$x = this.$x || t.x || {}),
(this[p] = !0);
}
var m = M.prototype;
return (
(m.parse = function (t) {
(this.$d = ((t) => {
var e = t.date,
n = t.utc;
if (null === e) return new Date(Number.NaN);
if (b.u(e)) return new Date();
if (e instanceof Date) return new Date(e);
if ("string" == typeof e && !/Z$/i.test(e)) {
var r = e.match($);
if (r) {
var i = r[2] - 1 || 0,
s = (r[7] || "0").substring(0, 3);
return n
? new Date(
Date.UTC(
r[1],
i,
r[3] || 1,
r[4] || 0,
r[5] || 0,
r[6] || 0,
s,
),
)
: new Date(
r[1],
i,
r[3] || 1,
r[4] || 0,
r[5] || 0,
r[6] || 0,
s,
);
}
}
return new Date(e);
})(t)),
this.init();
}),
(m.init = function () {
var t = this.$d;
(this.$y = t.getFullYear()),
(this.$M = t.getMonth()),
(this.$D = t.getDate()),
(this.$W = t.getDay()),
(this.$H = t.getHours()),
(this.$m = t.getMinutes()),
(this.$s = t.getSeconds()),
(this.$ms = t.getMilliseconds());
}),
(m.$utils = () => b),
(m.isValid = function () {
return !(this.$d.toString() === l);
}),
(m.isSame = function (t, e) {
var n = O(t);
return this.startOf(e) <= n && n <= this.endOf(e);
}),
(m.isAfter = function (t, e) {
return O(t) < this.startOf(e);
}),
(m.isBefore = function (t, e) {
return this.endOf(e) < O(t);
}),
(m.$g = function (t, e, n) {
return b.u(t) ? this[e] : this.set(n, t);
}),
(m.unix = function () {
return Math.floor(this.valueOf() / 1e3);
}),
(m.valueOf = function () {
return this.$d.getTime();
}),
(m.startOf = function (t, e) {
var r = !!b.u(e) || e,
f = b.p(t),
l = (t, e) => {
var i = b.w(
this.$u ? Date.UTC(this.$y, e, t) : new Date(this.$y, e, t),
this,
);
return r ? i : i.endOf(a);
},
$ = (t, e) =>
b.w(
this.toDate()[t].apply(
this.toDate("s"),
(r ? [0, 0, 0, 0] : [23, 59, 59, 999]).slice(e),
),
this,
),
y = this.$W,
M = this.$M,
m = this.$D,
v = "set" + (this.$u ? "UTC" : "");
switch (f) {
case h:
return r ? l(1, 0) : l(31, 11);
case c:
return r ? l(1, M) : l(0, M + 1);
case o:
var g = this.$locale().weekStart || 0,
D = (y < g ? y + 7 : y) - g;
return l(r ? m - D : m + (6 - D), M);
case a:
case d:
return $(v + "Hours", 0);
case u:
return $(v + "Minutes", 1);
case s:
return $(v + "Seconds", 2);
case i:
return $(v + "Milliseconds", 3);
default:
return this.clone();
}
}),
(m.endOf = function (t) {
return this.startOf(t, !1);
}),
(m.$set = function (t, e) {
var n,
o = b.p(t),
f = "set" + (this.$u ? "UTC" : ""),
l = ((n = {}),
(n[a] = f + "Date"),
(n[d] = f + "Date"),
(n[c] = f + "Month"),
(n[h] = f + "FullYear"),
(n[u] = f + "Hours"),
(n[s] = f + "Minutes"),
(n[i] = f + "Seconds"),
(n[r] = f + "Milliseconds"),
n)[o],
$ = o === a ? this.$D + (e - this.$W) : e;
if (o === c || o === h) {
var y = this.clone().set(d, 1);
y.$d[l]($),
y.init(),
(this.$d = y.set(d, Math.min(this.$D, y.daysInMonth())).$d);
} else l && this.$d[l]($);
return this.init(), this;
}),
(m.set = function (t, e) {
return this.clone().$set(t, e);
}),
(m.get = function (t) {
return this[b.p(t)]();
}),
(m.add = function (r, f) {
var d;
r = Number(r);
var $ = b.p(f),
y = (t) => {
var e = O(this);
return b.w(e.date(e.date() + Math.round(t * r)), this);
};
if ($ === c) return this.set(c, this.$M + r);
if ($ === h) return this.set(h, this.$y + r);
if ($ === a) return y(1);
if ($ === o) return y(7);
var M = ((d = {}), (d[s] = e), (d[u] = n), (d[i] = t), d)[$] || 1,
m = this.$d.getTime() + r * M;
return b.w(m, this);
}),
(m.subtract = function (t, e) {
return this.add(-1 * t, e);
}),
(m.format = function (t) {
var n = this.$locale();
if (!this.isValid()) return n.invalidDate || l;
var r = t || "YYYY-MM-DDTHH:mm:ssZ",
i = b.z(this),
s = this.$H,
u = this.$m,
a = this.$M,
o = n.weekdays,
c = n.months,
f = n.meridiem,
h = (t, n, i, s) =>
(t && (t[n] || t(this, r))) || i[n].slice(0, s),
d = (t) => b.s(s % 12 || 12, t, "0"),
$ =
f ||
((t, e, n) => {
var r = t < 12 ? "AM" : "PM";
return n ? r.toLowerCase() : r;
});
return r.replace(
y,
(t, r) =>
r ||
((t) => {
switch (t) {
case "YY":
return String(this.$y).slice(-2);
case "YYYY":
return b.s(this.$y, 4, "0");
case "M":
return a + 1;
case "MM":
return b.s(a + 1, 2, "0");
case "MMM":
return h(n.monthsShort, a, c, 3);
case "MMMM":
return h(c, a);
case "D":
return this.$D;
case "DD":
return b.s(this.$D, 2, "0");
case "d":
return String(this.$W);
case "dd":
return h(n.weekdaysMin, this.$W, o, 2);
case "ddd":
return h(n.weekdaysShort, this.$W, o, 3);
case "dddd":
return o[this.$W];
case "H":
return String(s);
case "HH":
return b.s(s, 2, "0");
case "h":
return d(1);
case "hh":
return d(2);
case "a":
return $(s, u, !0);
case "A":
return $(s, u, !1);
case "m":
return String(u);
case "mm":
return b.s(u, 2, "0");
case "s":
return String(this.$s);
case "ss":
return b.s(this.$s, 2, "0");
case "SSS":
return b.s(this.$ms, 3, "0");
case "Z":
return i;
}
return null;
})(t) ||
i.replace(":", ""),
);
}),
(m.utcOffset = function () {
return 15 * -Math.round(this.$d.getTimezoneOffset() / 15);
}),
(m.diff = function (r, d, l) {
var $,
M = b.p(d),
m = O(r),
v = (m.utcOffset() - this.utcOffset()) * e,
g = this - m,
D = () => b.m(this, m);
switch (M) {
case h:
$ = D() / 12;
break;
case c:
$ = D();
break;
case f:
$ = D() / 3;
break;
case o:
$ = (g - v) / 6048e5;
break;
case a:
$ = (g - v) / 864e5;
break;
case u:
$ = g / n;
break;
case s:
$ = g / e;
break;
case i:
$ = g / t;
break;
default:
$ = g;
}
return l ? $ : b.a($);
}),
(m.daysInMonth = function () {
return this.endOf(c).$D;
}),
(m.$locale = function () {
return D[this.$L];
}),
(m.locale = function (t, e) {
if (!t) return this.$L;
var n = this.clone(),
r = w(t, e, !0);
return r && (n.$L = r), n;
}),
(m.clone = function () {
return b.w(this.$d, this);
}),
(m.toDate = function () {
return new Date(this.valueOf());
}),
(m.toJSON = function () {
return this.isValid() ? this.toISOString() : null;
}),
(m.toISOString = function () {
return this.$d.toISOString();
}),
(m.toString = function () {
return this.$d.toUTCString();
}),
M
);
})(),
k = _.prototype;
return (
(O.prototype = k),
[
["$ms", r],
["$s", i],
["$m", s],
["$H", u],
["$W", a],
["$M", c],
["$y", h],
["$D", d],
].forEach((t) => {
k[t[1]] = function (e) {
return this.$g(e, t[0], t[1]);
};
}),
(O.extend = (t, e) => (t.$i || (t(e, _, O), (t.$i = !0)), O)),
(O.locale = w),
(O.isDayjs = S),
(O.unix = (t) => O(1e3 * t)),
(O.en = D[g]),
(O.Ls = D),
(O.p = {}),
O
);
};
```
Discussion