なぜ新しいフォーマッターを作成するのではなく、フォーマッターを再利用すべきなのか?

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

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

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

このコードは1つのフォーマッターを作成し、それを3回使用します。1000の日付に対しても、1つのフォーマッターを作成し、それを1000回使用するだけです。

これらのアプローチ間の時間差は、フォーマットする値の数が増えるほど大きくなります。1000のフォーマッターを作成して1000の日付をフォーマットすることは、1つの再利用されるフォーマッターでフォーマットするよりも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);
}

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

// 非効率的: 毎回の呼び出しでフォーマッターを作成
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);
}

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

// 大規模データセットには非効率的
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) {
  // ロケールとオプションからキャッシュキーを作成
  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));

// 2回目の呼び出しではキャッシュされたフォーマッターを再利用する
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()メソッドを何度も呼び出します。これによりフォーマット操作にかかる時間が短縮されます。

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

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