Оптимизация производительности

Библиотека Globalize предоставляет мощный механизм интернационализации на основе стандарта CLDR и библиотеки ICU. Однако высокая гибкость сопровождается дополнительными вычислительными затратами. Основные проблемы производительности возникают в следующих ситуациях:

  • загрузка большого объёма CLDR-данных;
  • повторное создание форматтеров;
  • избыточная инициализация локалей;
  • синхронная загрузка JSON-файлов;
  • частые вызовы форматирования в циклах;
  • работа с крупными наборами чисел и дат;
  • использование всех модулей библиотеки вместо выборочной сборки.

Оптимизация в проектах с Globalize обычно сводится к уменьшению:

  • количества загружаемых данных;
  • числа создаваемых объектов;
  • времени парсинга CLDR;
  • количества повторных операций форматирования.

Стоимость загрузки CLDR-данных

Globalize использует данные Unicode CLDR. Полный набор локализационных файлов может занимать несколько мегабайт.

Пример неэффективной загрузки:

const Globalize = require("globalize");

Globalize.load(
    require("cldr-data"),
);

Подобный подход загружает:

  • все локали;
  • все календари;
  • все числовые системы;
  • все правила pluralization;
  • все временные зоны.

В браузере это критически увеличивает:

  • размер bundle;
  • время загрузки страницы;
  • время инициализации приложения.

Выборочная загрузка CLDR

Наиболее важная оптимизация — загрузка только необходимых данных.

Минимальный набор для чисел

const Globalize = require("globalize");

Globalize.load(
    require("cldr-data/main/en/numbers.json"),
    require("cldr-data/supplemental/numberingSystems.json")
);

Globalize.locale("en");

Минимальный набор для дат

Globalize.load(
    require("cldr-data/main/en/ca-gregorian.json"),
    require("cldr-data/main/en/timeZoneNames.json"),
    require("cldr-data/supplemental/timeData.json"),
    require("cldr-data/supplemental/weekData.json")
);

Преимущества подхода

  • уменьшение bundle size;
  • сокращение времени parse/eval;
  • уменьшение потребления памяти;
  • ускорение старта приложения.

Использование только нужных модулей

Globalize состоит из отдельных модулей:

  • globalize/number
  • globalize/date
  • globalize/currency
  • globalize/plural
  • globalize/message
  • globalize/relative-time

Полный импорт:

const Globalize = require("globalize");

Подключает значительную часть библиотеки.

Оптимизированный вариант:

const Globalize = require("globalize/dist/globalize/number");

Или:

import Globalize from "globalize/dist/globalize/number";

Результат

Tree shaking начинает работать эффективнее:

  • уменьшается итоговый JS-файл;
  • снижается количество зависимостей;
  • ускоряется выполнение.

Кэширование форматтеров

Создание formatter-функций — дорогая операция.

Неэффективный код:

function formatPrice(value) {
    return Globalize
        .numberFormatter({
            style: "currency",
            currency: "USD"
        })(value);
}

Здесь formatter создаётся при каждом вызове.


Правильное кэширование

const currencyFormatter = Globalize.numberFormatter({
    style: "currency",
    currency: "USD"
});

function formatPrice(value) {
    return currencyFormatter(value);
}

Что оптимизируется

  • уменьшается количество аллокаций;
  • снижается нагрузка на garbage collector;
  • сокращается время выполнения;
  • уменьшается потребление памяти.

Повторное использование formatter-объектов

Особенно важно в:

  • React;
  • Vue;
  • Angular;
  • Node.js API;
  • SSR-приложениях.

Плохой пример внутри React-компонента:

function Price({ value }) {
    const formatter = Globalize.numberFormatter();

    return <span>{formatter(value)}</span>;
}

Formatter будет создаваться при каждом render.

Оптимизированный вариант:

const formatter = Globalize.numberFormatter();

function Price({ value }) {
    return <span>{formatter(value)}</span>;
}

Lazy initialization

Иногда formatter нужен не сразу.

Отложенная инициализация

let formatter = null;

function getFormatter() {
    if (!formatter) {
        formatter = Globalize.numberFormatter();
    }

    return formatter;
}

Когда это полезно

  • крупные SPA;
  • SSR;
  • динамические модули;
  • редко используемые страницы;
  • административные панели.

Предварительная компиляция форматтеров

Globalize поддерживает компиляцию через пакет:

globalize-compiler

Это один из наиболее мощных механизмов оптимизации.

Обычная работа

Во время выполнения:

  1. читаются CLDR-данные;
  2. строится formatter;
  3. генерируются внутренние структуры;
  4. выполняется интерпретация шаблонов.

Скомпилированный режим

В production formatter генерируется заранее.


Пример компиляции

const GlobalizeCompiler = require("globalize-compiler");

Результатом становится готовый JS-код:

function format(value) {
    return "$" + value.toFixed(2);
}

Преимущества

  • резкое ускорение runtime;
  • отсутствие сложной инициализации;
  • уменьшение нагрузки на CPU;
  • улучшение Time To Interactive.

Использование globalize-compiler в production

Наиболее эффективная архитектура:

Этап Выполнение
Компиляция formatter build-time
Загрузка CLDR build-time
Генерация функций build-time
Форматирование runtime

Минимизация локалей

Ошибка многих проектов — загрузка всех языков.

Плохой подход:

Globalize.load(
    enData,
    frData,
    deData,
    ruData,
    esData,
    zhData,
    jaData
);

Если пользователь использует только одну локаль, остальные данные загружаются бессмысленно.


Динамическая загрузка локалей

Пример с dynamic import

async function loadLocale(locale) {
    const data = await import(`./locales/${locale}.json`);

    Globalize.load(data.default);

    Globalize.locale(locale);
}

Эффект

  • уменьшение initial bundle;
  • ускорение first paint;
  • более быстрый startup.

Code Splitting

Globalize хорошо сочетается с:

  • Webpack;
  • Vite;
  • Rollup;
  • Parcel.

Разделение локалей

const localeLoaders = {
    en: () => import("./i18n/en"),
    ru: () => import("./i18n/ru"),
    de: () => import("./i18n/de")
};

Устранение повторной загрузки данных

Иногда приложение несколько раз вызывает:

Globalize.load(...)

Это приводит к:

  • повторному merge;
  • дополнительному парсингу;
  • росту памяти.

Оптимизация

Создание единого bootstrap-модуля:

// i18n.js

Globalize.load(...);

Globalize.locale("en");

export default Globalize;

Оптимизация работы с датами

Форматирование дат — одна из самых дорогих операций.

Неэффективный вариант:

items.map(item => {
    return Globalize.formatDate(item.date, {
        datetime: "medium"
    });
});

Каждый вызов создаёт внутренний formatter.


Предварительное создание formatter

const dateFormatter = Globalize.dateFormatter({
    datetime: "medium"
});

items.map(item => {
    return dateFormatter(item.date);
});

Массовое форматирование

При обработке тысяч значений эффективность становится критической.

Неэффективный код

for (let i = 0; i < values.length; i++) {
    console.log(
        Globalize.formatNumber(values[i])
    );
}

Оптимизированный вариант

const formatter = Globalize.numberFormatter();

for (let i = 0; i < values.length; i++) {
    console.log(
        formatter(values[i])
    );
}

Избежание форматирования во время render

Проблема UI-фреймворков — дорогое форматирование при перерисовке.

Плохой подход

render() {
    return users.map(user => (
        <div>
            {Globalize.formatDate(user.created)}
        </div>
    ));
}

Memoization

React-пример

const formatter = Globalize.dateFormatter({
    datetime: "short"
});

const formatted = useMemo(() => {
    return formatter(date);
}, [date]);

Эффект

  • меньше re-render;
  • меньше CPU usage;
  • более плавный UI.

Оптимизация plural rules

Pluralization тоже создаёт внутренние структуры.

Неэффективно:

function getPlural(count) {
    return Globalize.plural(count);
}

Оптимизированный вариант:

const pluralGenerator = Globalize.pluralGenerator();

function getPlural(count) {
    return pluralGenerator(count);
}

Снижение нагрузки на память

Globalize хранит:

  • CLDR-структуры;
  • formatter cache;
  • internal parser trees.

При большом количестве локалей память может быстро расти.

Методы оптимизации

Использование одной активной локали

Globalize.locale(currentLocale);

Очистка ненужных модулей

Разделение:

  • admin locale pack;
  • user locale pack;
  • analytics locale pack.

Использование CDN для локалей

Крупные JSON-файлы локализации можно вынести отдельно.

Пример

fetch("/cldr/ru.json")

Преимущества

  • кэширование браузером;
  • уменьшение JS bundle;
  • независимое обновление локалей.

Оптимизация SSR

В SSR каждый request может создавать formatter заново.

Неэффективно

app.get("/", (req, res) => {
    const formatter = Globalize.numberFormatter();

    res.send(formatter(1000));
});

Кэш на уровне процесса

const formatter = Globalize.numberFormatter();

app.get("/", (req, res) => {
    res.send(formatter(1000));
});

Использование singleton-подхода

Централизованный i18n-модуль

class I18nService {
    constructor() {
        this.currencyFormatter =
            Globalize.numberFormatter({
                style: "currency",
                currency: "USD"
            });
    }

    formatPrice(value) {
        return this.currencyFormatter(value);
    }
}

export default new I18nService();

Benchmarking производительности

Без измерений оптимизация теряет смысл.

Измерение времени

console.time("format");

for (let i = 0; i < 100000; i++) {
    formatter(i);
}

console.timeEnd("format");

Сравнение formatter creation vs reuse

Типичный результат:

Операция Время
Создание formatter 100000 раз очень медленно
Повторное использование в десятки раз быстрее

Использование Web Workers

При сложной локализации:

  • больших таблиц;
  • аналитики;
  • финансовых отчётов;
  • BI-интерфейсов;

форматирование можно переносить в worker.

Пример

worker.postMessage(data);

Внутри worker:

const formatter =
    Globalize.numberFormatter();

Результат

Главный поток UI остаётся свободным.


Оптимизация bundle size

Globalize может существенно увеличивать размер приложения.

Основные методы уменьшения

1. Tree shaking

import Globalize from
    "globalize/dist/globalize/number";

2. Dynamic import

await import("./locales/ru");

3. Исключение ненужных модулей

Не подключать:

  • relative-time;
  • currency;
  • message;
  • plural;

если они не используются.


Анализ bundle size

Полезные инструменты:

  • webpack-bundle-analyzer;
  • vite-bundle-visualizer;
  • source-map-explorer.

Они помогают выявить:

  • дубли CLDR;
  • неиспользуемые локали;
  • повторные импорты.

Оптимизация сообщений MessageFormat

Сообщения ICU-format могут быть дорогими.

Неэффективно:

Globalize.messageFormatter("hello");

при каждом вызове.


Кэширование message formatter

const helloFormatter =
    Globalize.messageFormatter("hello");

helloFormatter({
    name: "John"
});

Предкомпиляция сообщений

Особенно важна для:

  • больших словарей;
  • enterprise-приложений;
  • CRM;
  • ERP;
  • e-commerce.

Что устраняется

  • runtime parsing;
  • AST generation;
  • message compilation.

Профилирование

При оптимизации Globalize важно анализировать:

  • CPU profile;
  • memory allocation;
  • scripting time;
  • GC activity.

Инструменты

  • Chrome DevTools;
  • Lighthouse;
  • Node.js profiler;
  • Performance API.

Типичная стратегия оптимизации

Development

  • полные локали;
  • runtime formatter;
  • удобная отладка.

Production

  • compiled formatters;
  • code splitting;
  • lazy loading;
  • formatter cache;
  • минимальный CLDR.

Архитектура высокопроизводительной интернационализации

Эффективная схема обычно выглядит так:

Build Stage
 ├── CLDR extraction
 ├── Message compilation
 ├── Formatter compilation
 └── Locale splitting

Runtime
 ├── Lazy locale loading
 ├── Cached formatter usage
 ├── Singleton i18n service
 └── Minimal allocations

Практические рекомендации

Критически важные оптимизации

Оптимизация Эффект
Кэширование formatter очень высокий
Минимизация CLDR очень высокий
Предкомпиляция очень высокий
Dynamic import высокий
Code splitting высокий
Lazy initialization средний
Web Workers зависит от нагрузки

Частые ошибки

Создание formatter в циклах

for (...) {
    Globalize.numberFormatter();
}

Загрузка всех локалей

require("cldr-data");

Форматирование внутри render

render() {
    Globalize.formatDate(...);
}

Повторная инициализация locale

Globalize.locale("en");
Globalize.locale("en");
Globalize.locale("en");

Оптимизация для мобильных устройств

На слабых устройствах проблемы проявляются сильнее:

  • медленный CPU;
  • ограниченная память;
  • высокая стоимость parse/eval.

Особенно важно:

  • минимизировать bundle;
  • уменьшать JSON;
  • избегать runtime compilation;
  • использовать lazy loading.

Производительность Node.js

В серверных приложениях важны:

  • throughput;
  • latency;
  • memory footprint.

Основные рекомендации

  • singleton formatter;
  • shared locale cache;
  • build-time compilation;
  • отказ от repeated initialization.

Сравнение runtime и compile-time подходов

Подход Скорость
Runtime Globalize ниже
Compiled Globalize очень высокая

Паттерн централизованного formatter registry

class FormatterRegistry {
    constructor() {
        this.cache = new Map();
    }

    getCurrencyFormatter(currency) {
        if (!this.cache.has(currency)) {
            this.cache.set(
                currency,
                Globalize.numberFormatter({
                    style: "currency",
                    currency
                })
            );
        }

        return this.cache.get(currency);
    }
}

Преимущества

  • отсутствие дублирования;
  • контролируемый cache;
  • уменьшение памяти;
  • единая точка управления formatter-объектами.