Against moment.js
mmntjs wins every row in the current 14-row public moment comparison table.
Performance
mmntjs treats performance as a set of trade-offs that are measured, documented, and reproducible rather than advertised. The benchmark harness, methodology, and results are all in the repository so that reviewers can verify the claims against their own workloads.
Headline Results
mmntjs wins every row in the current 14-row public moment comparison table.
The object API wins 12 of 29 semantically aligned date-fns rows with a geometric mean of 1.45x (mmntjs faster overall); wins cluster in formatting, diff, and read-heavy paths, while fresh-object mutation often favors date-fns.
The standalone fns entry wins or ties every row against date-fns with a geometric mean of 1.78x and avoids the Moment-wrapper overhead entirely.
mmntjs wins diff, add, now, and offset reads by 1.4x-13.7x; Temporal wins ISO parsing and zoned boundary reads with its C++-backed implementation.
Relative gains were cross-validated on Node.js 26 so the story is not Bun-only.
Package size is measured across raw, compressed, bundled, and runtime layers. Runtime benchmarks address execution speed, not transfer or parse cost. The two are tracked on separate pages with separate methodology.
Representative Operations
| Operation | mmntjs | date-fns | moment.js | vs date-fns |
|---|---|---|---|---|
| diff in days | 22 ns | 850 ns | 421 ns | 38.6x |
| daysInMonth | 8 ns | 230 ns | 130 ns | 28.8x |
| format YYYY-MM-DD | 57 ns | 1.41 us | 395 ns | 24.7x |
| isBefore | 17 ns | 128 ns | 212 ns | 7.5x |
| parse ISO string | 275 ns | 1.02 us | 3.97 us | 3.7x |
| diff in months | 109 ns | 107 ns | 1.57 us | 1.0x |
| startOf month | 344 ns | 175 ns | 2.34 us | 0.5x |
| startOf day | 253 ns | 91 ns | 2.22 us | 0.4x |
| add 1 day | 296 ns | 99 ns | 2.34 us | 0.3x |
| moment() / new Date() | 147 ns | 35 ns | 260 ns | 0.2x |
These rows are deliberately representative rather than exhaustive. The intent is to show the shape of the current results while keeping the page honest about what microbenchmarks can and cannot prove.
ns = nanosecond, μs = microsecond (1,000 ns = 1 μs). Lower is faster. "vs date-fns" >1 means mmntjs is faster.
The losses are structural, not implementation gaps. Every moment() call constructs a wrapper object around a Date. That is the cost of preserving moment-compatible mutability, method chaining, and .fn/.prototype extensibility. date-fns operates on bare Date instances directly and avoids this overhead entirely.
Benchmark rows labeled [fresh] create a new instance per iteration. This amplifies the wrapper cost because mmntjs builds and destroys a wrapper object on every operation, while date-fns only allocates a plain Date. Real applications often reuse Moment objects across multiple operations, which amortizes the construction cost.
The standalone mmntjs/fns entry removes the wrapper layer entirely. It operates on plain Date objects and flips most of these losses to wins.
Temporal Comparison
Measured on Node.js 26 with native Temporal. Results shown as warm-path medians.
| Operation | mmntjs | Temporal | Ratio |
|---|---|---|---|
| diff in days | 27 ns | 373 ns | 13.7x |
| get offset string (zoned) | 24 ns | 82 ns | 3.4x |
| now/create | 230 ns | 673 ns | 2.9x |
| add 1 day | 238 ns | 468 ns | 2.0x |
| add 1 day (zoned) | 372 ns | 606 ns | 1.6x |
| toISOString | 249 ns | 338 ns | 1.4x |
| parse ISO to zoned | 399 ns | 351 ns | 0.9x |
| set month+day | 304 ns | 258 ns | 0.8x |
| startOf day (zoned) | 468 ns | 297 ns | 0.6x |
| parse date-only ISO | 394 ns | 177 ns | 0.4x |
"Ratio" >1 means mmntjs is faster. <1 means Temporal is faster. Data from bench/temporal.ts on Node.js 26. Mutating operations use fresh objects per iteration (same methodology as the date-fns table above).
Temporal prioritizes immutable correctness over speed. mmntjs wins on mutation-heavy and diff-heavy paths where cached fields and mutability avoid repeated allocation.
Standalone Helpers
mmntjs/fns is the closer apples-to-apples comparison with date-fns.
Removing the Moment wrapper makes the comparison essentially lossless for mmntjs:
mmntjs/fns wins or ties every row against date-fns, with a
geometric mean of 1.78x. The few rows near 1.0x are bare Date.set*
wrappers where both implementations make the same native call.
| Operation | mmntjs/fns | date-fns | vs date-fns |
|---|---|---|---|
| differenceInDays | 47 ns | 1.10 us | 23.3x |
| dayOfYear | 70 ns | 1.11 us | 15.9x |
| format YYYY-MM-DD | 183 ns | 1.26 us | 6.9x |
| daysInMonth | 33 ns | 231 ns | 7.0x |
| setMonth | 80 ns | 446 ns | 5.6x |
| parse ISO string | 322 ns | 1.12 us | 3.5x |
| startOfMonth | 40 ns | 101 ns | 2.5x |
| setMinutes | 42 ns | 59 ns | 1.4x |
| setHours | 50 ns | 61 ns | 1.2x |
| setMilliseconds | 47 ns | 58 ns | 1.2x |
"vs date-fns" >1 means fns is faster. Rows at 1.0x are within measurement noise.
The bundle story is also different: a single format import measures about 507 B gzip,
and a small parseISO + format + addDays bundle measures about 1.3 KB gzip.
Cross-Runtime Note
The performance docs explicitly validate key rows on Node.js 26 as well as Bun. That is important because absolute numbers move with the engine, but the useful migration claim is whether the relative advantage stays intact across runtimes teams actually deploy.
Why It Is Fast
The speedups are not from one trick. They mostly come from doing less work on hot paths while preserving moment.js semantics: fewer allocations, less repeated Date work, less regex work, and narrower paths for common parsing, formatting, and arithmetic cases.
Moment instances keep decomposed fields like year, month, day, hour, minute, and millisecond directly on the object so common getters and formatters avoid repeated Date API calls.
Construction defers field refresh work behind a dirty flag, so short-lived moments do not pay for decomposed fields until a getter, formatter, or calendar operation actually needs them.
Common ISO inputs are classified with charCodeAt and tiny digit helpers before falling back to broader regex-based parsing paths.
UTC month, year, startOf, and endOf paths use integer calendar helpers instead of bouncing through Date allocations where compatibility allows it.
Hot English formats such as YYYY-MM-DD and HH:mm:ss bypass the general token interpreter and format from cached fields directly.
Invalid-state and parse-debug metadata are kept away from the normal valid-moment path, preserving simpler object layout and cheaper validity checks.
Workloads
High-impact for migration because parsing differences break behavior silently. Performance wins matter only after semantic equivalence is checked.
Formatting is both user-visible and frequent. The common token fast paths are where mmntjs currently shows some of its strongest results.
Mutation-heavy date arithmetic is one of the areas where a moment-compatible mutable design can stay efficient.
Reporting and aggregation code often leans on these operations. They should be measured because they appear in hot loops and ETL-style transforms.
This matters for comparisons, reporting windows, and business logic. Benchmarks should note when compared APIs do not expose identical semantics.
Duration construction and math affect both app code and humanized display flows.
Locale-sensitive formatting is important because fast English output is not the whole story.
Real workloads often map over large arrays of timestamps rather than calling one function once.
Principles
Methodology
How To Interpret Results
The metric that matters depends on your deployment environment:
| Scenario | Most relevant factors |
|---|---|
| CDN script tag | Compressed transfer size (gzip/brotli), minified output |
| SPA or browser app | Bundled output after tree-shaking, parse/eval cost |
| SSR or edge runtime | Startup cost, cold start, dependency footprint |
| Node backend | Initialization time, total dependency depth |
These microbenchmarks measure hot-path throughput, which is one dimension of performance. Compressed transfer, parse time, and cold-start behavior are reported separately.
Reproducibility
A performance page for this project should make it easy for a skeptical engineer to rerun the numbers rather than trust a screenshot. The benchmark files live in the repository and are intended to be rerun locally with documented commands.
bench/moment-compat.ts: moment.js vs mmntjs public table and appendixbench/date-fns.ts: object-API comparison against date-fnsbench/fns.ts: standalone mmntjs/fns vs date-fns helpersbench/bench-regression.ts: Regression guard thresholdsbench/bench-mem.ts: Memory footprint checksbench/temporal.ts: Native Temporal comparisonbun run benchbun run bench:date-fnsbun bench/fns.tsbun run bench:guardbun run bench:membun run bench:temporal
The benchmark source of truth is split between README.md and
docs/perf/BENCHMARKS.md. This page should track those numbers rather than invent
a separate performance story.
Package Size
Runtime speed and package size answer different questions. The package-size page reports raw, minified, gzip, brotli, and bundled output sizes, along with scenario-based guidance for which metric matters in each deployment context. Compressed transfer size (gzip/brotli) is treated as one layer of the measurement, not the headline.
Key points for mmntjs specifically: core does not silently bundle timezone data, locale and timezone data are measured and reported separately, and the lite/full/temporal/timezone entry points have distinct size profiles. Tree-shaking and side-effect behavior are preserved so bundlers can remove unused paths.