date-fns v4 vs Temporal API vs Day.js: Date Handling in 2026
·PkgPulse Team
TL;DR
date-fns v4 (with TypeScript and tree-shaking) is the 2026 default for date manipulation — 13KB, functional, no mutation. The Temporal API is now in Stage 3 (polyfill available) and will eventually replace Date built-in. Day.js is still useful for its Moment.js-compatible API (minimal migration). Avoid: Moment.js (immutable values, 70KB), Luxon (overkill unless heavy timezone work).
Key Takeaways
- date-fns v4: ~13KB tree-shaken, pure functions, TypeScript, best ecosystem
- Temporal API: Future web standard, no mutation, proper timezone handling
- Day.js: Moment.js drop-in, 2KB core, plugin system
- date-fns v4 vs v3: Now supports Temporal natively, fp submodule improved
- Bundle sizes: date-fns (13KB used) < Day.js (2KB + plugins) < Luxon (24KB) << Moment (70KB)
Downloads
| Package | Weekly Downloads | Trend |
|---|---|---|
date-fns | ~30M | → Stable |
dayjs | ~15M | → Stable |
luxon | ~8M | ↓ Declining |
moment | ~12M | ↓ Declining (legacy) |
date-fns v4: The Default Choice
import {
format, formatDistance, formatRelative,
add, sub, differenceInDays, differenceInHours,
isAfter, isBefore, isSameDay,
startOfDay, endOfDay, startOfWeek, startOfMonth,
parseISO, parse,
isValid,
} from 'date-fns';
import { toZonedTime, formatInTimeZone } from 'date-fns-tz';
// Basic formatting:
const now = new Date();
format(now, 'yyyy-MM-dd'); // "2026-03-09"
format(now, 'MMMM d, yyyy'); // "March 9, 2026"
format(now, 'h:mm a'); // "3:45 PM"
format(now, "yyyy-MM-dd'T'HH:mm:ssXXX"); // ISO 8601
// Date arithmetic (immutable — always returns new Date):
const nextWeek = add(now, { weeks: 1 });
const lastMonth = sub(now, { months: 1 });
const threeHoursLater = add(now, { hours: 3, minutes: 30 });
// Relative time:
formatDistance(new Date('2026-03-01'), now, { addSuffix: true });
// "8 days ago"
formatRelative(new Date('2026-03-09'), now);
// "today at 3:45 PM"
// Comparisons:
isAfter(nextWeek, now); // true
isBefore(lastMonth, now); // true
isSameDay(now, new Date('2026-03-09')); // true
// Range helpers:
const start = startOfWeek(now, { weekStartsOn: 1 }); // Monday
const end = endOfDay(now);
differenceInDays(end, start); // 0 to 6
// Parse:
const date = parseISO('2026-03-09T15:45:00Z'); // ISO string → Date
const dateFromString = parse('09/03/2026', 'dd/MM/yyyy', new Date());
isValid(dateFromString); // true
// Timezone handling with date-fns-tz:
import { toZonedTime, fromZonedTime, formatInTimeZone } from 'date-fns-tz';
const utcDate = new Date('2026-03-09T15:45:00Z');
const userTz = 'America/New_York';
// Convert UTC → user timezone:
const localDate = toZonedTime(utcDate, userTz);
format(localDate, 'h:mm a'); // "10:45 AM"
// Format directly in timezone:
formatInTimeZone(utcDate, userTz, 'yyyy-MM-dd HH:mm zzz');
// "2026-03-09 10:45 EST"
// Convert user input → UTC for storage:
const inputDate = parse('2026-03-09 10:45', 'yyyy-MM-dd HH:mm', new Date());
const utcForStorage = fromZonedTime(inputDate, userTz);
Temporal API (Future Standard)
// Temporal API — Stage 3, polyfill available:
// npm install @js-temporal/polyfill
import { Temporal } from '@js-temporal/polyfill';
// PlainDate — no time, no timezone:
const today = Temporal.Now.plainDateISO(); // 2026-03-09
const birthday = Temporal.PlainDate.from('1990-06-15');
const yearsOld = today.since(birthday, { largestUnit: 'years' }).years;
// Temporal.ZonedDateTime — the full type:
const now = Temporal.Now.zonedDateTimeISO('America/New_York');
console.log(now.toString()); // "2026-03-09T10:45:00-05:00[America/New_York]"
// Arithmetic (immutable — returns new instances):
const nextWeek = now.add({ weeks: 1 });
const lastMonth = now.subtract({ months: 1 });
// No timezone confusion:
// Date DST + arithmetic = bugs; Temporal handles DST correctly:
const springForward = Temporal.ZonedDateTime.from('2026-03-08T01:00:00[America/New_York]');
const twoHoursLater = springForward.add({ hours: 2 });
// Correctly returns 04:00 (skips 02:00 DST gap)
// Duration:
const duration = Temporal.Duration.from({ hours: 2, minutes: 30 });
const futureTime = now.add(duration);
Day.js: Moment.js Drop-In
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import customParseFormat from 'dayjs/plugin/customParseFormat';
dayjs.extend(relativeTime);
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(customParseFormat);
// Moment.js-compatible API:
dayjs().format('YYYY-MM-DD'); // "2026-03-09"
dayjs().format('MMMM D, YYYY'); // "March 9, 2026"
dayjs('2026-03-01').fromNow(); // "8 days ago"
dayjs().add(1, 'week').toDate(); // Date object
dayjs().subtract(1, 'month').toDate();
// Timezone:
dayjs().tz('America/New_York').format('h:mm A'); // "10:45 AM"
dayjs.utc('2026-03-09T15:45:00Z').tz('America/New_York').format();
Benchmark Comparison
Bundle size (minified + gzipped, typical usage):
date-fns (tree-shaken, 10 fns): ~8KB
Day.js (core): 2KB + plugins
date-fns (all imported): ~13KB
date-fns-tz: +3KB for timezone
Luxon: 24KB
Moment.js: 70KB (avoid)
Temporal polyfill: 35KB (will be 0 when native)
Performance (1M date operations):
date-fns: ~85ms
Day.js: ~120ms (wrapper overhead)
Temporal: ~95ms (polyfill overhead)
Decision Guide
Use date-fns v4 if:
→ Default choice for TypeScript projects
→ Need tree-shaking (bundle size matters)
→ Want pure functions, no mutation
→ Need date-fns-tz for serious timezone handling
Use Day.js if:
→ Migrating from Moment.js (1:1 API)
→ Very small bundle budget (2KB core)
→ Plugin ecosystem covers your needs
Use Temporal API if:
→ New projects willing to use a polyfill
→ Complex timezone arithmetic where DST correctness matters
→ Building for the future (native in 2-3 years)
Avoid:
→ Moment.js: 70KB, mutation, deprecated
→ Luxon: good but 24KB when date-fns is smaller
Compare date-fns, Day.js, and Luxon download trends on PkgPulse.