date-fns, dayjs, luxon, and moment are JavaScript libraries designed to simplify working with dates and times. They address limitations of the native Date object—such as mutability, inconsistent parsing, poor time zone support, and awkward formatting—by providing more intuitive, consistent, and powerful APIs. While moment was once the dominant solution, it is now in maintenance mode, and modern alternatives offer better performance, immutability, and modular design.
Working with dates in JavaScript is notoriously tricky. The built-in Date object has quirks around mutability, time zones, and parsing. To solve this, developers turn to dedicated libraries. Here’s a deep technical comparison of the four most widely used options: date-fns, dayjs, luxon, and moment. We’ll look at how they handle core tasks like parsing, formatting, time zones, and immutability — with real code examples for each.
moment uses mutable objects. When you call methods like .add() or .subtract(), it changes the original instance.
// moment: mutable by default
const now = moment();
const later = now.add(1, 'hour');
console.log(now === later); // true — same object!
⚠️ Important: As of September 2020,
momentis officially in maintenance mode. Its documentation states: “We do not recommend starting new projects with Moment.js.” Avoid it for new codebases.
luxon, dayjs, and date-fns all use immutable approaches — but in different ways.
luxon returns new DateTime instances on every operation:
// luxon: immutable objects
const now = luxon.DateTime.now();
const later = now.plus({ hours: 1 });
console.log(now === later); // false — new object
dayjs also returns new instances, mimicking moment’s API but without mutation:
// dayjs: immutable (despite API similarity to moment)
const now = dayjs();
const later = now.add(1, 'hour');
console.log(now === later); // false
date-fns takes a pure functional approach: functions accept a Date object and return a new one.
// date-fns: pure functions
import { addHours } from 'date-fns';
const now = new Date();
const later = addHours(now, 1);
console.log(now === later); // false
All libraries can parse ISO 8601 strings reliably. But behavior diverges with ambiguous formats.
moment supports custom format strings but warns against them due to performance and ambiguity:
// moment: custom parsing (discouraged)
const d = moment('2023-05-15', 'YYYY-MM-DD');
luxon avoids custom string parsing entirely. It only accepts ISO 8601 or Date objects by default. For other formats, you must use fromFormat() explicitly:
// luxon: strict parsing
const d = luxon.DateTime.fromISO('2023-05-15');
// For non-ISO:
const d2 = luxon.DateTime.fromFormat('15/05/2023', 'dd/MM/yyyy');
dayjs parses ISO strings natively. Custom formats require a plugin:
// dayjs: needs plugin for custom formats
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
dayjs.extend(customParseFormat);
const d = dayjs('15/05/2023', 'DD/MM/YYYY');
date-fns provides parse() for custom formats as a core function:
// date-fns: built-in custom parsing
import { parse } from 'date-fns';
const d = parse('15/05/2023', 'dd/MM/yyyy', new Date());
moment requires the separate moment-timezone package:
// moment + moment-timezone
const d = moment.tz('2023-05-15', 'America/New_York');
luxon has first-class time zone support using the browser’s Intl API (no extra deps):
// luxon: native time zones
const d = luxon.DateTime.fromISO('2023-05-15', { zone: 'America/New_York' });
dayjs needs the timezone plugin and either a browser with Intl support or a manual time zone dataset:
// dayjs: plugin required
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(timezone);
const d = dayjs.tz('2023-05-15', 'America/New_York');
date-fns does not support time zones in its core. You must manage offsets manually or use date-fns-tz (a separate package):
// date-fns-tz: external package
import { zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz';
const d = utcToZonedTime(new Date(), 'America/New_York');
All libraries support standard and custom formatting.
moment:
moment().format('YYYY-MM-DD HH:mm'); // '2023-05-15 14:30'
luxon uses toFormat() with LDML patterns:
luxon.DateTime.now().toFormat('yyyy-MM-dd HH:mm'); // '2023-05-15 14:30'
dayjs matches moment’s tokens:
dayjs().format('YYYY-MM-DD HH:mm'); // '2023-05-15 14:30'
date-fns uses format() with its own token system:
import { format } from 'date-fns';
format(new Date(), 'yyyy-MM-dd HH:mm'); // '2023-05-15 14:30'
Note: date-fns and luxon use lowercase yyyy, while moment and dayjs use uppercase YYYY.
date-fns is designed for optimal tree shaking. You import only the functions you use:
import { addDays, format } from 'date-fns';
dayjs is tiny by default (~2KB), and plugins are opt-in. Unused plugins don’t bloat your bundle.
luxon is larger (~10–15KB minified) because it includes robust time zone logic via Intl. You can’t easily trim parts of it.
moment cannot be effectively tree-shaken. Even if you use one method, the entire library loads (~70KB minified).
Adding 3 days:
// moment
moment().add(3, 'days');
// luxon
dt.plus({ days: 3 });
// dayjs
dayjs().add(3, 'day');
// date-fns
addDays(new Date(), 3);
Comparing two dates:
// moment
moment(a).isBefore(b);
// luxon
dtA < dtB;
// dayjs
dayjs(a).isBefore(b);
// date-fns
isBefore(a, b);
moment: Rich plugin ecosystem, but largely frozen.dayjs: Modular via plugins (e.g., relativeTime, timezone, customParseFormat).luxon: Not plugin-based. Features are built-in or require manual composition.date-fns: Functions are composable by design. Extra locales and functions live in the main package but are imported individually.All support internationalization, but setup differs:
moment: Requires importing locale files.luxon: Uses the browser’s Intl, so locales work automatically if the environment supports them.dayjs: Locales are separate imports; you must register them.date-fns: Each locale is a separate module you import and pass to functions.// date-fns locale example
import { format } from 'date-fns';
import { es } from 'date-fns/locale';
format(date, 'PPPP', { locale: es });
| Feature | date-fns | dayjs | luxon | moment |
|---|---|---|---|---|
| Mutability | Immutable (functional) | Immutable | Immutable | Mutable |
| Time Zones | External (date-fns-tz) | Plugin required | Built-in (Intl) | Plugin (moment-timezone) |
| Tree Shaking | Excellent | Good | Poor | None |
| Bundle Size | Pay-per-use | ~2KB base | ~10–15KB | ~70KB (full) |
| Custom Parsing | Built-in | Plugin | Built-in (fromFormat) | Built-in (discouraged) |
| Maintenance Status | Active | Active | Active | Legacy / Maintenance Only |
moment for new projects. It’s outdated and heavy.date-fns if you prioritize bundle size, functional style, and fine-grained control.dayjs if you want a lightweight, moment-like API with plugin flexibility.luxon if you need robust time zone handling out of the box and prefer a modern, object-oriented API.Each of the three active libraries solves real problems well — your choice should hinge on architecture (functional vs OOP), time zone needs, and bundle constraints.
Choose date-fns if you need maximum bundle size efficiency through tree-shaking, prefer a functional programming style, and don’t require built-in time zone support. It’s ideal for applications where every kilobyte counts and you’re comfortable managing time zones externally via date-fns-tz.
Choose dayjs if you want a lightweight, familiar API similar to moment but immutable, and you’re okay with adding plugins for features like time zones or custom parsing. It strikes a balance between ease of migration from moment and modern performance requirements.
Do not choose moment for new projects. It is officially in maintenance mode, has a large bundle size, uses mutable state, and lacks modern optimizations like tree-shaking. Existing projects may continue using it, but new development should evaluate date-fns, dayjs, or luxon instead.
Choose luxon if your application heavily relies on time zones, daylight saving time, or internationalization, and you prefer an object-oriented, immutable API that leverages the browser’s built-in Intl support without extra dependencies.
🔥️ NEW: date-fns v4.0 with first-class time zone support is out!
date-fns provides the most comprehensive, yet simple and consistent toolset for manipulating JavaScript dates in a browser & Node.js
👉 Blog
It's like Lodash for dates
import { compareAsc, format } from "date-fns";
format(new Date(2014, 1, 11), "yyyy-MM-dd");
//=> '2014-02-11'
const dates = [
new Date(1995, 6, 2),
new Date(1987, 1, 11),
new Date(1989, 6, 10),
];
dates.sort(compareAsc);
//=> [
// Wed Feb 11 1987 00:00:00,
// Mon Jul 10 1989 00:00:00,
// Sun Jul 02 1995 00:00:00
// ]
The library is available as an npm package. To install the package run:
npm install date-fns --save
See date-fns.org for more details, API, and other docs.