mmntjs
Compatibility-first migration bridge away from moment.js

Headline Results

Against moment.js

mmntjs wins every row in the current 14-row public moment comparison table.

Against date-fns

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.

Against date-fns with `mmntjs/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.

Against Temporal

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.

Cross-runtime check

Relative gains were cross-validated on Node.js 26 so the story is not Bun-only.

Size and speed are separate concerns

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.

Why mmntjs loses some date-fns rows

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 days27 ns373 ns13.7x
get offset string (zoned)24 ns82 ns3.4x
now/create230 ns673 ns2.9x
add 1 day238 ns468 ns2.0x
add 1 day (zoned)372 ns606 ns1.6x
toISOString249 ns338 ns1.4x
parse ISO to zoned399 ns351 ns0.9x
set month+day304 ns258 ns0.8x
startOf day (zoned)468 ns297 ns0.6x
parse date-only ISO394 ns177 ns0.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
differenceInDays47 ns1.10 us23.3x
dayOfYear70 ns1.11 us15.9x
format YYYY-MM-DD183 ns1.26 us6.9x
daysInMonth33 ns231 ns7.0x
setMonth80 ns446 ns5.6x
parse ISO string322 ns1.12 us3.5x
startOfMonth40 ns101 ns2.5x
setMinutes42 ns59 ns1.4x
setHours50 ns61 ns1.2x
setMilliseconds47 ns58 ns1.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 implementation specializes the common compatibility paths.

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.

Cached date fields

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.

Lazy field materialization

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.

Digit parsers before regex

Common ISO inputs are classified with charCodeAt and tiny digit helpers before falling back to broader regex-based parsing paths.

Direct UTC arithmetic

UTC month, year, startOf, and endOf paths use integer calendar helpers instead of bouncing through Date allocations where compatibility allows it.

Common format fast paths

Hot English formats such as YYYY-MM-DD and HH:mm:ss bypass the general token interpreter and format from cached fields directly.

Cold-path separation

Invalid-state and parse-debug metadata are kept away from the normal valid-moment path, preserving simpler object layout and cheaper validity checks.

Workloads

Parse

High-impact for migration because parsing differences break behavior silently. Performance wins matter only after semantic equivalence is checked.

Format

Formatting is both user-visible and frequent. The common token fast paths are where mmntjs currently shows some of its strongest results.

Add and subtract

Mutation-heavy date arithmetic is one of the areas where a moment-compatible mutable design can stay efficient.

startOf and endOf

Reporting and aggregation code often leans on these operations. They should be measured because they appear in hot loops and ETL-style transforms.

diff

This matters for comparisons, reporting windows, and business logic. Benchmarks should note when compared APIs do not expose identical semantics.

Duration

Duration construction and math affect both app code and humanized display flows.

Locale formatting

Locale-sensitive formatting is important because fast English output is not the whole story.

Bulk transformations

Real workloads often map over large arrays of timestamps rather than calling one function once.

Principles

  • Performance claims should be reproducible, scoped, and benchmark-specific.
  • Compatibility matters more than microbenchmark wins in ambiguous behavior.
  • Common-path overhead matters more than leaderboard language.
  • Bundle size is measured at multiple layers — raw, minified, gzip, brotli, bundled, parsed, evaluated — not reduced to a single number.

Methodology

CPU
Apple M4 performance core
OS
macOS arm64
Default runtime
Bun 1.x, with cross-runtime checks on Node.js 26
Harness
process.hrtime.bigint()
Warm measurement
Median of 5 runs x 5000 iterations after 1000-iteration warmup
Cold measurement
First call from module load to capture startup-tier behavior
Dead-code protection
Benchmark outputs are consumed so optimized-away work is not counted
Size measurement
Raw, minified, gzip, brotli, and bundled outputs are measured in a separate size script (bun run size)
Parse and eval cost
Not included in these microbenchmarks; measured separately in the bundle and startup instrumentation

How To Interpret Results

Scenario-based reading

The metric that matters depends on your deployment environment:

Scenario Most relevant factors
CDN script tagCompressed transfer size (gzip/brotli), minified output
SPA or browser appBundled output after tree-shaking, parse/eval cost
SSR or edge runtimeStartup cost, cold start, dependency footprint
Node backendInitialization 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.

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

Size is measured at multiple layers, not a single gzip number.

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.