date-fns, dayjs, js-joda, luxon, and moment are JavaScript libraries designed to simplify working with dates and times, addressing limitations of the native Date object such as mutability, poor timezone support, and inconsistent APIs. These libraries provide utilities for parsing, formatting, manipulating, and comparing dates across different timezones and locales, with varying approaches to immutability, bundle size, and internationalization.
Working with dates and times in JavaScript has long been a pain point. The built-in Date object is mutable, lacks timezone support, and has confusing APIs. Over the years, several libraries have emerged to solve these problems — each with different philosophies, trade-offs, and capabilities. Let’s compare the five most widely used options: date-fns, dayjs, js-joda, luxon, and moment.
moment in New Projectsmoment is officially deprecated. Its maintainers state on npm and GitHub that it should not be used in new projects due to its large bundle size, mutability, and lack of modern features like tree-shaking or immutable design. While it’s still maintained for legacy compatibility, do not choose moment for new applications.
// moment (deprecated)
const now = moment();
now.add(1, 'day'); // mutates the original object!
Now let’s focus on the four actively maintained alternatives.
Each library takes a fundamentally different approach to handling date objects.
date-fns uses a functional, immutable style. It treats dates as plain JavaScript Date objects and never mutates them. Every operation returns a new Date.
// date-fns
import { addDays, format } from 'date-fns';
const today = new Date();
const tomorrow = addDays(today, 1); // returns new Date
console.log(format(tomorrow, 'yyyy-MM-dd'));
dayjs follows an immutable, chainable API inspired by moment, but with a tiny footprint. It wraps native Date in a lightweight object.
// dayjs
import dayjs from 'dayjs';
const today = dayjs();
const tomorrow = today.add(1, 'day'); // returns new Dayjs instance
console.log(tomorrow.format('YYYY-MM-DD'));
js-joda is a faithful port of Java’s java.time package. It provides rich, immutable date-time types like LocalDateTime, ZonedDateTime, and Duration.
// js-joda
import { LocalDate, DateTimeFormatter } from '@js-joda/core';
const today = LocalDate.now();
const tomorrow = today.plusDays(1);
const formatter = DateTimeFormatter.ofPattern('yyyy-MM-dd');
console.log(tomorrow.format(formatter));
luxon uses an immutable, object-oriented model built on top of the modern Intl API. It emphasizes developer ergonomics and integrates deeply with browser timezones.
// luxon
import { DateTime } from 'luxon';
const today = DateTime.now();
const tomorrow = today.plus({ days: 1 });
console.log(tomorrow.toFormat('yyyy-MM-dd'));
Timezone handling varies significantly.
date-fns has no built-in timezone support. You must use companion packages like date-fns-tz for parsing or formatting in specific zones.
// date-fns + date-fns-tz
import { zonedTimeToUtc, utcToZonedTime, format } from 'date-fns-tz';
const utcDate = zonedTimeToUtc('2024-06-01 12:00:00', 'America/New_York');
const nyTime = utcToZonedTime(utcDate, 'America/New_York');
console.log(format(nyTime, 'yyyy-MM-dd HH:mm:ss z', { timeZone: 'America/New_York' }));
dayjs requires the timezone plugin for full IANA timezone support. Without it, only UTC and local time are available.
// dayjs with timezone plugin
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);
dayjs.extend(timezone);
const nyTime = dayjs.tz('2024-06-01 12:00', 'America/New_York');
console.log(nyTime.format('YYYY-MM-DD HH:mm z'));
js-joda includes first-class timezone support via the @js-joda/timezone package. Once loaded, all operations respect zone rules.
// js-joda with timezone
import '@js-joda/timezone'; // side-effect import loads tz data
import { ZonedDateTime, ZoneId } from '@js-joda/core';
const nyZone = ZoneId.of('America/New_York');
const zdt = ZonedDateTime.now(nyZone);
console.log(zdt.toString()); // includes offset and zone
luxon has built-in, automatic timezone support using the browser’s Intl API. No plugins needed.
// luxon
import { DateTime } from 'luxon';
const nyTime = DateTime.fromISO('2024-06-01T12:00:00', { zone: 'America/New_York' });
console.log(nyTime.toFormat('yyyy-MM-dd HH:mm z')); // uses system Intl
How much code ends up in your final bundle matters for frontend performance.
date-fns is fully tree-shakable because every function is a standalone export. You only pay for what you use.
// Only imports addDays and format — nothing else
import { addDays, format } from 'date-fns';
dayjs is modular by design. Core is tiny; plugins (like timezone) are opt-in and must be explicitly imported.
// Core + two plugins
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import customParseFormat from 'dayjs/plugin/customParseFormat';
js-joda is modular but not perfectly tree-shaken due to internal class dependencies. However, you can avoid loading timezone data unless needed.
// Basic usage without timezone
import { LocalDate } from '@js-joda/core';
// Only adds core classes
luxon is not easily tree-shaken because it exports a single DateTime class with many methods. You typically get the whole API even if you use only a few functions.
// Entire DateTime API is bundled
import { DateTime } from 'luxon';
All libraries support custom formats, but syntax differs.
date-fns uses token-based formatting (e.g., 'yyyy-MM-dd'). Parsing requires explicit format strings.
// date-fns
import { parse, format } from 'date-fns';
const date = parse('01.06.2024', 'dd.MM.yyyy', new Date());
console.log(format(date, 'yyyy-MM-dd'));
dayjs uses moment-style tokens ('YYYY-MM-DD'). Custom parsing needs the customParseFormat plugin.
// dayjs
dayjs.extend(customParseFormat);
const date = dayjs('01.06.2024', 'DD.MM.YYYY');
console.log(date.format('YYYY-MM-DD'));
js-joda uses Java-style pattern strings ('yyyy-MM-dd') via DateTimeFormatter.
// js-joda
const formatter = DateTimeFormatter.ofPattern('dd.MM.yyyy');
const date = LocalDate.parse('01.06.2024', formatter);
console.log(date.format(DateTimeFormatter.ISO_LOCAL_DATE));
luxon uses Intl-compatible tokens ('yyyy-MM-dd') and supports both fromFormat() and ISO parsing.
// luxon
const date = DateTime.fromFormat('01.06.2024', 'dd.MM.yyyy');
console.log(date.toFormat('yyyy-MM-dd'));
DateHow easily can you convert to/from JavaScript’s built-in Date?
date-fns: Works directly with Date objects — no conversion needed.
const nativeDate = new Date();
const future = addDays(nativeDate, 5); // still a Date
dayjs: Provides .toDate() to get a native Date.
const dayjsObj = dayjs();
const nativeDate = dayjsObj.toDate();
js-joda: Requires explicit conversion via .toDate() (for LocalDateTime) or .toInstant().atZone(...).toDate().
const jodaDate = LocalDateTime.now();
const nativeDate = jodaDate.atZone(ZoneId.SYSTEM).toDate();
luxon: Offers .toJSDate() to return a native Date.
const luxonDate = DateTime.now();
const nativeDate = luxonDate.toJSDate();
date-fns if:Date objects.dayjs if:moment with similar syntax.js-joda if:LocalDate, LocalTime).luxon if:Intl).moment for new projects.| Feature | date-fns | dayjs | js-joda | luxon | moment (⚠️ deprecated) |
|---|---|---|---|---|---|
| Mutability | Immutable (functional) | Immutable | Immutable | Immutable | Mutable |
| Tree-shaking | ✅ Excellent | ✅ Good (plugins) | ⚠️ Partial | ❌ Poor | ❌ None |
| Timezone Support | ❌ (via date-fns-tz) | ⚠️ (plugin) | ✅ (with package) | ✅ Built-in | ⚠️ (plugin) |
Native Date Interop | ✅ Direct | ✅ .toDate() | ⚠️ Manual | ✅ .toJSDate() | ✅ .toDate() |
| Parsing Syntax | Token (yyyy) | Moment-style (YYYY) | Java-style (yyyy) | Intl-style (yyyy) | Moment-style (YYYY) |
| Bundle Friendliness | ⭐ Best | ⭐ Very Good | ⚠️ Moderate | ⚠️ Heavy | ❌ Avoid |
There’s no universal “best” date library — only the best fit for your project’s constraints. If you’re building a content site with simple date display, date-fns or dayjs will keep your bundle lean. If you’re building a global scheduling app with complex timezone logic, luxon or js-joda will save you from subtle bugs. And if you’re maintaining legacy code, you might still encounter moment — but leave it in the past when starting fresh.
Choose date-fns if you prioritize minimal bundle size through excellent tree-shaking, work primarily with local or UTC dates, and prefer a functional programming style that operates directly on native JavaScript Date objects. It’s ideal for content-driven sites or apps where date logic is straightforward and timezone complexity is low.
Choose dayjs if you need a lightweight, moment-inspired API with chainable methods and are comfortable enabling plugins for features like timezone support or custom parsing. It strikes a balance between familiarity, performance, and modularity, making it a strong drop-in replacement for moment in new projects.
Choose js-joda if your application demands rigorous date-time modeling similar to Java’s java.time—such as distinct types for dates, times, and timezones—and you require strong immutability and type safety. It’s well-suited for enterprise systems with complex temporal business logic, though it comes with a steeper learning curve and larger footprint.
Choose luxon if you need robust, built-in timezone and internationalization support without managing external plugins, and you want to leverage modern browser standards like the Intl API. Its fluent, object-oriented interface works well for global applications like scheduling tools or dashboards that display localized time data.
Do not choose moment for new projects. It is officially deprecated, has a large bundle size, uses a mutable API that leads to subtle bugs, and lacks modern features like tree-shaking. While it remains in maintenance mode for legacy compatibility, all new development should evaluate date-fns, dayjs, js-joda, or luxon instead.
🔥️ 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.