为什么要复用格式化器而不是每次新建?
创建 Intl 格式化器开销较大,复用同一个实例可提升性能
简介
在 JavaScript 中创建 Intl 格式化器时,浏览器会执行一系列高开销操作来初始化格式化器实例。它会解析你的选项,从磁盘加载本地化数据,并构建用于格式化的内部数据结构。如果每次需要格式化数值时都新建一个格式化器,就会重复这些耗时的操作。
复用格式化器实例可以避免重复这些工作。你只需创建一次格式化器,然后多次使用它。这个模式在循环、高频调用的函数以及需要格式化大量数据的高性能代码中尤为重要。
新建格式化器和复用已有格式化器之间的性能差异可能非常大。在常见场景下,复用格式化器可以将格式化时间从数百毫秒降低到几毫秒。
为什么创建格式化器开销大
创建 Intl 格式化器时,浏览器内部会执行多个高成本操作。
首先,浏览器会解析并校验你提供的选项。它会检查区域标识符是否合法,数值选项是否在有效范围内,以及是否存在不兼容的选项组合。这些校验需要进行字符串解析和查找操作。
其次,浏览器会进行区域协商。它会根据你请求的区域,查找浏览器支持的最佳匹配区域。这涉及对比区域标识符并应用回退规则。
第三,浏览器会加载特定区域的数据。日期格式化器需要该区域的月份名称、星期名称和格式化模式。数字格式化器需要分组规则、小数点分隔符和数字字符。这些数据来自浏览器的内部本地化数据库,必须加载到内存中。
第四,浏览器会为格式化构建内部数据结构。它会将格式化模式编译为高效的表示形式,并为处理数值设置状态机。这些结构会在 formatter 的整个生命周期内持续存在。
每次创建 formatter 时,以上所有工作都会执行一次。如果你创建了一个 formatter,只用了一次就丢弃,那么所有这些初始化工作都被浪费了。
性能差异
当你需要格式化大量数值时,重复创建 formatter 的性能影响会变得明显。
来看一段没有复用 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);
});
toLocaleDateString() 方法会为每个要格式化的日期在内部创建一个新的 DateTimeFormat 实例。格式化三个日期就会创建三个 formatter,格式化一千个日期就会创建一千个 formatter。
对比一下只创建一个 formatter 并复用的代码。
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);
});
这段代码只创建一个 formatter,并重复使用三次。即使是格式化一千个日期,也只创建一个 formatter 并使用一千次。
随着需要格式化的数值数量增加,这两种方式的耗时差距会越来越大。为一千个日期各自创建 formatter 的耗时,可能会比复用同一个 formatter 多出 50 倍以上。
在模块作用域复用格式化器
最简单的复用 formatter 方式,是在模块作用域创建一次,然后在整个模块中复用。
// 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()]));
当你在代码中始终以相同方式格式化数值时,这种模式非常适用。formatter 会在应用程序的整个生命周期内存在,所有需要它的函数都可以使用同一个实例。
同样的模式适用于数字格式化器、列表格式化器以及所有其他 Intl 格式化器。
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);
}
在函数中复用格式化器
当你在代码的不同部分需要不同的格式化选项时,可以在函数内部创建格式化器,并利用闭包来保存它们。
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')));
这种模式适用于你希望复用已配置的格式化器,但又不想直接暴露格式化器本身的场景。
格式化器复用最重要的场景
在特定场景下,复用格式化器带来的收益最大。
第一个场景是循环。如果你在循环中格式化数据,每次迭代都新建格式化器会将开销按迭代次数放大。
// 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);
}
第二个场景是高频调用的函数。如果某个函数需要格式化数据且被频繁调用,复用格式化器可以避免每次调用都重新创建实例。
// 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);
}
第三个场景是处理大型数据集。当你需要格式化数百或数千个数据时,创建格式化器的初始化开销会占据总耗时的很大比例。
// 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)
}));
}
在这些场景下,复用格式化器可以显著减少格式化操作的耗时,提高应用响应速度。
按不同选项缓存格式化器
当你需要使用多种不同配置的格式化器时,可以根据配置缓存格式化器实例。
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));
这种模式可以让你在不同代码部分需要不同格式化配置时,依然获得格式化器复用的性能优势。
现代浏览器的优化
现代 JavaScript 引擎已经优化了 Intl 格式化器的创建过程,降低了性能开销。现在创建格式化器比旧版浏览器快得多。
不过,复用格式化器依然是最佳实践。即使经过优化,创建格式化器的开销仍然高于在已有格式化器上调用 format() 方法。虽然两者的成本差距比以前小了,但依然存在。
在高性能代码、循环执行的代码以及处理大型数据集的代码中,复用格式化器依然能带来显著的性能收益。格式化器创建的优化并没有消除复用的价值。
关键要点
创建 Intl 格式化器开销较大,因为浏览器需要解析选项、进行本地化协商、加载本地化数据,并构建内部数据结构。每次创建格式化器时,这些操作都会执行。
复用格式化器实例可以避免重复这些操作。你只需创建一次格式化器,然后多次调用其 format() 方法,从而减少格式化操作所需的时间。
格式化器复用在循环、频繁调用的函数以及处理大型数据集的代码中尤为重要。在这些场景下,创建格式化器的开销可能占据总执行时间的很大一部分。
最简单的复用模式是在模块作用域创建格式化器。对于更复杂的场景,可以根据配置选项使用闭包或缓存机制。