Browsers today ship over 500 IANA timezones baked right into their engines. No downloads. No updates. Just works.
And here’s the kicker — a solo dev built a cron scheduler viewer around it, static HTML, zero deps, that spits out the next 10 runs in UTC, JST, PST, EST, CET. Side by side. DST handled. Try typing ‘0 9 * * 1-5’ and watch it map your mental gymnastics into a clean table.
Live demo’s here: https://sen.ltd/portfolio/cron-tz-viewer/. GitHub too: https://github.com/sen-ltd/cron-tz-viewer.
cron-tz-viewer. That’s the tool. Dead simple. But dig deeper, and it’s a masterclass in why you’re probably lugging around 100KB payloads like moment-timezone or Luxon for no good reason.
Why Cron Still Triggers Timezone Nightmares
Hand a cron string to a teammate oceans away? You’re doing the math. “0 9 * * 1-5 in JST — that’s midnight UTC, 4pm PT standard time, 5pm daylight… wait, is DST on?” Crontab.guru? Great, but one timezone at a time.
This tool fixes that. Shows everything at once. No server. No build step. Pure client-side JS.
The author nails it:
For years, “timezone math in JavaScript” meant moment-timezone or luxon. Both are excellent. Both are also 100+ KB of payload for a tool whose entire UI is a table.
Spot on. Those libs? Battle-tested beasts from when browsers were timezone deserts.
But now? Intl.DateTimeFormat packs the full IANA db. DST transitions for Tokyo, LA, Berlin — all there, auto-updated by your browser vendor.
Native Wall Clock: 20 Lines That Crush Libraries
Check this function. Wall-clock time in any zone from a UTC millis timestamp.
<a href="/tag/javascript/">javascript</a>
const DOW_MAP = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
export function wallClock(utcMs, tz) {
const fmt = new Intl.DateTimeFormat('en-US', {
timeZone: tz,
year: 'numeric', month: 'numeric', day: 'numeric',
hour: 'numeric', minute: 'numeric',
weekday: 'short',
hourCycle: 'h23',
});
const parts = fmt.formatToParts(new Date(utcMs));
const get = (type) => parts.find((p) => p.type === type).value;
return {
year: Number(get('year')),
month: Number(get('month')),
day: Number(get('day')),
hour: Number(get('hour')),
minute: Number(get('minute')),
dow: DOW_MAP[get('weekday')],
};
}
Boom. formatToParts spits structured data — no parsing “Dec 25” strings. h23 locks 24-hour format. Any IANA ID plugs in.
Five calls, one timestamp: full table row. Scales to your app? Damn right. (Pro tip: cache the formatter if you’re hammering it.)
My unique angle? This echoes the polyfill era’s end. Remember when every JS project bundled es5-shim, BigInt polyfills? Browsers caught up — caniuse.com lit green — libs died. Timezones? Same arc. ECMA-402 shipped the goods years ago, but devs clung to familiar giants. No more.
Do You Still Need Luxon or Moment-timezone?
Short answer: For toy tools like this? Hell no.
Luxon’s a gem — zoned times, immutability — but 200KB gzipped? For a table? Pass.
The real win: browser parity. Chrome, Firefox, Safari — all sync IANA via OS or direct pulls. Your Tokyo deploy won’t drift because Firefox lagged.
But caveats — older browsers (IE11, ancient Android)? Still need fallbacks. Enterprise? Test it. Most greenfield stuff? Native flies.
The Cron Loop That Doesn’t Freeze Your Tab
Naive next-runs algo: tick minute-by-minute till you hit 10 matches. /5 * * * ? Snappy.
@yearly? 525k loops per year × 10 = browser ice age.
Fix: smart skips.
If month mismatches, jump to next month start. Hour off? Leap to next hour’s zero-minute. Genius.
But here’s the subtlety — naive hour-skip (t += HOUR) fails tests. Why?
Take ‘0 0 * * *’ in JST, anchor near 2026-01-01.
Start 09:01 JST (post-midnight). Minute wrong → +1min to 09:02… up to 10:00. Hour 10 ≠0 → naive +HOUR lands 11:00.
Loop grinds. Eventually hits 00:00 next day. Pushes it.
Next iter starts 00:01. Minute wrong → steps to 01:00. Then hour-skips circle back wrong, overshooting by a day.
Expected the first next run to be 2026-01-02 00:00 JST. Got 2026-01-03 00:00 JST. One full day late.
The save: t += HOUR - (wc.minute * MINUTE). Resets to hour-start clean.
That’s craftsmanship. Tests caught the edge. Ships solid.
This isn’t hype — it’s architectural shift. JS engines own temporal math now. Libs pivot to high-level (fuzzy parsing, i18n extras) or die.
Prediction: By 2026, npmdl timezone bundles shrink 50%. Native-first wins. Your cron dashboard? Ditch the bundle.
Look, if you’re still copy-pasting moment() — stop. Crack open devtools, Intl.DateTimeFormat(‘en-US’, {timeZone: ‘Asia/Tokyo’}).formatToParts(new Date()). Boom, addicted.
Why Does This Matter for Distributed Dev Teams?
Cron’s dev ops staple. Slack bots, CI pipelines, reports — all timezone-fragile.
One table ends the Slack pings: “When does this run for you?”
Broader? Native APIs lure static sites deeper. PWAs with offline cron previews. SPAs dropping webpack for raw JS.
Surface area: 500 lines incl tests. That’s maintainable forever.
🧬 Related Insights
- Read more: Rust Runtime for TypeScript: The Backend Speedup Developers Crave
- Read more: Bheeshma Diagnosis Benchmarks: Megallm AI Tackles 20,000 Medical Records Without Flinching
Frequently Asked Questions
What is cron-tz-viewer?
A zero-dependency static tool showing next cron runs across UTC/JST/PST/EST/CET, DST-aware.
How does JavaScript handle timezones without libraries?
Via Intl.DateTimeFormat with formatToParts — browsers bundle full IANA DB, auto DST.
Can I use native JS timezones in production?
Yes for modern browsers; test edges, cache formatters for perf.
Word count: ~950.