¿Por qué deberías reutilizar formateadores en lugar de crear nuevos?

Crear formateadores Intl es costoso, pero reutilizar la misma instancia de formateador mejora el rendimiento

Introducción

Cuando creas un formateador Intl en JavaScript, el navegador realiza operaciones costosas para configurar la instancia del formateador. Analiza tus opciones, carga datos de configuración regional desde el disco y construye estructuras de datos internas para el formateo. Si creas un nuevo formateador cada vez que necesitas formatear un valor, repites este trabajo costoso innecesariamente.

Reutilizar instancias de formateadores elimina este trabajo repetitivo. Creas el formateador una vez y lo utilizas muchas veces. Este patrón es particularmente importante en bucles, funciones frecuentemente llamadas y código de alto rendimiento donde formateas muchos valores.

La diferencia de rendimiento entre crear nuevos formateadores y reutilizar los existentes puede ser sustancial. En escenarios típicos, reutilizar formateadores puede reducir el tiempo de formateo de cientos de milisegundos a solo unos pocos milisegundos.

Por qué crear formateadores es costoso

Crear un formateador Intl implica varias operaciones costosas que ocurren dentro del navegador.

Primero, el navegador analiza y valida las opciones que proporcionas. Comprueba que los identificadores de configuración regional sean válidos, que las opciones numéricas estén dentro del rango y que no se combinen opciones incompatibles. Esta validación requiere análisis de cadenas y operaciones de búsqueda.

Segundo, el navegador realiza la negociación de configuración regional. Toma la configuración regional solicitada y encuentra la mejor coincidencia disponible entre las configuraciones regionales que el navegador soporta. Esto implica comparar identificadores de configuración regional y aplicar reglas de respaldo.

Tercero, el navegador carga datos específicos de la configuración regional. Los formateadores de fecha necesitan nombres de meses, nombres de días y patrones de formateo para la configuración regional. Los formateadores de números necesitan reglas de agrupación, separadores decimales y caracteres de dígitos. Estos datos provienen de la base de datos interna de configuración regional del navegador y deben cargarse en memoria.

Cuarto, el navegador construye estructuras de datos internas para el formateo. Compila patrones de formateo en representaciones eficientes y configura máquinas de estado para procesar valores. Estas estructuras persisten durante la vida útil del formateador.

Todo este trabajo ocurre cada vez que creas un formateador. Si creas un formateador, lo usas una vez y lo descartas, desperdicias todo ese trabajo de configuración.

La diferencia de rendimiento

El impacto en el rendimiento de recrear formateadores se vuelve visible cuando formateas muchos valores.

Considera un código que formatea una lista de fechas sin reutilizar el formateador.

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

// Crea un nuevo formateador para cada fecha
dates.forEach(date => {
  const formatted = date.toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  });
  console.log(formatted);
});

El método toLocaleDateString() crea internamente una nueva instancia de DateTimeFormat para cada fecha que formatea. Para tres fechas, esto crea tres formateadores. Para mil fechas, crea mil formateadores.

Compara esto con un código que crea un formateador y lo reutiliza.

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

// Crea el formateador una sola vez
const formatter = new Intl.DateTimeFormat('en-US', {
  year: 'numeric',
  month: 'long',
  day: 'numeric'
});

// Reutiliza el formateador para cada fecha
dates.forEach(date => {
  const formatted = formatter.format(date);
  console.log(formatted);
});

Este código crea un formateador y lo utiliza tres veces. Para mil fechas, sigue creando un solo formateador y lo utiliza mil veces.

La diferencia de tiempo entre estos enfoques crece con el número de valores que formateas. Formatear mil fechas creando mil formateadores puede tardar más de 50 veces más que formatearlas con un formateador reutilizado.

Reutilización de formateadores a nivel de módulo

La forma más simple de reutilizar un formateador es crearlo una vez a nivel de módulo y utilizarlo en todo el módulo.

// Crea el formateador a nivel de módulo
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));
}

// Todas las funciones comparten la misma instancia del formateador
console.log(formatDate(new Date()));
console.log(formatDates([new Date(), new Date()]));

Este patrón funciona bien cuando formateas valores de la misma manera en todo tu código. El formateador existe durante toda la vida útil de tu aplicación, y cada función que lo necesite puede usar la misma instancia.

El mismo patrón funciona para formateadores de números, formateadores de listas y todos los demás formateadores de 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);
}

Reutilización de formateadores en funciones

Cuando necesitas diferentes opciones de formato en distintas partes de tu código, puedes crear formateadores dentro de funciones y confiar en los cierres (closures) para preservarlos.

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();

// El formateador se crea una vez cuando llamas a createDateFormatter
// Cada llamada a formatDate reutiliza el mismo formateador
console.log(formatDate(new Date('2024-01-15')));
console.log(formatDate(new Date('2024-02-20')));
console.log(formatDate(new Date('2024-03-10')));

Este patrón es útil cuando quieres crear un formateador configurado que se reutilice pero no deseas exponer el formateador en sí.

Cuándo la reutilización de formateadores es más importante

La reutilización de formateadores proporciona el mayor beneficio en escenarios específicos.

El primer escenario son los bucles. Si formateas valores dentro de un bucle, crear un nuevo formateador en cada iteración multiplica el costo por el número de iteraciones.

// Ineficiente: crea N formateadores
for (let i = 0; i < 1000; i++) {
  const formatted = new Intl.NumberFormat('en-US').format(i);
  processValue(formatted);
}

// Eficiente: crea 1 formateador
const formatter = new Intl.NumberFormat('en-US');
for (let i = 0; i < 1000; i++) {
  const formatted = formatter.format(i);
  processValue(formatted);
}

El segundo escenario son las funciones llamadas frecuentemente. Si una función formatea valores y se llama muchas veces, la reutilización del formateador evita recrearlo en cada llamada.

// Ineficiente: crea el formateador en cada llamada
function formatCurrency(amount) {
  const formatter = new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD'
  });
  return formatter.format(amount);
}

// Eficiente: crea el formateador una sola vez
const currencyFormatter = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
});

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

El tercer escenario es el procesamiento de grandes conjuntos de datos. Cuando formateas cientos o miles de valores, el costo de configuración de crear formateadores se convierte en una parte significativa del tiempo total.

// Ineficiente para grandes conjuntos de datos
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)
  }));
}

// Eficiente para grandes conjuntos de datos
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)
  }));
}

En estos escenarios, la reutilización de formateadores reduce el tiempo dedicado a las operaciones de formato y mejora la capacidad de respuesta de la aplicación.

Almacenamiento en caché de formateadores con diferentes opciones

Cuando necesitas utilizar formateadores con muchas combinaciones diferentes de opciones, puedes almacenar en caché los formateadores según su configuración.

const formatterCache = new Map();

function getNumberFormatter(locale, options) {
  // Crear una clave de caché a partir del locale y las opciones
  const key = JSON.stringify({ locale, options });

  // Devolver el formateador en caché si existe
  if (formatterCache.has(key)) {
    return formatterCache.get(key);
  }

  // Crear un nuevo formateador y almacenarlo en caché
  const formatter = new Intl.NumberFormat(locale, options);
  formatterCache.set(key, formatter);
  return formatter;
}

// La primera llamada crea y almacena en caché el formateador
const formatter1 = getNumberFormatter('en-US', { style: 'currency', currency: 'USD' });
console.log(formatter1.format(42.50));

// La segunda llamada reutiliza el formateador en caché
const formatter2 = getNumberFormatter('en-US', { style: 'currency', currency: 'USD' });
console.log(formatter2.format(99.99));

// Diferentes opciones crean y almacenan en caché un nuevo formateador
const formatter3 = getNumberFormatter('en-US', { style: 'percent' });
console.log(formatter3.format(0.42));

Este patrón te permite obtener los beneficios de la reutilización de formateadores incluso cuando necesitas diferentes configuraciones de formato en distintas partes de tu código.

Optimizaciones en navegadores modernos

Los motores de JavaScript modernos han optimizado la creación de formateadores Intl para reducir el costo de rendimiento. La creación de formateadores es más rápida hoy que en navegadores antiguos.

Sin embargo, reutilizar formateadores sigue siendo una mejor práctica. Incluso con optimizaciones, crear un formateador sigue siendo más costoso que llamar al método format() en un formateador existente. La diferencia de costo es menor de lo que solía ser, pero aún existe.

En código de alto rendimiento, código que se ejecuta en bucles y código que procesa grandes conjuntos de datos, la reutilización de formateadores continúa proporcionando beneficios medibles. La optimización de la creación de formateadores no elimina el valor de reutilizarlos.

Puntos clave

Crear formateadores Intl es costoso porque el navegador debe analizar las opciones, realizar la negociación de configuración regional, cargar datos de localización y construir estructuras de datos internas. Este trabajo ocurre cada vez que creas un formateador.

Reutilizar instancias de formateadores evita repetir este trabajo. Creas el formateador una vez y llamas a su método format() muchas veces. Esto reduce el tiempo dedicado a operaciones de formateo.

La reutilización de formateadores es más importante en bucles, funciones frecuentemente llamadas y código que procesa grandes conjuntos de datos. En estos escenarios, el costo de crear formateadores puede convertirse en una parte significativa del tiempo total de ejecución.

El patrón de reutilización más simple es crear formateadores en el ámbito del módulo. Para escenarios más complejos, puedes usar clausuras o almacenamiento en caché basado en opciones de configuración.