mmntjs
Compatibility-first migration bridge away from moment.js

Performance

Performance should be explained, not advertised.

The goal of this page is to show what is measured, how it is measured, and how to interpret the numbers without treating microbenchmarks as universal truth.

Performance Philosophy

Performance is not one number either.

Just as bundle size should not be reduced to a single gzip number, runtime performance should not be reduced to a single microbenchmark result. Different environments optimise for different costs: hot-path throughput in the browser, startup latency in edge functions, parse time in SSR contexts, and dependency depth in server environments.

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.

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

Headline Results

Against moment.js

mmntjs wins all 31 benchmarked operations in the current benchmark set.

Against date-fns

mmntjs wins 24 of 25 benchmarked operations; the main loss is raw constructor-style creation.

Against native APIs

Some hot paths beat native Intl and Temporal in the current focused benchmarks, especially simple formatting and mutation-heavy operations.

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 How to read it
format YYYY-MM-DD 56 ns 1.31 us 420 ns Common fast-path formatting is substantially lower overhead than both date-fns and moment.js in this benchmark.
parse ISO string 363 ns 1.30 us 4.20 us Parsing is one of the clearest wins in the current microbenchmarks.
diff in days 20 ns 935 ns 491 ns Difference calculations benefit from a smaller internal path and cached fields.
add 1 second 15 ns 108 ns - Simple mutation-heavy arithmetic is a strong case for the current implementation.
startOf month 17 ns 75 ns - Boundary normalization is cheap in the common path benchmark.
moment() / new Date() 40 ns 35 ns 221 ns This is the main place where raw construction can still lose to a bare Date-style path because compatibility wrapping has a cost.

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.

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.

How To Interpret Results

  • These are microbenchmark results, not a guarantee about every application workload.
  • Some comparisons are implementation-cost comparisons rather than perfect semantic equivalents, especially around date-fns calendar-difference helpers.
  • Cold-start and warm-path behavior should be read separately when startup latency matters.
  • Compatibility remains the higher priority when a faster path would change legacy behavior.
  • Parse and eval cost are not captured by these microbenchmarks. They are tracked separately in the size and startup-cost measurements.
  • For SSR, edge, or serverless environments, startup cost and dependency footprint should be evaluated alongside hot-path throughput.

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.

  • test/bench.ts: moment.js vs mmntjs main table
  • test/bench-datefns2.ts: mmntjs vs date-fns comparisons
  • test/bench-regression.ts: Regression guard thresholds
  • test/bench-mem.ts: Memory footprint checks
  • test/bench-temporal.ts: Native Temporal comparison
  • bun run bench
  • bun run bench:guard
  • bun run bench:mem
  • bun test test/bench-temporal.ts

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.

Temporal Comparison

  • In the current Node.js 26 microbenchmarks, mmntjs wins 7 of 10 listed operations against native Temporal.
  • Temporal is faster in some parsing-oriented cases, while mmntjs tends to win in mutation-heavy and formatting-heavy paths.
  • This should be read as a trade-off note, not as an argument against Temporal's design goals.

That comparison is useful mainly as context. Temporal optimizes for a different design point, and this site should not frame the result as a universal ranking of libraries.

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.