为什么要复用格式化器而不是每次新建?

创建 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() 方法,从而减少格式化操作所需的时间。

格式化器复用在循环、频繁调用的函数以及处理大型数据集的代码中尤为重要。在这些场景下,创建格式化器的开销可能占据总执行时间的很大一部分。

最简单的复用模式是在模块作用域创建格式化器。对于更复杂的场景,可以根据配置选项使用闭包或缓存机制。