dayjs review
Our sandbox build of Day.js 1.11.23 was 7.5 KB minified and 3.3 KB gzipped, which is the main reason to consider this Moment-style date wrapper. It wraps JavaScript `Date` in immutable, chainable values for parsing, formatting, comparisons, and calendar arithmetic. Strict formats, UTC, named zones, durations, relative text, and many comparison helpers arrive as plugins that must be registered with `dayjs.extend`. UTC and named-zone calls return the same `Dayjs` TypeScript type as local values; the duration plugin supplies a separate `Duration` type. The current release is a narrow timezone fix: invalid zoned input now stays an invalid Day.js value instead of causing `Intl.DateTimeFormat` to throw `RangeError`.
Our Day.js 1.11.23 install finished in 1.4 seconds, left 1 package using 3 MB, and produced a 3.3 KB gzipped browser bundle with 0 audit findings. Install it for Moment-shaped formatting and calendar math when startup plugin registration and a single Date-backed value type are acceptable.
We installed it
| Install | ✓ · 1.4s | 1 package on disk · 3 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | 3.3 KB | gzipped (7.5 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does dayjs install cleanly?
Yes. In a fresh container with an empty cache, npm install dayjs finished in 1 seconds, leaving 1 package and 3 MB on disk. npm audit reported no known vulnerabilities.
How much does dayjs add to a browser bundle?
3.3 KB gzipped (7.5 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does dayjs work with both ESM and CommonJS?
Yes. Both import 'dayjs' and require('dayjs') worked in Node 22 in our run. The package is published as CommonJS.
Does dayjs include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
dayjs or date-fns: which should you use?
date-fns: Choose it when named function imports and native Date objects fit better than a wrapper with registered methods. Our Day.js 1.11.23 install finished in 1.4 seconds, left 1 package using 3 MB, and produced a 3.3 KB gzipped browser bundle with 0 audit findings.
When should you not use dayjs?
Your domain types must prevent a plain date from being passed where an instant is expected. The bundled declarations return the same Dayjs class from core, UTC, and timezone calls.
Discussed on
- hnShow HN: Fast 2kB date library alternative to Moment.js with the same API122 points
- hnShow HN: Day.js 1.6 locale and plugins 2kb moment.js alternative with same API6 points
- hnShow HN: I made a React/DayJS picker as my first open source library5 points
- hnAsk HN: The Day.js Dilemma: How Should We Handle OSS Maintainers Going MIA?4 points
Use it if
- You are replacing Moment call sites and want to retain familiar tokens, method names, and chains while making date operations immutable.
- A browser feature needs parsing, formatting, comparisons, and calendar math within the 3.3 KB gzipped bundle we measured.
- Your application has one startup path where every required plugin and locale can be registered before request or UI code runs.
- Each supported runtime has `Intl` timezone data you can test, and shipping a separate IANA database is unnecessary.
- Your domain types must prevent a plain date from being passed where an instant is expected. The bundled declarations return the same `Dayjs` class from core, UTC, and timezone calls.
- A form parser must reject overflowed dates with no bootstrap step. Day.js documents non-strict normalization; exact checks require `customParseFormat` and a final `true` argument.
- A reusable module cannot change shared date behavior. `dayjs.extend()` adds methods to the shared module, while `dayjs.locale()` switches the global locale for later instances.
- Timezone results must use identical rule data across browsers, servers, and older devices. The timezone source asks each host's `Intl.DateTimeFormat` for offsets instead of bundling IANA data.
- You expect `tz.setDefault()` to redirect ordinary `dayjs()` calls. The timezone docs say only `dayjs.tz()` uses that default, so Moment's default-zone behavior does not carry over.
Setup reality
In our measurement setup, Day.js 1.11.23 installed in 1.4 seconds inside an unprivileged Node 22 Bookworm container with 3 CPUs, 8 GB RAM, and no cache. It left 1 package using 3 MB. The package was 2,136 KB unpacked, with 0 direct dependencies and 0 peers. npm audit reported 0 vulnerabilities at every severity. TypeScript declarations were bundled, and both require() and ESM import worked even though the package is CommonJS and has no exports map.
Day.js 1.11.23 asks for no credentials and reads no config file. Setup lives in code: import each plugin and pass it to dayjs.extend() before a caller touches the added method, then import every non-English locale you select. The declaration files use module augmentation, so TypeScript can accept .tz() even when a process forgot runtime registration. Put one bootstrap module ahead of application code and load it in tests too.
Core validation is permissive: the docs show 2022-01-33 rolling into February and passing isValid(). Register customParseFormat and pass true for form input. Assign results from add, subtract, set, and startOf because the original object does not change. diff returns an integer by default and keeps decimals only when its third argument is true. Moment-shaped method names can hide these different rules during a migration.
Named-zone work needs utc registered before timezone and relies on host Intl data. The plugin caches Intl.DateTimeFormat objects per zone and display-name choice; it has no IANA file to refresh. Version 1.11.23 turns invalid zoned parsing into an invalid value instead of a RangeError, so callers still need isValid(). Our esbuild browser build of a package-namespace import measured 7.5 KB minified and 3.3 KB gzipped. Re-measure after adding the plugins and locales used by the real entry point.
Patterns
Parse an ISO instant and format it parse-iso-timestamp
import dayjs from 'dayjs';
const createdAt = dayjs('2026-09-07T08:15:00Z');
if (!createdAt.isValid()) throw new Error('invalid timestamp');
console.log(createdAt.format('YYYY-MM-DD [at] HH:mm Z'));An ISO string ending in `Z` names an instant, but `format()` renders it in the process's local zone. Use UTC or timezone mode when output must ignore that zone.
Reject an impossible form date strict-form-parse
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
dayjs.extend(customParseFormat);
const birthday = dayjs('29/02/2025', 'DD/MM/YYYY', true);
if (!birthday.isValid()) throw new Error('invalid birthday');The final `true` requires an exact, valid match. Without `customParseFormat`, the named layout is unavailable.
Add calendar time without changing the source immutable-calendar-math
import dayjs from 'dayjs';
const orderedAt = dayjs('2026-09-07T09:00:00');
const dueAt = orderedAt.add(7, 'day').add(2, 'hour');
console.log(orderedAt.format());
console.log(dueAt.format());Each `add()` returns a new Day.js object. Code that ignores the return value also loses the calculation.
Build a half-open calendar-month range build-month-range
import dayjs from 'dayjs';
const value = dayjs('2026-09-18T10:15:00');
const from = value.startOf('month');
const toExclusive = value.add(1, 'month').startOf('month');
console.log({ from: from.toISOString(), toExclusive: toExclusive.toISOString() });`startOf()` returns a clone. An exclusive next-month boundary avoids depending on the final millisecond returned by `endOf('month')`.
Keep or discard fractional differences calculate-difference
import dayjs from 'dayjs';
const startedAt = dayjs('2026-01-01');
const checkedAt = dayjs('2026-09-07');
const wholeMonths = checkedAt.diff(startedAt, 'month');
const fractionalMonths = checkedAt.diff(startedAt, 'month', true);A two-argument `diff()` truncates to an integer. Passing `true` as the third argument keeps the fractional result.
Check an inclusive day range check-inclusive-range
import dayjs from 'dayjs';
import isBetween from 'dayjs/plugin/isBetween';
dayjs.extend(isBetween);
const value = dayjs('2026-09-07');
const inside = value.isBetween('2026-09-01', '2026-09-07', 'day', '[]');The `[]` marker includes both boundaries. Leaving it out makes the range exclusive at both ends.
Keep an instant in UTC mode parse-and-serialize-utc
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);
const receivedAt = dayjs.utc('2026-09-07T08:15:00Z');
console.log(receivedAt.format('YYYY-MM-DD HH:mm [UTC]'));
console.log(receivedAt.toISOString());`dayjs.utc()` makes getters and formatting use UTC. Core `dayjs()` uses the host's local zone for offset-free input.
Parse a wall time in a named zone parse-named-timezone
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(customParseFormat);
dayjs.extend(utc);
dayjs.extend(timezone);
const opensAt = dayjs.tz(
'07/09/2026 09:30',
'DD/MM/YYYY HH:mm',
'Europe/London',
);
if (!opensAt.isValid()) throw new Error('invalid opening time');The timezone plugin depends on `utc`, and the explicit layout depends on `customParseFormat`. In 1.11.23, a bad zoned value stays invalid instead of throwing `RangeError`.
Show one instant in another zone convert-timezone
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
const instant = dayjs.utc('2026-09-07T08:15:00Z');
const kolkata = instant.tz('Asia/Kolkata');
console.log(kolkata.format('YYYY-MM-DD HH:mm Z'));`.tz(zone)` preserves the instant and changes its display. Passing `true` as the second argument preserves wall-clock fields and changes the instant.
Describe one date relative to another format-relative-time
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
const publishedAt = dayjs('2026-09-04');
const viewedAt = dayjs('2026-09-07');
console.log(publishedAt.from(viewedAt));`from()` and `fromNow()` exist only after registering `relativeTime`. Their wording follows the active locale.
Format one value in French localize-one-value
import dayjs from 'dayjs';
import 'dayjs/locale/fr';
const label = dayjs('2026-09-07')
.locale('fr')
.format('dddd D MMMM YYYY');
console.log(label);The `fr` module must be imported before selection. Instance-level `locale()` returns a new value and leaves the global default unchanged.
Add a duration object to an instant create-and-apply-duration
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
dayjs.extend(duration);
const retryDelay = dayjs.duration({ minutes: 2, seconds: 30 });
const retryAt = dayjs('2026-09-07T08:15:00Z').add(retryDelay);
console.log(retryDelay.asSeconds());
console.log(retryAt.toISOString());`dayjs.duration()` and duration-aware `add()` come from the duration plugin; neither call is available in core.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| date-fns | npm | Choose it when named function imports and native `Date` objects fit better than a wrapper with registered methods. |
| luxon | npm | Choose it when zones, intervals, and durations should be explicit objects in the main API. |
| @js-temporal/polyfill | npm | Choose it when code should distinguish instants, plain dates, wall-clock values, and zoned date-times by type. |
More utils guides
lru-cache · type-fest · ajv · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

