Skip to main content

Why Developers Are Abandoning Moment.js (and What They Use Instead)

·PkgPulse Team

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."

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

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>

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.


Compare date library package health on PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.