Skip to content

Commit b4e5960

Browse files
committed
fix: Cache Intl.DateTimeFormat instances for better performance
1 parent 8c37834 commit b4e5960

3 files changed

Lines changed: 111 additions & 3 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { getDateTimeFormat } from '../intl-date-time-format-cache';
4+
5+
describe('getDateTimeFormat', () => {
6+
test('returns an Intl.DateTimeFormat instance', () => {
7+
const formatter = getDateTimeFormat('en-US', { month: 'long' });
8+
expect(formatter).toBeInstanceOf(Intl.DateTimeFormat);
9+
});
10+
11+
test('returns the same instance for identical locale and options', () => {
12+
const formatter1 = getDateTimeFormat('en-US', { month: 'long', year: 'numeric' });
13+
const formatter2 = getDateTimeFormat('en-US', { month: 'long', year: 'numeric' });
14+
expect(formatter1).toBe(formatter2);
15+
});
16+
17+
test('returns the same instance regardless of options property order', () => {
18+
const formatter1 = getDateTimeFormat('en-US', { month: 'long', year: 'numeric' });
19+
const formatter2 = getDateTimeFormat('en-US', { year: 'numeric', month: 'long' });
20+
expect(formatter1).toBe(formatter2);
21+
});
22+
23+
test('returns different instances for different locales', () => {
24+
const formatter1 = getDateTimeFormat('en-US', { month: 'long' });
25+
const formatter2 = getDateTimeFormat('de-DE', { month: 'long' });
26+
expect(formatter1).not.toBe(formatter2);
27+
});
28+
29+
test('returns different instances for different options', () => {
30+
const formatter1 = getDateTimeFormat('en-US', { month: 'long' });
31+
const formatter2 = getDateTimeFormat('en-US', { month: 'short' });
32+
expect(formatter1).not.toBe(formatter2);
33+
});
34+
35+
test('handles undefined locale', () => {
36+
const formatter = getDateTimeFormat(undefined, { month: 'long' });
37+
expect(formatter).toBeInstanceOf(Intl.DateTimeFormat);
38+
});
39+
40+
test('formats dates correctly', () => {
41+
const formatter = getDateTimeFormat('en-US', { month: 'long', year: 'numeric' });
42+
const date = new Date(2023, 5, 15); // June 15, 2023
43+
expect(formatter.format(date)).toBe('June 2023');
44+
});
45+
46+
test('caches formatters with complex options', () => {
47+
const options: Intl.DateTimeFormatOptions = {
48+
hour: '2-digit',
49+
hourCycle: 'h23',
50+
minute: '2-digit',
51+
second: '2-digit',
52+
};
53+
const formatter1 = getDateTimeFormat('en-US', options);
54+
const formatter2 = getDateTimeFormat('en-US', options);
55+
expect(formatter1).toBe(formatter2);
56+
});
57+
});

src/internal/utils/date-time/format-date-localized.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { isValid, parseISO } from 'date-fns';
44

55
import { formatTimeOffsetLocalized } from './format-time-offset';
6+
import { getDateTimeFormat } from './intl-date-time-format-cache';
67

78
export default function formatDateLocalized({
89
date: isoDate,
@@ -26,15 +27,15 @@ export default function formatDateLocalized({
2627
}
2728

2829
if (isMonthOnly) {
29-
const formattedMonthDate = new Intl.DateTimeFormat(locale, {
30+
const formattedMonthDate = getDateTimeFormat(locale, {
3031
month: 'long',
3132
year: 'numeric',
3233
}).format(date);
3334

3435
return formattedMonthDate;
3536
}
3637

37-
const formattedDate = new Intl.DateTimeFormat(locale, {
38+
const formattedDate = getDateTimeFormat(locale, {
3839
month: 'long',
3940
year: 'numeric',
4041
day: 'numeric',
@@ -44,7 +45,7 @@ export default function formatDateLocalized({
4445
return formattedDate;
4546
}
4647

47-
const formattedTime = new Intl.DateTimeFormat(locale, {
48+
const formattedTime = getDateTimeFormat(locale, {
4849
hour: '2-digit',
4950
hourCycle: 'h23',
5051
minute: '2-digit',
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
/**
5+
* Cache for Intl.DateTimeFormat instances.
6+
*
7+
* Creating Intl.DateTimeFormat objects is expensive because the browser must
8+
* parse locale data and resolve options. This cache stores formatter instances
9+
* keyed by locale and options, allowing reuse across multiple format calls.
10+
*
11+
* The cache uses a simple Map with a string key derived from the locale and
12+
* serialized options. Since the number of unique locale/option combinations
13+
* in a typical application is small and bounded, we don't implement cache
14+
* eviction.
15+
*/
16+
17+
const formatterCache = new Map<string, Intl.DateTimeFormat>();
18+
19+
/**
20+
* Returns a cached Intl.DateTimeFormat instance for the given locale and options.
21+
* If no cached instance exists, creates one and stores it in the cache.
22+
*/
23+
export function getDateTimeFormat(
24+
locale: string | undefined,
25+
options: Intl.DateTimeFormatOptions
26+
): Intl.DateTimeFormat {
27+
const cacheKey = createCacheKey(locale, options);
28+
const cached = formatterCache.get(cacheKey);
29+
30+
if (cached) {
31+
return cached;
32+
}
33+
34+
const formatter = new Intl.DateTimeFormat(locale, options);
35+
formatterCache.set(cacheKey, formatter);
36+
return formatter;
37+
}
38+
39+
/**
40+
* Creates a cache key from locale and options.
41+
* Options are sorted by key to ensure consistent cache hits regardless of property order.
42+
*/
43+
function createCacheKey(locale: string | undefined, options: Intl.DateTimeFormatOptions): string {
44+
const localeKey = locale ?? '';
45+
const optionsKey = Object.keys(options)
46+
.sort()
47+
.map(key => `${key}:${options[key as keyof Intl.DateTimeFormatOptions]}`)
48+
.join(',');
49+
return `${localeKey}|${optionsKey}`;
50+
}

0 commit comments

Comments
 (0)