Taming Timezones in JS: parseZone
Last week we shared the getISOTimezoneOffset magic dust which is a ~30 line function to extract a timezone offset gracefully.
Now the bulk of our parseZone compatible function will be:
new TZDate(dateStr, getISOTimezoneOffset(dateStr));
The rest of this utility is creating a wrapper that keeps legacy moment.js behaviour. It's mainly necessary in non-TypeScript codebases where there won't be a type error due to passing a dateStr which is not a string or passing nothing at all (we keep the same behaviour as moment.parseZone()/moment()).
import { TZDate } from '@date-fns/tz';
export function parseISOToDateInTimezone(dateStr: string): TZDate {
if (!dateStr) {
return new TZDate();
}
if (typeof dateStr !== 'string') {
console.warn(
`[parseISOToDateInTimezone] received non-string dateStr parameter`,
dateStr
);
if (process.env.NODE_ENV === 'production') {
dateStr = String(dateStr);
}
}
return new TZDate(dateStr, getISOTimezoneOffset(dateStr));
}
Benchmarking the custom parser against moment.parseZone
The theory behind our whole parsing utility is that using new Date(str) should be faster than running in userland with Regex via moment.parseZone or date-fns#parseISO. However Regex is fast and really we were trying to get rid the recorded Regex/moment memory usage so it's probably good to benchmark? What I'll tell you is that it cleared off the memory usage (sort of no surprise since we stopped using a Regex-based parser).
As the following benchmark shows it is faster.
import { Bench } from 'tinybench';
import moment from 'moment';
const bench = new Bench({ name: 'tz-aware ISO parsing' });
bench
.add('moment.parseZone()', () => {
moment.parseZone('2020-10-14T14:03:00+0200');
})
.add('parseISOToDateInTimezone()', () => {
parseISOToDateInTimezone('2020-10-14T14:03:00+0200');
});
(async () => {
console.log('Starting benchmark run');
await bench.run();
console.log(bench.name);
console.table(bench.table());
})();
Results running on a MacBook Air M1 on Node 24.5.0:
| Task Name | Median Latency (ns) | Median Throughput (ops/s) |
|---|---|---|
| moment.parseZone() | 4166.0 ± 41.00 | 240,038 ± 2386 |
| parseISOToDateInTimezone() | 2667.0 ± 41.00 | 374,953 ± 5677 |
In short: the custom parser has 36% better latency and 55% higher throughput.
That concludes the "taming timezones" series, hopefully you learnt something.
As always, the annotated source is available at github.com/HugoDF/real-world-ts/blob/main/src/parse-date-in-tz.ts