Most teams don't keep using moment.js because they love it. They keep using it because rewriting date logic across a large codebase is risky, expensive, and never becomes this quarter's priority.
mmntjs is designed for that reality.
Drop-in replacement for moment.js — migration path to Temporal API.
Website: mmntjs.veritycost.com
Replace moment.js with no code changes:
npm install moment@npm:mmntjsimport moment from "moment"; // unchanged
moment("2024-01-01").add(1, "month"); // now runs on mmntjsSwap imports from moment to mmntjs across your codebase:
npx mmntjs migrate --check ./src # dry run first
npx mmntjs migrate --apply ./src # apply codemodMigrate file by file — moment and mmntjs can coexist in the same project:
npm install mmntjs// file-a.js — not yet migrated
import moment from "moment"; // still on moment.js
// file-b.js — migrated
import moment from "mmntjs"; // now on mmntjsThis works because the two runtimes don't conflict — you can move module by module at your own pace. No need for a big-bang switch.
Tip: Start with
mmntjs audit ./srcto identify which files use only compatible APIs. Those are your safest first candidates.
npm install mmntjsimport moment from "mmntjs";
moment().format("YYYY-MM-DD");
moment.duration(2, "hours").humanize();| Import | gzip (bundled) | gzip (dist) | Description |
|---|---|---|---|
mmntjs |
45.1 KB | 63.4 KB | Full compatibility (default) — core + display + utc + locale registry + format-parse |
mmntjs/lite |
14.8 KB | 20.5 KB | ISO-centric, size-first — core-lite + strict parsing, no locale registry, no display extras |
mmntjs/full |
45.1 KB | 63.4 KB | Same as default — explicit alias |
mmntjs/fns |
0.5-1.3 KB* | 6.8 KB | Tree-shakeable Date helpers — single format is 507 B gzip, parseISO+format+addDays is 1.3 KB gzip |
mmntjs/temporal |
46.7 KB | 102.6 KB | Temporal bridge — toTemporal(m) / fromTemporal(t) |
mmntjs/plugin/* |
— | +separate | Optional plugins (utc, format-parse) — self-contained, add features to lite |
mmntjs/locale/* |
— | +1-5 KB | Individual locales (136 total) — tree-shakeable, each <2 KB gzip |
mmntjs-timezone |
81.1 KB | 39.0 KB | Separate package — full IANA timezone data + installTimezone(moment) |
bundled: measured from source with
Bun.build({minify:true, target:"browser"})— represents what consumer bundlers produce. dist: raw tsup output withsplitting:false— self-contained files, some code duplication across entries is expected.*mmntjs/fnsis fully tree-shakeable, so its bundled size depends on what you import.
// Use lite + plugins for smaller bundles
import moment from "mmntjs/lite";
import "mmntjs/plugin/format-parse";
import "mmntjs/locale-auto/ja";
import "mmntjs-timezone";If you want the smallest locale import with explicit registration instead of side effects:
import moment from "mmntjs";
import { jaLocale } from "mmntjs/locale/ja";
moment.locale("ja", jaLocale);| Feature | lite |
default |
full |
temporal |
|---|---|---|---|---|
moment() / format / parse (ISO) |
✅ | ✅ | ✅ | — |
| Strict ISO 8601 parsing | ✅ | ✅ | ✅ | — |
.add() / .subtract() / .startOf() / .endOf() |
✅ | ✅ | ✅ | — |
.diff() / .from() / .to() |
✅ | ✅ | ✅ | — |
UTC mode (moment.utc() / .utc() / .local()) |
✅ | ✅ | ✅ | — |
Locale registry (moment.locale() / defineLocale()) |
— | ✅ | ✅ | — |
Custom format parsing (moment("…", "YYYY-MM-DD")) |
via plugin | ✅ | ✅ | — |
.format("LLL") locale-aware |
via plugin | ✅ | ✅ | — |
moment.duration() |
via plugin | ✅ | ✅ | — |
moment.min() / moment.max() / moment.parseZone() |
— | ✅ | ✅ | — |
toTemporal(m) / fromTemporal(t) |
— | — | — | ✅ |
CLI (mmntjs migrate / mmntjs audit) |
— | — | — | — |
liteincludes.format("YYYY-MM-DD")(basic format tokens) and ISO parsing. Addformat-parseplugin for custom format strings and locale-aware long date formats.
- Timezone is fully opt-in:
mmntjs-timezoneis a separate package. Core bundles (lite,default,full) contain zero timezone resolution code — noIntl.DateTimeFormatreferences. - Temporal is opt-in:
mmntjs/temporalis the only entry that exportstoTemporal/fromTemporal. Neitherlitenordefaultpulls@js-temporal/polyfill. - Locales are tree-shakeable: Each locale is a standalone module. Importing
mmntjs/locale/jagives you pure locale data; importingmmntjs/locale-auto/jaauto-registers that locale for migration convenience. - CLI is separate: The
mmntjsCLI binary usesdist/bin/cli.js; none of the library entry points contain CLI code. - Side effects: Core entry initialization (
moment()setup) always runs on import.plugin/*andlocale-auto/*submodules are marked as side-effectful for bundler safety.locale/*export pure data and are fully tree-shakeable.
| Runtime | Support |
|---|---|
| Node.js | 16+ (CJS require("mmntjs"), ESM) |
| Browser | IIFE via CDN (<script src="…/mmntjs.min.js">) |
| Bun | Native ESM, first-class support |
| Deno | Compatible via npm specifiers |
<script src="https://cdn.jsdelivr.net/npm/mmntjs/dist/mmntjs.min.js"></script>
<script>
mmntjs().format("LLLL");
</script>// Node.js CJS
const moment = require("mmntjs");moment.js's own test suite: 630/630 pass (52 QUnit files via compat layer).
Oracle comparison: 112 properties, 45k+ assertions against upstream moment.js.
Mutation: 48/48 killed in the current curated mutation suite.
Fuzzing: 11 coverage-guided harnesses + grammar-based ISO 8601 generator.
The only known incompatibilities are malformed/edge-case strings discovered through fuzzing (e.g. sign-prefixed strings without delimiters). These are under active repair — see REMAINING.md for the shortlist.
Compatibility story: Fuzzer found
moment("0000 03")— both engines must produce2000-03-01(year 2000, March 1). Our ISO table parser was matching"0000 03"as year-0000 month-03, a wrong answer. Rather than adding more regex special cases, we introduced a_claimedsentinel: when the table parser finds a low-confidence match, it returns_claimed: trueto delegate to JavaScript's nativenew Date(str)— exactly what moment.js does as its last resort. This single mechanism closed 7+ fuzz-discovered gaps without adding parser complexity.
136 locales, timezone, duration, calendar, custom format parse — all existing moment.js API surface covered.
Runs on Node 16+, browsers (IIFE/CDN), Bun, and Deno. CJS and ESM both supported.
TypeScript types included — import moment from "moment" resolves to mmntjs's types automatically. No @types/moment needed.
| Entry | gzip (bundled) | Notes |
|---|---|---|
mmntjs (full) |
45.1 KB | Full compatibility entry for migration |
mmntjs/lite |
14.8 KB | Recommended default when you do not need the full surface |
mmntjs/fns |
507 B - 1.3 KB | Single helpers stay tiny; size scales with imports |
mmntjs-timezone |
81.1 KB | Separate package; named-zone data is opt-in |
lite drops locale registry, Temporal bridge, custom format parse, and marginal APIs — add them back via plugins only when needed.
In the current public moment comparison table, mmntjs wins every tracked row. Against date-fns, the picture is mixed: the object-oriented mmntjs API wins read-heavy rows, while date-fns often wins fresh-object mutation rows. The standalone mmntjs/fns entry closes much of that gap and wins 19 of 26 direct Date-helper comparisons.
| Operation | mmntjs | date-fns | vs moment.js |
|---|---|---|---|
| format YYYY-MM-DD | 57 ns | 1.41 us (24.7x) | 395 ns (8.2x) |
| parse ISO string | 275 ns | 1.02 us (3.7x) | 3.97 us (17.9x) |
| diff in days | 22 ns | 850 ns (39.2x) | 421 ns (13.0x) |
| add 1 day | 296 ns | 99 ns (3.0x slower) | 2.34 us (9.0x) |
| startOf day | 253 ns | 91 ns (2.8x slower) | 2.22 us (10.8x) |
| moment() / new Date() | 147 ns | 35 ns (4.2x slower) | 260 ns (1.6x) |
mmntjs/fns format YYYY-MM-DD |
507 B gzip | — | single-import bundle |
The losses against date-fns on fresh-object rows are structural, not implementation gaps. Every moment() call allocates a wrapper object around a Date — this is the cost of preserving moment-compatible mutability, method chaining, .fn/.prototype extensibility, and locale context. date-fns operates on bare Date instances and skips that overhead entirely. The [fresh] benchmark marker amplifies the difference because it creates and discards a wrapper per iteration; real applications amortize it by reusing Moment objects. If you want date-fns-style standalone helpers without the wrapper, mmntjs/fns is the closer comparison point — it wins 19 of 26 direct Date-helper rows.
Representative Bun microbenchmarks on Apple Silicon. ns-scale results use median-of-repeated warmed runs after warmup — see BENCHMARKS.md for methodology, noise markers, and caveats.
For month/quarter/year comparisons, note that date-fns uses calendar-difference helpers while mmntjs matches moment.js's truncated fractional diff semantics. Those rows are still useful as implementation-cost comparisons, but they are not result-equivalent APIs.
Techniques: decomposed field cache, lazy init, Shape stability, charCodeAt parsing, branch reduction, pre-computed tables. See Performance Analysis, Techniques, Benchmarks.
mmntjs is not just a drop-in replacement. It also serves as a migration-analysis toolchain for legacy Moment codebases targeting Temporal:
Legacy moment.js codebase
│
├── mmntjs audit ./src ← Observability: inventory all moment API usage
├── mmntjs stats ./src ← Quantify: usage patterns, migration surface
│
├── mmntjs migrate --check ← Dry-run compatibility assessment
├── mmntjs migrate --apply ← Automated codemod (moment → mmntjs)
│
└── mmntjs report ./src ← Temporal migration guidance per module
Each phase is independent. Start with audit to understand your legacy surface, then migrate to mmntjs at your own pace, and finally generate a Temporal migration report when ready.
bun install
bun run build # CJS + ESM + IIFE + DTS
bun run test:hard # Full test suite
bun run lint # oxlintMIT