新しいフォーマッターを作成する代わりに、フォーマッターを再利用すべき理由

Intlフォーマッターの作成にはコストがかかりますが、同じフォーマッターインスタンスを再利用することでパフォーマンスが向上します

はじめに

JavaScriptでIntlフォーマッターを作成すると、ブラウザはフォーマッターインスタンスをセットアップするために高コストな操作を実行します。オプションを解析し、ディスクからロケールデータを読み込み、フォーマット用の内部データ構造を構築します。値をフォーマットする必要があるたびに新しいフォーマッターを作成すると、この高コストな作業を不必要に繰り返すことになります。

フォーマッターインスタンスを再利用することで、この繰り返し作業を排除できます。フォーマッターを一度作成し、何度も使用します。このパターンは、ループ、頻繁に呼び出される関数、多くの値をフォーマットする高パフォーマンスコードで特に重要です。

新しいフォーマッターを作成する場合と既存のフォーマッターを再利用する場合のパフォーマンス差は大きくなる可能性があります。一般的なシナリオでは、フォーマッターを再利用することで、フォーマット時間を数百ミリ秒からわずか数ミリ秒に短縮できます。

フォーマッターの作成にコストがかかる理由

Intlフォーマッターの作成には、ブラウザ内部で発生するいくつかの高コストな操作が含まれます。

まず、ブラウザは提供されたオプションを解析して検証します。ロケール識別子が有効であること、数値オプションが範囲内にあること、互換性のないオプションが組み合わされていないことを確認します。この検証には文字列解析とルックアップ操作が必要です。

次に、ブラウザはロケールネゴシエーションを実行します。要求されたロケールを受け取り、ブラウザがサポートするロケールから最適な一致を見つけます。これには、ロケール識別子の比較とフォールバックルールの適用が含まれます。

3番目に、ブラウザはロケール固有のデータを読み込みます。日付フォーマッターには、ロケールの月名、曜日名、フォーマットパターンが必要です。数値フォーマッターには、グループ化ルール、小数点区切り文字、数字文字が必要です。このデータはブラウザの内部ロケールデータベースから取得され、メモリに読み込まれる必要があります。

第4に、ブラウザはフォーマット用の内部データ構造を構築します。フォーマットパターンを効率的な表現にコンパイルし、値を処理するためのステートマシンをセットアップします。これらの構造は、フォーマッターの存続期間中持続します。

この作業はすべて、フォーマッターを作成するたびに発生します。フォーマッターを作成し、一度使用して破棄すると、すべてのセットアップ作業が無駄になります。

パフォーマンスの違い

フォーマッターを再作成することによるパフォーマンスへの影響は、多くの値をフォーマットする際に顕著になります。

フォーマッターを再利用せずに日付のリストをフォーマットするコードを考えてみましょう。

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インスタンスを作成します。3つの日付の場合、3つのフォーマッターが作成されます。1000個の日付の場合、1000個のフォーマッターが作成されます。

これを、1つのフォーマッターを作成して再利用するコードと比較してみましょう。

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

このコードは1つのフォーマッターを作成し、それを3回使用します。1000個の日付の場合でも、1つのフォーマッターを作成し、それを1000回使用します。

これらのアプローチ間の時間差は、フォーマットする値の数に応じて増大します。1000個のフォーマッターを作成して1000個の日付をフォーマットすると、1つの再利用されたフォーマッターでフォーマットする場合と比較して、50倍以上の時間がかかることがあります。

モジュールスコープでのフォーマッターの再利用

フォーマッターを再利用する最もシンプルな方法は、モジュールスコープで一度作成し、モジュール全体で使用することです。

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

このパターンは、コード全体で同じ方法で値をフォーマットする場合に適しています。フォーマッターはアプリケーションの存続期間中存在し、それを必要とするすべての関数が同じインスタンスを使用できます。

同じパターンは、数値フォーマッター、リストフォーマッター、その他すべての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);
}

2番目のシナリオは、頻繁に呼び出される関数です。関数が値をフォーマットし、何度も呼び出される場合、フォーマッターの再利用により、呼び出しごとにフォーマッターを再作成することを回避できます。

// 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);
}

3番目のシナリオは、大規模なデータセットの処理です。数百または数千の値をフォーマットする場合、フォーマッターを作成するセットアップコストが全体時間の大部分を占めるようになります。

// 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()メソッドを何度も呼び出します。これにより、フォーマット処理に費やす時間が削減されます。

フォーマッターの再利用は、ループ内、頻繁に呼び出される関数、大規模なデータセットを処理するコードで最も重要になります。これらのシナリオでは、フォーマッター作成のコストが総実行時間の大部分を占める可能性があります。

最も簡単な再利用パターンは、モジュールスコープでフォーマッターを作成することです。より複雑なシナリオでは、クロージャや設定オプションに基づくキャッシングを使用できます。