Skip to main content

Why Developers Are Abandoning Moment.js in 2026

·PkgPulse Team
0

TL;DR

Moment.js is deprecated for new projects — Day.js or date-fns are the replacements. Moment.js (~14M weekly downloads, legacy) officially entered maintenance mode in 2020. Day.js (~25M downloads) offers a nearly identical API at 2KB instead of 72KB. date-fns (~50M downloads) is the functional alternative — tree-shakeable, TypeScript-first, zero mutations. For new projects: Day.js if you want the Moment.js API, date-fns if you want modern functional style.

Key Takeaways

  • date-fns: ~50M weekly downloads — functional, tree-shakeable, TypeScript-first
  • Day.js: ~25M downloads — Moment.js API-compatible, 2KB, plugin system
  • Moment.js: ~14M downloads — legacy, maintenance mode, 72KB bundle tax
  • Luxon: ~8M downloads — Moment.js team's spiritual successor, Intl-based
  • Temporal API — native JS date handling coming (Stage 3 proposal, polyfill available now)

Why Moment.js Is Dying

# The brutal numbers:
# Moment.js bundle size: 72KB minified + gzipped
# Day.js bundle size:     2KB minified + gzipped
# date-fns (full):       ~78KB but 100% tree-shakeable → 2-5KB typical usage
# Luxon bundle size:     ~24KB

# Moment.js issues:
# 1. Mutable objects — leads to subtle bugs
# 2. Not tree-shakeable — you get all 72KB regardless of what you use
# 3. Large built-in locale files (adds ~40KB for full i18n)
# 4. No TypeScript types (needs @types/moment)
# 5. Chain API mutates the original — classic footgun:

const now = moment();
const nextWeek = now.add(7, 'days');
// now === nextWeek — both point to same mutated object!
// This breaks React state, causes impossible bugs

# The official recommendation (from Moment.js docs, 2020+):
# "We now generally consider Moment to be a legacy project in maintenance mode.
# It is not dead, but it is indeed done."

The mutation problem is more subtle and dangerous than it appears in a code comment. In modern React development, mutation-based date libraries interact badly with the immutability expectations that React's reconciliation relies on. React's shallow comparison assumes that previous state references remain unchanged when deciding whether a re-render is needed. When moment().add() returns the same mutated object, React's comparison misses the change — leading to components that don't re-render when they should, or that re-render spuriously because a reference changed unexpectedly.

The issue surfaces most often in scheduling interfaces, calendar components, and any UI that displays date ranges. A developer builds a date range picker, stores startDate = moment(), then calls endDate = startDate.add(7, 'days') thinking they have two separate dates. They have one date that's been mutated seven days into the future. Both variables point to the same object. This exact bug pattern appears in hundreds of GitHub issues across popular calendar libraries built on Moment.js — and it's almost impossible to catch without knowing to look for it.

The Moment.js documentation is explicit about mutation. The maintainers have known about it for years. But fixing it would require a breaking API change to the entire library, which is why maintenance mode was declared in 2020 rather than a v3 released. The mutation semantics are too deeply embedded in existing codebases for a seamless upgrade path. Day.js solved this by adopting the same API surface with a different implementation: every method returns a new instance. The add() call never modifies the original. This single design decision eliminates the entire class of mutation bugs.

Day.js (Drop-In Replacement)

// Day.js — 2KB, Moment.js-compatible API
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import duration from 'dayjs/plugin/duration';
import isBetween from 'dayjs/plugin/isBetween';

// Register plugins as needed
dayjs.extend(relativeTime);
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(duration);
dayjs.extend(isBetween);

// Basic usage — identical to Moment.js API
const now = dayjs();
const formatted = now.format('YYYY-MM-DD HH:mm:ss');
const parsed = dayjs('2026-03-08');

// Immutable — unlike Moment.js
const today = dayjs();
const nextWeek = today.add(7, 'days');
// today is unchanged — Day.js is immutable by design

// Relative time (requires relativeTime plugin)
const publishDate = dayjs('2026-01-01');
console.log(publishDate.fromNow()); // "2 months ago"
console.log(dayjs().to(publishDate)); // "in a few seconds"

// UTC / Timezone (requires utc + timezone plugins)
const utcTime = dayjs.utc('2026-03-08T14:00:00Z');
const nyTime = utcTime.tz('America/New_York');
console.log(nyTime.format('h:mm A z')); // "9:00 AM EST"

// Duration
const dur = dayjs.duration(90, 'minutes');
console.log(dur.hours());    // 1
console.log(dur.minutes());  // 30
console.log(dur.humanize()); // "an hour"

// Comparison
const a = dayjs('2026-01-01');
const b = dayjs('2026-06-01');
console.log(a.isBefore(b));  // true
console.log(b.diff(a, 'months')); // 5

// Chaining (returns new instance each time — immutable)
const result = dayjs()
  .startOf('month')
  .add(1, 'week')
  .format('MMMM D, YYYY');
// Day.js migration from Moment.js — almost identical
// Moment.js:
import moment from 'moment';
moment().format('MMMM Do YYYY');
moment('2026-03-08').fromNow();
moment().subtract(10, 'days').calendar();

// Day.js equivalent (same API):
import dayjs from 'dayjs';
dayjs().format('MMMM Do YYYY');
dayjs('2026-03-08').fromNow(); // needs relativeTime plugin
dayjs().subtract(10, 'days').calendar(); // needs calendar plugin

// Key differences:
// 1. Plugins must be explicitly loaded (keeps bundle small)
// 2. All objects are immutable (no mutation bugs)
// 3. 2KB vs 72KB

Day.js's plugin architecture is both its greatest strength and its most significant potential footgun. The 2KB core is impressively tiny, but that size comes from moving non-essential functionality into opt-in plugins: relative time, timezone support, calendar formatting, quarter handling, and duration all require explicit registration. For teams migrating from Moment.js, this is usually smooth — most features have direct plugin equivalents, and dayjs.extend(plugin) is straightforward. The friction occurs in specific scenarios.

First, plugins must be registered before any module that uses them. Teams used to Moment.js's "everything always available" model occasionally create race conditions where a module uses a plugin before the registration code has run. The solution is straightforward — register all plugins in a single application entry point before any other imports — but the error message when it fails isn't always obvious.

Second, some timezone edge cases behave differently between moment-timezone and Day.js's timezone plugin, particularly around ambiguous times during DST transitions where an hour occurs twice. Most applications never encounter this edge case, but scheduling and financial applications dealing with exact wall-clock times need to verify behavior explicitly in their test suites before completing the migration.

Third, Day.js covers 63 locales vs date-fns's 100+. For applications serving audiences in less common language regions, date-fns is the better choice. For English-speaking or major-European-language audiences, Day.js's coverage is sufficient and the 2KB bundle makes it the obvious pick.

date-fns (Functional Style)

// date-fns — functional, tree-shakeable, TypeScript-first
import {
  format,
  parseISO,
  addDays,
  subMonths,
  differenceInDays,
  differenceInCalendarDays,
  isAfter,
  isBefore,
  isWithinInterval,
  startOfMonth,
  endOfMonth,
  eachDayOfInterval,
  formatDistanceToNow,
  formatDistance,
  fromUnixTime,
  getUnixTime,
} from 'date-fns';

// Basic formatting
const date = new Date('2026-03-08');
console.log(format(date, 'MMMM do, yyyy'));      // March 8th, 2026
console.log(format(date, 'yyyy-MM-dd'));          // 2026-03-08
console.log(format(date, "EEEE, MMMM d 'at' h:mm a")); // Sunday, March 8 at 12:00 AM

// Parsing
const parsed = parseISO('2026-03-08T14:30:00');  // Returns Date object

// Arithmetic — always returns new Date (immutable)
const today = new Date();
const nextWeek = addDays(today, 7);           // +7 days
const lastMonth = subMonths(today, 1);        // -1 month

// Difference
const days = differenceInDays(nextWeek, today);  // 7
const calDays = differenceInCalendarDays(nextWeek, today); // 7

// Comparison
const future = new Date('2027-01-01');
console.log(isAfter(future, today));          // true
console.log(isBefore(today, future));         // true

// Interval
const interval = { start: startOfMonth(today), end: endOfMonth(today) };
console.log(isWithinInterval(today, interval)); // true

// Relative time
console.log(formatDistanceToNow(parseISO('2026-01-01'))); // "2 months"
console.log(formatDistanceToNow(parseISO('2026-01-01'), { addSuffix: true })); // "2 months ago"

// Generate date ranges
const days = eachDayOfInterval({
  start: startOfMonth(today),
  end: endOfMonth(today),
});
// days = [Date, Date, ...] for every day in the month
// date-fns v3 — ESM-native, better TypeScript
// date-fns v3 ships ES modules by default
// Tree-shaking: import only what you use

// Only imports ~3KB for these 3 functions:
import { format, parseISO, addDays } from 'date-fns';

// i18n — import locale as needed
import { formatRelative } from 'date-fns';
import { es } from 'date-fns/locale';  // Spanish

const formatted = formatRelative(
  new Date('2026-03-08'),
  new Date(),
  { locale: es }
);
// date-fns — React usage pattern
import { format, formatDistanceToNow, parseISO } from 'date-fns';

interface ArticleMeta {
  publishedAt: string;  // ISO string from API
}

function ArticleDate({ publishedAt }: ArticleMeta) {
  const date = parseISO(publishedAt);
  const formattedDate = format(date, 'MMMM d, yyyy');
  const timeAgo = formatDistanceToNow(date, { addSuffix: true });

  return (
    <time dateTime={publishedAt} title={formattedDate}>
      {timeAgo}
    </time>
  );
}
// Renders: <time datetime="2026-01-15T09:00:00">about 2 months ago</time>

date-fns's position as the most-downloaded date library (~50M weekly) is counterintuitive given its verbose API. While Moment.js and Day.js offer chainable methods (dayjs().add(7, 'days').format('MMMM D')), date-fns uses standalone function imports (format(addDays(new Date(), 7), 'MMMM d')). The functional style requires learning different function names and a composition pattern rather than method chaining.

The reason date-fns dominates on downloads is tree-shaking. import { format } from 'date-fns' ships roughly 2KB. import { format, addDays, parseISO } from 'date-fns' ships about 4KB. The bundle grows proportionally to actual usage. A date-heavy application using 10 functions adds 15-20KB total. The same functionality in Moment.js costs an unavoidable 72KB, and intermediate Day.js costs 2KB core plus 1-3KB per plugin.

The secondary reason is TypeScript support quality. date-fns ships first-party TypeScript types with every function precisely typed, and the types flow correctly through composed operations. Teams that adopted date-fns when TypeScript became standard in the React ecosystem (2020–2022) found that the typing quality caught date parsing errors at compile time rather than at runtime — particularly important around format strings, where an incorrect format pattern wouldn't surface until a user saw garbled output in production.

For teams weighing Day.js vs date-fns for a new TypeScript project: if the Moment.js API familiarity doesn't matter, date-fns's tree-shaking and TypeScript quality edge makes it the better long-term choice. If API familiarity matters (or bundle size is already acceptable at Day.js's 2KB + plugins), Day.js wins on simplicity.

date-fns v4, released in 2024, made the ESM-native transition complete and introduced a date-fns-tz package split for timezone handling that keeps the core bundle size even tighter for projects that don't need timezone operations. The v3-to-v4 migration is significantly smaller than previous major version migrations — most teams completed it in under an hour with the provided codemod. One notable v4 change: parseISO behavior became stricter for non-standard date strings, which required a few fixes in projects that were passing loosely formatted dates. The stricter behavior is correct — ISO 8601 compliance is now enforced — but teams should run their test suites against v4 before completing the upgrade in production. The overall trajectory of date-fns v4 is toward smaller default bundle size and stricter correctness guarantees, which makes it even more compelling for new projects in 2026.

Luxon (Moment's Spiritual Successor)

// Luxon — built by Moment.js contributors, Intl-based
import { DateTime, Duration, Interval } from 'luxon';

// Fluent, immutable API (like Moment but better)
const now = DateTime.now();
const formatted = now.toFormat('MMMM d, yyyy');  // March 8, 2026
const iso = now.toISO();                          // 2026-03-08T14:30:00.000Z

// Parsing
const parsed = DateTime.fromISO('2026-03-08T14:00:00');
const fromHttp = DateTime.fromHTTP('Sun, 08 Mar 2026 14:00:00 GMT');
const fromMillis = DateTime.fromMillis(Date.now());

// Timezone handling — uses native Intl (no separate plugin needed)
const nyTime = now.setZone('America/New_York');
const tokyoTime = now.setZone('Asia/Tokyo');
const utcTime = now.toUTC();

console.log(nyTime.zoneName);   // "America/New_York"
console.log(nyTime.offset);     // -300 (minutes)
console.log(nyTime.offsetNameShort); // "EST"

// Arithmetic
const nextWeek = now.plus({ days: 7 });
const lastMonth = now.minus({ months: 1 });
const tomorrow = now.startOf('day').plus({ days: 1 });

// Duration
const dur = Duration.fromObject({ hours: 1, minutes: 30 });
console.log(dur.as('minutes'));  // 90
console.log(dur.toISO());        // "PT1H30M"
console.log(dur.toHuman());      // "1 hour, 30 minutes"

// Interval
const interval = Interval.fromDateTimes(
  DateTime.fromISO('2026-01-01'),
  DateTime.fromISO('2026-12-31')
);
console.log(interval.contains(now));   // true
console.log(interval.length('months')); // 12
console.log(interval.splitBy({ months: 1 }).length); // 12 monthly intervals

// Relative time (built-in, no plugin needed)
const past = now.minus({ hours: 5 });
console.log(past.toRelative());       // "5 hours ago"
console.log(past.toRelativeCalendar()); // "today"

When to Choose

ScenarioPick
Migrating from Moment.jsDay.js (identical API)
New project, TypeScript-firstdate-fns
Need timezone support out of boxLuxon
Bundle size is criticalDay.js (2KB)
Server-side date manipulationdate-fns or Luxon
React / component librarydate-fns (tree-shakeable)
Simple formatting onlydate-fns (import just format)
Need i18n with many localesdate-fns (43+ locales) or Day.js
Financial / calendar appsLuxon (best timezone handling)

Bundle Size Comparison

LibraryMinifiedGzippedTree-shakeable
Moment.js291KB72KB❌ No
Luxon73KB24KBPartial
Day.js (core)7KB2KB❌ (plugin-based)
date-fns (full)78KB~24KB✅ Yes
date-fns (typical)~10KB~3KB✅ (per import)
Temporal (native)0KB0KBN/A (built-in)

The Temporal API (Coming Soon)

// Temporal — native JavaScript date/time (Stage 3, 2026)
// Polyfill: npm install @js-temporal/polyfill
import { Temporal } from '@js-temporal/polyfill';

// Immutable, timezone-aware, no surprises
const now = Temporal.Now.plainDateTimeISO();
const formatted = now.toString(); // "2026-03-08T14:30:00"

// Timezone-aware zoned datetime
const zoned = Temporal.Now.zonedDateTimeISO('America/New_York');
console.log(zoned.toString()); // "2026-03-08T09:30:00-05:00[America/New_York]"

// Arithmetic — always returns new instance
const nextWeek = now.add({ days: 7 });
const lastMonth = now.subtract({ months: 1 });

// Duration
const duration = Temporal.Duration.from({ hours: 1, minutes: 30 });

// When to use the polyfill in 2026:
// - New greenfield projects comfortable with polyfills
// - Internal tooling where you control the runtime
// - Wait for native browser support before production-critical apps

Timeline: Temporal is Stage 3 (nearly finalized). Expect native support in 2027+. The polyfill is production-stable for non-browser environments.


The bundle size comparison reveals something important about how modern JavaScript tooling changed the entire calculus for library selection. In 2019, when most React applications used Webpack without aggressive tree-shaking, a 72KB Moment.js import was a significant but accepted cost — one of many large packages in a bundle that often totaled 500KB or more. Performance expectations were set by jQuery-era development where 2MB of JavaScript was considered normal.

By 2022, the performance conversation had fundamentally shifted. Core Web Vitals became a Google ranking factor. Lighthouse scores influenced product decisions. First Contentful Paint and Time to Interactive became metrics that product teams actually tracked. In this environment, 72KB for date formatting — an avoidable cost, given alternatives — was no longer acceptable for new projects making deliberate package choices.

The irony: Moment.js's download count stayed high throughout this period. The ~14M weekly downloads in 2026 are almost entirely legacy dependency chains — projects that started using Moment.js before the performance conversation intensified, internal tooling that was never performance-sensitive, and Google-indexed tutorials from 2020-2022 that still rank for "how to format dates in JavaScript." The npm download count reflects what's installed everywhere, not what thoughtful developers choose for new projects.

That distinction between installed base and new adoption explains why the ecosystem appears contradictory: Moment.js is both "the most commonly seen date library in existing codebases" and "never the right choice for a new project." Both statements are true simultaneously.

The Migration Reality: How Hard Is It Actually?

The size of the Moment.js migration varies enormously by codebase. For codebases that use Moment.js primarily for formatting, the migration to Day.js takes 1-2 hours: install Day.js, run a global find-replace of moment( with dayjs(, add the plugins you need (relativeTime, timezone), and run your tests. The API is nearly identical. For codebases that use Moment.js for timezone-heavy operations (moment-timezone is common in scheduling apps), the migration is more involved. Day.js's timezone plugin requires explicit plugin registration and behaves slightly differently for ambiguous times (DST transitions). Luxon handles these edge cases more gracefully through its native Intl API integration. The timezone.guess() function in moment-timezone maps to Intl.DateTimeFormat().resolvedOptions().timeZone in native code — and Day.js/Luxon both support Intl timezone detection natively. For codebases that use Moment.js for locale-based formatting with many locales, date-fns is often the better migration target. date-fns ships locale files as separate imports (import { es } from 'date-fns/locale'), keeping bundle size proportional to actual locale usage. Day.js has locales as plugins too, but date-fns has broader locale support (100+ locales vs Day.js's 63). The most common migration mistake: running the global find-replace without accounting for mutation. Code like const nextWeek = start.add(7, 'days') in Moment.js mutates start. In Day.js, add() returns a new instance. If your code depended on mutation (storing the date, adding to it, and expecting start to be modified), you'll need to update those patterns. Test coverage is your primary safeguard here.

Performance: When Does the Size Actually Matter?

The 72KB difference between Moment.js and Day.js sounds dramatic, but its actual user impact depends on your context. For server-side Node.js code (background jobs, API routes), bundle size is irrelevant — the import cost is paid once at startup, not per request. Swapping Moment.js for Day.js in a Node.js service doesn't improve user-facing performance at all. The performance win is entirely on the client side, and even there it depends on the framework. In a Next.js app with SSR, the date library is only included in the client bundle if it's imported in a client component. If all your date formatting is in Server Components (which is common — format a date string once on the server and send HTML), the client bundle never includes the date library at all. The places where the 72KB → 2KB savings is immediately measurable: single-page applications where all rendering is client-side, PWAs where the JavaScript bundle is cached and the first-load size determines install acceptance rates, and mobile web experiences where JavaScript parse time on slow devices is the bottleneck. The 72KB JavaScript parsing cost on a mid-range mobile device is roughly 250-350ms. That's a third of a second added to your Time to Interactive for using Moment.js on a SPA. At that level, the migration pays for itself on user experience metrics.

The Temporal API: Should You Look at It Now?

The Temporal API (TC39 Stage 3 as of 2026) is the long-awaited native JavaScript date handling system, designed to replace the broken Date object. Its design goals directly address every complaint about Moment.js: immutability (all operations return new instances), timezone-first design (every datetime knows its calendar and timezone), no mutation footguns, and precise ISO 8601 support. The polyfill (@js-temporal/polyfill) is production-stable and well-tested. Using it today carries a bundle cost (the polyfill is ~50KB) and a documentation cost (the API is different from both Moment.js and Day.js). The pragmatic 2026 answer: don't adopt Temporal for production applications yet unless you're specifically targeting environments where you control the runtime (Node.js, Deno, Bun — not browsers). Native browser support is progressing but isn't consistent enough for production use without a polyfill. The polyfill adds more bundle weight than Day.js, which defeats the migration goal. Watch Temporal's progress — it will become the right default eventually. For new projects in 2026, the guidance remains: use Day.js for Moment.js API familiarity, date-fns for functional/TypeScript-first codebases, Luxon for timezone-heavy applications. Revisit Temporal when native browser support reaches 90%+ (likely 2027-2028 based on current TC39 and browser implementation progress).


Choosing in 2026: The Practical Decision Framework

The decision has simplified considerably since 2022. The main question is whether you're migrating existing Moment.js code or starting fresh.

Migrating from Moment.js: use Day.js. API compatibility is the decisive factor. Find-replace automation handles 80-90% of the migration; the remaining 10% requires reviewing mutation-dependent patterns and timezone edge cases. For most teams, migration completes in 2-4 hours. Registering all necessary plugins at your application entry point before testing is the step most teams miss on first pass.

New project, TypeScript-first: use date-fns. The functional API that felt verbose in 2020 is well-suited to TypeScript's type inference — each function has precise input and output types, composition is explicit, and the bundle cost scales with actual usage. If your project also uses tRPC or Zod, date-fns's first-class TypeScript support fits naturally.

Timezone-heavy application (scheduling, calendar, financial, telecommunications): use Luxon. Its native Intl API integration handles DST transitions, UTC offset changes, and calendar system differences more reliably than any other option. If you're parsing timestamps that users input in local time and need to store in UTC, Luxon's explicit zone handling prevents the ambiguous-time bugs that catch Day.js users by surprise.

Simple display formatting in a Server Component: consider Intl.DateTimeFormat directly. Many Server Components need only to format a date string for display — no parsing, arithmetic, or comparison required. The native Intl API handles this with zero npm dependency and zero bundle cost, because it runs server-side and the formatted HTML string is what reaches the client.

One underappreciated scenario: server-side Node.js date manipulation for batch jobs, scheduled tasks, or backend-only date processing. In these contexts, bundle size is irrelevant — no user is downloading the JavaScript. Date-fns, Day.js, and Luxon all perform identically from a user impact perspective. Choose based on API preference and team familiarity. Luxon's explicitness often wins in server-side scheduling code where timezone precision is critical and the verbose API is actually desirable.

The Temporal API polyfill is worth monitoring but not yet worth adopting for production client-side applications in 2026. Its design is the correct long-term direction for JavaScript date handling, but native browser support hasn't reached the threshold where shipping without a polyfill is practical. Revisit when major browsers cross 90% support — likely 2027-2028 based on current implementation progress.


Compare date library package health on PkgPulse.

See also: Day.js vs Moment.js and date-fns vs Moment.js, date-fns vs Day.js vs Luxon 2026: Which Date Library Wins?.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.