🐥

いまさらdate-fnsとdayjsを比較

2025/03/03に公開

評価基準

以下の2つです。書き味は好みによるので比べません。(筆者はdate-fnsが好き)

  • webpackによるbundleサイズ
  • node@22.14.0環境での実行速度

評価対象

  • format
  • parse
  • add
  • date-fnsがdayjsと同じ機能量になるようにしたときの比較

留意事項

date-fnsの format 関数 は使うべきではない(個人的見解)

後述しますが、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は複数のフォーマット用関数があるのでいくつか比較に採用してます。

採用理由

https://github.com/tentaShiratori/date-fns-vs-dayjs/blob/main/bench/format.ts

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") の比較をしました。

https://github.com/tentaShiratori/date-fns-vs-dayjs/blob/main/bench/parse.ts

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を足す処理を比較しました。

https://github.com/tentaShiratori/date-fns-vs-dayjs/blob/main/bench/add.ts

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サイズ、実行速度ともに十分比較として意味を持つと考えられる」ことを意味しています。

https://github.com/tentaShiratori/date-fns-vs-dayjs/blob/main/bench/allfeature.ts

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したい人が多いと思います。

そういう方には以下がおすすめです。

https://qiita.com/uhyo/items/842e51e0d8cc46856d04

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