Why Developers Are Abandoning Moment.js (and What They Use Instead)
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
| Scenario | Pick |
|---|---|
| Migrating from Moment.js | Day.js (identical API) |
| New project, TypeScript-first | date-fns |
| Need timezone support out of box | Luxon |
| Bundle size is critical | Day.js (2KB) |
| Server-side date manipulation | date-fns or Luxon |
| React / component library | date-fns (tree-shakeable) |
| Simple formatting only | date-fns (import just format) |
| Need i18n with many locales | date-fns (43+ locales) or Day.js |
| Financial / calendar apps | Luxon (best timezone handling) |
Bundle Size Comparison
| Library | Minified | Gzipped | Tree-shakeable |
|---|---|---|---|
| Moment.js | 291KB | 72KB | ❌ No |
| Luxon | 73KB | 24KB | Partial |
| Day.js (core) | 7KB | 2KB | ❌ (plugin-based) |
| date-fns (full) | 78KB | ~24KB | ✅ Yes |
| date-fns (typical) | ~10KB | ~3KB | ✅ (per import) |
| Temporal (native) | 0KB | 0KB | N/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.
See the live comparison
View dayjs vs. date fns on PkgPulse →