为什么应该重用格式化器而不是创建新的?

创建 Intl 格式化器成本很高,但重用相同的格式化器实例可以提高性能

介绍

当您在 JavaScript 中创建一个 Intl 格式化器时,浏览器会执行一些耗时的操作来设置格式化器实例。它会解析您的选项,从磁盘加载区域设置数据,并构建用于格式化的内部数据结构。如果每次需要格式化一个值时都创建一个新的格式化器,就会不必要地重复这些耗时的工作。

重用格式化器实例可以消除这些重复的工作。您只需创建一次格式化器,然后多次使用它。这种模式在循环中、频繁调用的函数中以及需要格式化大量值的高性能代码中尤为重要。

在创建新格式化器和重用现有格式化器之间的性能差异可能非常显著。在典型场景中,重用格式化器可以将格式化时间从几百毫秒减少到仅几毫秒。

为什么创建格式化器的成本很高

创建一个 Intl 格式化器涉及浏览器内部的几个高成本操作。

首先,浏览器会解析并验证您提供的选项。它会检查区域标识符是否有效,数值选项是否在范围内,以及不兼容的选项是否未被组合使用。这种验证需要字符串解析和查找操作。

其次,浏览器会执行区域协商。它会根据您请求的区域设置,找到浏览器支持的最佳匹配。这涉及比较区域标识符并应用回退规则。

第三,浏览器会加载特定于区域的本地化数据。日期格式化器需要月份名称、星期名称以及该区域的格式化模式。数字格式化器需要分组规则、小数分隔符和数字字符。这些数据来自浏览器的内部区域数据库,并需要加载到内存中。

第四,浏览器会为格式化构建内部数据结构。它将格式化模式编译为高效的表示形式,并设置用于处理值的状态机。这些结构在格式化器的生命周期内持续存在。

所有这些工作每次创建格式化器时都会发生。如果您创建一个格式化器,只使用一次然后丢弃,就会浪费所有这些设置工作。

性能差异

当格式化大量值时,重新创建格式化器的性能影响会变得显而易见。

考虑以下代码,它在不重用格式化器的情况下格式化一组日期。

const dates = [
  new Date('2024-01-15'),
  new Date('2024-02-20'),
  new Date('2024-03-10')
];

// 为每个日期创建一个新的格式化器
dates.forEach(date => {
  const formatted = date.toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
  console.log(formatted);
});

toLocaleDateString() 方法在内部为每个需要格式化的日期创建一个新的 DateTimeFormat 实例。对于三个日期,这会创建三个格式化器。对于一千个日期,这会创建一千个格式化器。

对比以下代码,它只创建一个格式化器并重复使用。

const dates = [
  new Date('2024-01-15'),
  new Date('2024-02-20'),
  new Date('2024-03-10')
];

// 只创建一次格式化器
const formatter = new Intl.DateTimeFormat('en-US', {
  year: 'numeric',
  month: 'long',
  day: 'numeric'
});

// 对每个日期重复使用格式化器
dates.forEach(date => {
  const formatted = formatter.format(date);
  console.log(formatted);
});

这段代码只创建一个格式化器并使用三次。对于一千个日期,它仍然只创建一个格式化器并使用一千次。

随着格式化值数量的增加,这两种方法之间的时间差异会越来越大。通过创建一千个格式化器来格式化一千个日期的时间可能是使用一个重用的格式化器的 50 倍以上。

在模块范围内重用格式化器

重用格式化器最简单的方法是,在模块范围内创建一次格式化器,并在整个模块中使用它。

// 在模块范围内创建格式化器
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));
}

// 所有函数共享同一个格式化器实例
console.log(formatDate(new Date()));
console.log(formatDates([new Date(), new Date()]));

当你在代码中以相同的方式格式化值时,这种模式非常有效。格式化器的生命周期与应用程序相同,每个需要它的函数都可以使用同一个实例。

同样的模式也适用于数字格式化器、列表格式化器以及所有其他的 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();

// 调用 createDateFormatter 时,格式化器只会被创建一次
// 每次调用 formatDate 都会重用同一个格式化器
console.log(formatDate(new Date('2024-01-15')));
console.log(formatDate(new Date('2024-02-20')));
console.log(formatDate(new Date('2024-03-10')));

这种模式在您希望创建一个可重用的配置格式化器但又不想暴露格式化器本身时非常有用。

格式化器重用最重要的场景

在特定场景下,重用格式化器的好处最为显著。

第一个场景是循环。如果您在循环中格式化值,每次迭代都创建一个新的格式化器会将成本乘以迭代次数。

// 效率低:创建了 N 个格式化器
for (let i = 0; i < 1000; i++) {
  const formatted = new Intl.NumberFormat('en-US').format(i);
  processValue(formatted);
}

// 高效:只创建了 1 个格式化器
const formatter = new Intl.NumberFormat('en-US');
for (let i = 0; i < 1000; i++) {
  const formatted = formatter.format(i);
  processValue(formatted);
}

第二个场景是频繁调用的函数。如果一个函数格式化值并被多次调用,重用格式化器可以避免每次调用都重新创建格式化器。

// 效率低:每次调用都会创建格式化器
function formatCurrency(amount) {
  const formatter = new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD'
  });
  return formatter.format(amount);
}

// 高效:只创建一次格式化器
const currencyFormatter = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
});

function formatCurrency(amount) {
  return currencyFormatter.format(amount);
}

第三个场景是处理大型数据集。当您格式化数百或数千个值时,创建格式化器的设置成本会占用总时间的很大一部分。

// 对于大型数据集效率低
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)
  }));
}

// 对于大型数据集效率高
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) {
  // 根据 locale 和 options 创建缓存键
  const key = JSON.stringify({ locale, options });

  // 如果缓存中存在格式化器,则返回缓存的格式化器
  if (formatterCache.has(key)) {
    return formatterCache.get(key);
  }

  // 创建新的格式化器并将其缓存
  const formatter = new Intl.NumberFormat(locale, options);
  formatterCache.set(key, formatter);
  return formatter;
}

// 第一次调用创建并缓存格式化器
const formatter1 = getNumberFormatter('en-US', { style: 'currency', currency: 'USD' });
console.log(formatter1.format(42.50));

// 第二次调用重用缓存的格式化器
const formatter2 = getNumberFormatter('en-US', { style: 'currency', currency: 'USD' });
console.log(formatter2.format(99.99));

// 不同的选项会创建并缓存一个新的格式化器
const formatter3 = getNumberFormatter('en-US', { style: 'percent' });
console.log(formatter3.format(0.42));

这种模式使您即使在代码的不同部分需要不同的格式化配置时,也能享受格式化器重用的好处。

现代浏览器的优化

现代 JavaScript 引擎已经优化了 Intl 格式化器的创建,以降低性能成本。创建格式化器的速度比旧版浏览器快得多。

然而,重用格式化器仍然是最佳实践。即使有了优化,创建格式化器仍然比在现有格式化器上调用 format() 方法更昂贵。成本差异比以前小,但仍然存在。

在高性能代码、循环中运行的代码以及处理大型数据集的代码中,重用格式化器仍然可以提供可衡量的好处。格式化器创建的优化并未消除重用格式化器的价值。

关键要点

创建 Intl 格式化器的成本很高,因为浏览器必须解析选项、执行区域设置协商、加载区域设置数据并构建内部数据结构。这些工作每次创建格式化器时都会发生。

重用格式化器实例可以避免重复这些工作。您只需创建一次格式化器,然后多次调用其 format() 方法。这可以减少格式化操作所花费的时间。

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

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