Why should you reuse formatters instead of creating new ones?
Creating Intl formatters is expensive, but reusing the same formatter instance improves performance
Introduction
When you create an Intl formatter in JavaScript, the browser performs expensive operations to set up the formatter instance. It parses your options, loads locale data from disk, and builds internal data structures for formatting. If you create a new formatter every time you need to format a value, you repeat this expensive work unnecessarily.
Reusing formatter instances eliminates this repeated work. You create the formatter once and use it many times. This pattern is particularly important in loops, frequently called functions, and high-performance code where you format many values.
The performance difference between creating new formatters and reusing existing ones can be substantial. In typical scenarios, reusing formatters can reduce formatting time from hundreds of milliseconds to just a few milliseconds.
Why creating formatters is expensive
Creating an Intl formatter involves several costly operations that happen inside the browser.
First, the browser parses and validates the options you provide. It checks that locale identifiers are valid, that numeric options are in range, and that incompatible options are not combined. This validation requires string parsing and lookup operations.
Second, the browser performs locale negotiation. It takes your requested locale and finds the best available match from the locales the browser supports. This involves comparing locale identifiers and applying fallback rules.
Third, the browser loads locale-specific data. Date formatters need month names, day names, and formatting patterns for the locale. Number formatters need grouping rules, decimal separators, and digit characters. This data comes from the browser's internal locale database and must be loaded into memory.
Fourth, the browser builds internal data structures for formatting. It compiles formatting patterns into efficient representations and sets up state machines for processing values. These structures persist for the lifetime of the formatter.
All of this work happens every time you create a formatter. If you create a formatter, use it once, and discard it, you waste all that setup work.
The performance difference
The performance impact of recreating formatters becomes visible when you format many values.
Consider code that formats a list of dates without reusing the formatter.
const dates = [
new Date('2024-01-15'),
new Date('2024-02-20'),
new Date('2024-03-10')
];
// Creates a new formatter for each date
dates.forEach(date => {
const formatted = date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
console.log(formatted);
});
The toLocaleDateString() method creates a new DateTimeFormat instance internally for each date it formats. For three dates, this creates three formatters. For a thousand dates, it creates a thousand formatters.
Compare this to code that creates one formatter and reuses it.
const dates = [
new Date('2024-01-15'),
new Date('2024-02-20'),
new Date('2024-03-10')
];
// Create formatter once
const formatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
// Reuse formatter for each date
dates.forEach(date => {
const formatted = formatter.format(date);
console.log(formatted);
});
This code creates one formatter and uses it three times. For a thousand dates, it still creates one formatter and uses it a thousand times.
The time difference between these approaches grows with the number of values you format. Formatting a thousand dates by creating a thousand formatters can take over 50 times longer than formatting them with one reused formatter.
Reusing formatters at module scope
The simplest way to reuse a formatter is to create it once at module scope and use it throughout your module.
// Create formatter at module scope
const dateFormatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
function formatDate(date) {
return dateFormatter.format(date);
}
function formatDates(dates) {
return dates.map(date => dateFormatter.format(date));
}
// All functions share the same formatter instance
console.log(formatDate(new Date()));
console.log(formatDates([new Date(), new Date()]));
This pattern works well when you format values the same way throughout your code. The formatter lives for the lifetime of your application, and every function that needs it can use the same instance.
The same pattern works for number formatters, list formatters, and all other Intl formatters.
const numberFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
});
const listFormatter = new Intl.ListFormat('en-US', {
style: 'long',
type: 'conjunction'
});
function formatPrice(amount) {
return numberFormatter.format(amount);
}
function formatNames(names) {
return listFormatter.format(names);
}
Reusing formatters in functions
When you need different formatting options in different parts of your code, you can create formatters inside functions and rely on closures to preserve them.
function createDateFormatter() {
const formatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
return function formatDate(date) {
return formatter.format(date);
};
}
const formatDate = createDateFormatter();
// The formatter is created once when you call createDateFormatter
// Each call to formatDate reuses the same formatter
console.log(formatDate(new Date('2024-01-15')));
console.log(formatDate(new Date('2024-02-20')));
console.log(formatDate(new Date('2024-03-10')));
This pattern is useful when you want to create a configured formatter that gets reused but do not want to expose the formatter itself.
When formatter reuse matters most
Reusing formatters provides the most benefit in specific scenarios.
The first scenario is loops. If you format values inside a loop, creating a new formatter on each iteration multiplies the cost by the number of iterations.
// Inefficient: creates N formatters
for (let i = 0; i < 1000; i++) {
const formatted = new Intl.NumberFormat('en-US').format(i);
processValue(formatted);
}
// Efficient: creates 1 formatter
const formatter = new Intl.NumberFormat('en-US');
for (let i = 0; i < 1000; i++) {
const formatted = formatter.format(i);
processValue(formatted);
}
The second scenario is frequently called functions. If a function formats values and gets called many times, formatter reuse avoids recreating the formatter on each call.
// Inefficient: creates formatter on every call
function formatCurrency(amount) {
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
});
return formatter.format(amount);
}
// Efficient: creates formatter once
const currencyFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
});
function formatCurrency(amount) {
return currencyFormatter.format(amount);
}
The third scenario is processing large datasets. When you format hundreds or thousands of values, the setup cost of creating formatters becomes a significant portion of total time.
// Inefficient for large datasets
function processRecords(records) {
return records.map(record => ({
date: new Intl.DateTimeFormat('en-US').format(record.date),
amount: new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(record.amount)
}));
}
// Efficient for large datasets
const dateFormatter = new Intl.DateTimeFormat('en-US');
const amountFormatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
});
function processRecords(records) {
return records.map(record => ({
date: dateFormatter.format(record.date),
amount: amountFormatter.format(record.amount)
}));
}
In these scenarios, reusing formatters reduces the time spent on formatting operations and improves application responsiveness.
Caching formatters with different options
When you need to use formatters with many different option combinations, you can cache formatters based on their configuration.
const formatterCache = new Map();
function getNumberFormatter(locale, options) {
// Create a cache key from locale and options
const key = JSON.stringify({ locale, options });
// Return cached formatter if it exists
if (formatterCache.has(key)) {
return formatterCache.get(key);
}
// Create new formatter and cache it
const formatter = new Intl.NumberFormat(locale, options);
formatterCache.set(key, formatter);
return formatter;
}
// First call creates and caches formatter
const formatter1 = getNumberFormatter('en-US', { style: 'currency', currency: 'USD' });
console.log(formatter1.format(42.50));
// Second call reuses cached formatter
const formatter2 = getNumberFormatter('en-US', { style: 'currency', currency: 'USD' });
console.log(formatter2.format(99.99));
// Different options create and cache a new formatter
const formatter3 = getNumberFormatter('en-US', { style: 'percent' });
console.log(formatter3.format(0.42));
This pattern lets you get the benefits of formatter reuse even when you need different formatting configurations in different parts of your code.
Modern browser optimizations
Modern JavaScript engines have optimized the creation of Intl formatters to reduce the performance cost. Creating formatters is faster today than it was in older browsers.
However, reusing formatters remains a best practice. Even with optimizations, creating a formatter is still more expensive than calling the format() method on an existing formatter. The cost difference is smaller than it used to be, but it still exists.
In high-performance code, code that runs in loops, and code that processes large datasets, formatter reuse continues to provide measurable benefits. The optimization of formatter creation does not eliminate the value of reusing formatters.
Key takeaways
Creating Intl formatters is expensive because the browser must parse options, perform locale negotiation, load locale data, and build internal data structures. This work happens every time you create a formatter.
Reusing formatter instances avoids repeating this work. You create the formatter once and call its format() method many times. This reduces the time spent on formatting operations.
Formatter reuse matters most in loops, frequently called functions, and code that processes large datasets. In these scenarios, the cost of creating formatters can become a significant portion of total execution time.
The simplest reuse pattern is creating formatters at module scope. For more complex scenarios, you can use closures or caching based on configuration options.