From 88ce863121a86991a9864aaf3e02e89808a72166 Mon Sep 17 00:00:00 2001 From: Trevor Burnham Date: Wed, 31 Dec 2025 11:35:38 -0500 Subject: [PATCH 1/2] fix: Cache Intl.DateTimeFormat instances for better performance --- .../intl-date-time-format-cache.test.ts | 57 +++++++++++++++++++ .../utils/date-time/format-date-localized.ts | 7 ++- .../date-time/intl-date-time-format-cache.ts | 50 ++++++++++++++++ 3 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 src/internal/utils/date-time/__tests__/intl-date-time-format-cache.test.ts create mode 100644 src/internal/utils/date-time/intl-date-time-format-cache.ts diff --git a/src/internal/utils/date-time/__tests__/intl-date-time-format-cache.test.ts b/src/internal/utils/date-time/__tests__/intl-date-time-format-cache.test.ts new file mode 100644 index 0000000000..ac05bfc9e0 --- /dev/null +++ b/src/internal/utils/date-time/__tests__/intl-date-time-format-cache.test.ts @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { getDateTimeFormat } from '../intl-date-time-format-cache'; + +describe('getDateTimeFormat', () => { + test('returns an Intl.DateTimeFormat instance', () => { + const formatter = getDateTimeFormat('en-US', { month: 'long' }); + expect(formatter).toBeInstanceOf(Intl.DateTimeFormat); + }); + + test('returns the same instance for identical locale and options', () => { + const formatter1 = getDateTimeFormat('en-US', { month: 'long', year: 'numeric' }); + const formatter2 = getDateTimeFormat('en-US', { month: 'long', year: 'numeric' }); + expect(formatter1).toBe(formatter2); + }); + + test('returns the same instance regardless of options property order', () => { + const formatter1 = getDateTimeFormat('en-US', { month: 'long', year: 'numeric' }); + const formatter2 = getDateTimeFormat('en-US', { year: 'numeric', month: 'long' }); + expect(formatter1).toBe(formatter2); + }); + + test('returns different instances for different locales', () => { + const formatter1 = getDateTimeFormat('en-US', { month: 'long' }); + const formatter2 = getDateTimeFormat('de-DE', { month: 'long' }); + expect(formatter1).not.toBe(formatter2); + }); + + test('returns different instances for different options', () => { + const formatter1 = getDateTimeFormat('en-US', { month: 'long' }); + const formatter2 = getDateTimeFormat('en-US', { month: 'short' }); + expect(formatter1).not.toBe(formatter2); + }); + + test('handles undefined locale', () => { + const formatter = getDateTimeFormat(undefined, { month: 'long' }); + expect(formatter).toBeInstanceOf(Intl.DateTimeFormat); + }); + + test('formats dates correctly', () => { + const formatter = getDateTimeFormat('en-US', { month: 'long', year: 'numeric' }); + const date = new Date(2023, 5, 15); // June 15, 2023 + expect(formatter.format(date)).toBe('June 2023'); + }); + + test('caches formatters with complex options', () => { + const options: Intl.DateTimeFormatOptions = { + hour: '2-digit', + hourCycle: 'h23', + minute: '2-digit', + second: '2-digit', + }; + const formatter1 = getDateTimeFormat('en-US', options); + const formatter2 = getDateTimeFormat('en-US', options); + expect(formatter1).toBe(formatter2); + }); +}); diff --git a/src/internal/utils/date-time/format-date-localized.ts b/src/internal/utils/date-time/format-date-localized.ts index 0683e4afbc..9d04ea5295 100644 --- a/src/internal/utils/date-time/format-date-localized.ts +++ b/src/internal/utils/date-time/format-date-localized.ts @@ -3,6 +3,7 @@ import { isValid, parseISO } from 'date-fns'; import { formatTimeOffsetLocalized } from './format-time-offset'; +import { getDateTimeFormat } from './intl-date-time-format-cache'; export default function formatDateLocalized({ date: isoDate, @@ -26,7 +27,7 @@ export default function formatDateLocalized({ } if (isMonthOnly) { - const formattedMonthDate = new Intl.DateTimeFormat(locale, { + const formattedMonthDate = getDateTimeFormat(locale, { month: 'long', year: 'numeric', }).format(date); @@ -34,7 +35,7 @@ export default function formatDateLocalized({ return formattedMonthDate; } - const formattedDate = new Intl.DateTimeFormat(locale, { + const formattedDate = getDateTimeFormat(locale, { month: 'long', year: 'numeric', day: 'numeric', @@ -44,7 +45,7 @@ export default function formatDateLocalized({ return formattedDate; } - const formattedTime = new Intl.DateTimeFormat(locale, { + const formattedTime = getDateTimeFormat(locale, { hour: '2-digit', hourCycle: 'h23', minute: '2-digit', diff --git a/src/internal/utils/date-time/intl-date-time-format-cache.ts b/src/internal/utils/date-time/intl-date-time-format-cache.ts new file mode 100644 index 0000000000..88db2849ff --- /dev/null +++ b/src/internal/utils/date-time/intl-date-time-format-cache.ts @@ -0,0 +1,50 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * Cache for Intl.DateTimeFormat instances. + * + * Creating Intl.DateTimeFormat objects is expensive because the browser must + * parse locale data and resolve options. This cache stores formatter instances + * keyed by locale and options, allowing reuse across multiple format calls. + * + * The cache uses a simple Map with a string key derived from the locale and + * serialized options. Since the number of unique locale/option combinations + * in a typical application is small and bounded, we don't implement cache + * eviction. + */ + +const formatterCache = new Map(); + +/** + * Returns a cached Intl.DateTimeFormat instance for the given locale and options. + * If no cached instance exists, creates one and stores it in the cache. + */ +export function getDateTimeFormat( + locale: string | undefined, + options: Intl.DateTimeFormatOptions +): Intl.DateTimeFormat { + const cacheKey = createCacheKey(locale, options); + const cached = formatterCache.get(cacheKey); + + if (cached) { + return cached; + } + + const formatter = new Intl.DateTimeFormat(locale, options); + formatterCache.set(cacheKey, formatter); + return formatter; +} + +/** + * Creates a cache key from locale and options. + * Options are sorted by key to ensure consistent cache hits regardless of property order. + */ +function createCacheKey(locale: string | undefined, options: Intl.DateTimeFormatOptions): string { + const localeKey = locale ?? ''; + const optionsKey = Object.keys(options) + .sort() + .map(key => `${key}:${options[key as keyof Intl.DateTimeFormatOptions]}`) + .join(','); + return `${localeKey}|${optionsKey}`; +} From 552a60394a05352d5358ab3807c3690acaa25002 Mon Sep 17 00:00:00 2001 From: Trevor Burnham Date: Sat, 11 Jul 2026 09:01:48 -0400 Subject: [PATCH 2/2] chore: Simplify formatter cache comments per review feedback --- .../date-time/intl-date-time-format-cache.ts | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/internal/utils/date-time/intl-date-time-format-cache.ts b/src/internal/utils/date-time/intl-date-time-format-cache.ts index 88db2849ff..bb5e810e79 100644 --- a/src/internal/utils/date-time/intl-date-time-format-cache.ts +++ b/src/internal/utils/date-time/intl-date-time-format-cache.ts @@ -1,25 +1,13 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -/** - * Cache for Intl.DateTimeFormat instances. - * - * Creating Intl.DateTimeFormat objects is expensive because the browser must - * parse locale data and resolve options. This cache stores formatter instances - * keyed by locale and options, allowing reuse across multiple format calls. - * - * The cache uses a simple Map with a string key derived from the locale and - * serialized options. Since the number of unique locale/option combinations - * in a typical application is small and bounded, we don't implement cache - * eviction. - */ +// Constructing Intl.DateTimeFormat is expensive (the browser parses locale data and +// resolves options), so we cache instances keyed by locale and options for reuse. +// No eviction: the number of unique locale/option combinations in an application is +// small and bounded. const formatterCache = new Map(); -/** - * Returns a cached Intl.DateTimeFormat instance for the given locale and options. - * If no cached instance exists, creates one and stores it in the cache. - */ export function getDateTimeFormat( locale: string | undefined, options: Intl.DateTimeFormatOptions @@ -36,10 +24,7 @@ export function getDateTimeFormat( return formatter; } -/** - * Creates a cache key from locale and options. - * Options are sorted by key to ensure consistent cache hits regardless of property order. - */ +// Options are sorted by key so the cache hits regardless of property order. function createCacheKey(locale: string | undefined, options: Intl.DateTimeFormatOptions): string { const localeKey = locale ?? ''; const optionsKey = Object.keys(options)