Taming Timezones in JS
One of the last pieces of work I completed at my previous workplace was to migrate a core system from moment
to date-fns
, vanilla Date manipulation & formatting.
The next few weeks will detail how we preserved timezone-aware parsing when migrating from moment.parseZone
to @date-fns/tz
and the native JS Date() constructor to parse.
I drafted this post a while back, but only dusted it off after reading Zack Leatherman's Never write your own Date Parsing Library. So thanks to Zack for inspiring me to share my own JS Date story.
Setting the scene: what the timezone?
Why are timezones so important? What problems do you get dealing with dates and timezones?
Well, let's run through some Date use cases:
- you might want to display a date in the "date's timezone" eg. the departure time of a flight from Milan, should be displayed in Milan local time
- you might want to run calculations between dates that have different timezones eg. duration of flight from London to Milan
In the following image for a London-Milan return journey by plane:
The outbound flight departs 06:35 London time, arrives at 09:30 Milan time, if you calculate the duration in a non-timezone-aware manner you end up with 2h55, but the flight time is only 1h55, the other hour is due to the timezones.
Similarly on the return flight, departing 20:05 and arriving 21:10, the flight is not 1h5min, it's 2h05.
In short, we need to know the timezone of a date, otherwise we can get ourselves in trouble doing calculations and displaying the dates in different places.
What's wrong with moment.js anyway?
The current way to parse dates with TZ information was moment.parseZone
, but we're moving away from moment
. It's been in maintenance mode for years at this point and the issues with it are well documented:
- it's monolithic (not a problem in our Node.js service but can be an issue for client-side JavaScript)
- method calls tend to mutate the Moment object instance instead of making copies
- it's not TypeScript-native (not that our service was in TypeScript, more on that later)
The inside track to what happened is that some live memory profiles of one of our high-load Node.js services flagged high Regex usage. This usage could be traced back to the Moment.js parser triggered via moment.parseZone
.
Since we're trying to eliminate Regex from our memory profile, we opted against leveraging date-fns#parseISO
since that also uses regex-based parsing. Instead we want to use the native parser via new Date(stringToParse)
, which should mean the parser is in engine code and shouldn't use any of our memory.
We are parsing a few well-defined formats: 2020-10-14
, 2020-10-14T14:03:00+0200
, in a controlled environment (Node.js Docker image) which is why using the date constructor fits our needs.
Parsing using the Date()
constructor in a React Native app would have inconsistent output across platforms due to the different date parser (& JS engines) implementations across Android and iOS devices.
You can find the full commented source for this series at github.com/HugoDF/real-world-ts/blob/main/src/parse-date-in-tz.ts.