Share `Locale` instances between DateTimes with the same configuration
Is your feature request related to a problem? Please describe.
Every DateTime and Duration owns its own Locale, because Locale.create returns a fresh instance on every call. A Locale retains about 480 bytes in Node (its five lazy caches, the Intl config string and the instance itself), so a DateTime that is nothing more than a timestamp costs about 680 bytes retained, and roughly 70% of that is the Locale. In a heap snapshot of a large single-page application, about 24,800 retained DateTimes came with about 24,800 Locales, 13.6 MB in total, even though only a handful of distinct locale configurations existed.
The per-instance lazy caches have a CPU cost too: dt.weekdayLong, dt.monthLong, Info.months(..., { locale }) and friends rebuild their name lists through Intl.DateTimeFormat for every new DateTime in a non-English locale, because each instance starts with empty caches.
Describe the solution you'd like
Memoize Locale.create on the resolved configuration (locale, numbering system, output calendar, week settings, and whether the locale was specified or fell back), the way IANAZone.create already shares zones (#923), and clear it from Settings.resetCaches() with the other caches.
This is safe because a Locale is never mutated after construction (every change goes through clone(), which builds a new one) and its lazily populated lists depend only on its configuration. Two consequences of sharing need handling in the same change:
- Deep-freezing a
DateTimealso freezes itsLocale(#1104). With sharing, a frozen cached instance would break every otherDateTimewith that configuration, so the cache should treat a non-extensible instance as a miss and replace it. - The
Infolisters currently hand out theLocale's own cached arrays, which callers may mutate. With sharing they need to return copies, as the English lists andInfo.getWeekendWeekdaysalready do.
Measured on Node 26: retained heap per DateTime goes from 680 to 208 bytes, one Locale instead of 50,000 for 50,000 DateTimes, creation and formatting paths a few percent faster, and fresh-instance name lookups in non-English locales one to two orders of magnitude faster. No path gets slower outside the noise floor. I have a patch with tests ready and will open it as a PR referencing this issue.
Describe alternatives you've considered
- Doing nothing: callers cannot fix this from outside, since
Locale.createis internal and every factory goes through it. - Keying the cache on a composite string, as the Intl caches do: measured, and it regresses
DateTime.now()by 17% to 25% because building and hashing a fresh key on every creation costs more than the allocation it saves. Grouping entries by locale string and comparing the remaining fields directly avoids that. - An unbounded cache, matching luxon's other caches: workable, but a bound is cheap insurance for callers that pass unbounded distinct locale strings or
weekSettingsvalues. Happy to go either way. - An opt-in or opt-out setting: the other caches have none, and this would add API surface for no clear benefit.
Additional context
Byte figures are for V8 without pointer compression (Node); Chrome stores roughly half. Related: #923 (IANAZone cache), #1104 (frozen Duration), #1642.
Source: moment/luxon