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

Библиотека Globalize поддерживает два подхода к созданию форматтеров и парсеров:

  1. Динамическое создание во время выполнения
  2. Предварительная компиляция (precompilation)

Во время обычной работы методы formatDate, formatNumber, numberParser, currencyFormatter и другие выполняют внутреннюю генерацию функций на основе CLDR-данных. Этот процесс требует:

  • анализа локали;
  • обработки шаблонов форматирования;
  • генерации внутренних AST-структур;
  • создания функций форматирования;
  • загрузки модулей CLDR.

Для браузерных приложений это означает:

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

Предварительная компиляция решает эти проблемы путём генерации готовых форматтеров на этапе сборки проекта.


Архитектура компиляции в Globalize

Globalize разделяет библиотеку на две части:

Часть Назначение
Runtime Выполнение готовых форматтеров
Compiler Генерация форматтеров

Во время разработки используется полный пакет с возможностью компиляции, а в production остаётся только runtime.

Схема работы:

CLDR JSON
    ↓
Globalize Compiler
    ↓
Сгенерированный JS-код
    ↓
Runtime Globalize
    ↓
Форматирование без компиляции

Основные преимущества предварительной компиляции

Уменьшение размера бандла

Без precompile приложение включает:

  • CLDR parser;
  • message compiler;
  • number/date compiler;
  • plural rules compiler.

После компиляции остаются только:

  • runtime-функции;
  • готовые форматтеры;
  • минимальные CLDR-данные.

Разница может составлять десятки или сотни килобайт.


Ускорение первого рендера

Динамическая генерация форматтера:

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

выполняет сложную внутреннюю работу при первом вызове.

Предварительно скомпилированный форматтер:

const formatter = compiledDateFormatter;

готов к использованию мгновенно.


Отсутствие runtime-компиляции

В production не требуется:

  • создавать AST;
  • разбирать шаблоны ICU;
  • вычислять plural rules;
  • генерировать функции.

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

  • мобильных устройств;
  • SSR;
  • embedded browser;
  • low-end hardware.

Повышение предсказуемости

Компиляция происходит на этапе CI/CD.

Ошибки обнаруживаются заранее:

  • отсутствие CLDR-данных;
  • неверные шаблоны;
  • некорректные plural forms;
  • ошибки ICU MessageFormat.

Установка компонентов компиляции

Основные пакеты

npm install globalize
npm install cldr-data
npm install globalize-compiler

Иногда используется отдельный runtime:

npm install globalize-runtime

Структура проекта

Типичная структура:

project/
├── src/
├── build/
├── locales/
├── cldr/
├── compiled/
├── webpack.config.js
└── package.json

Подготовка CLDR-данных

Globalize использует Unicode CLDR.

Минимальный набор:

const Cldr = require("cldrjs");

Cldr.load(
    require("cldr-data/supplemental/likelySubtags.json"),
    require("cldr-data/supplemental/numberingSystems.json"),
    require("cldr-data/main/en/numbers.json"),
    require("cldr-data/main/en/ca-gregorian.json")
);

Концепция compiler extractor

Предварительная компиляция требует знания:

  • какие форматтеры используются;
  • какие сообщения нужны;
  • какие локали должны поддерживаться.

Для этого применяется extractor.


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

Базовый пример

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

const compiler = new GlobalizeCompiler({
    defaultLocale: "en"
});

Компиляция number formatter

Исходный runtime-код

const formatter = Globalize("en").numberFormatter({
    minimumFractionDigits: 2
});

Во время выполнения создаётся formatter factory.


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

compiler.compile([
    {
        path: "numberFormatter",
        args: [
            {
                minimumFractionDigits: 2
            }
        ]
    }
]);

Результат компиляции

Компилятор генерирует Jav * aScript:

(function() {
    return function(value) {
        return value.toFixed(2);
    };
})();

Реальный код значительно сложнее, поскольку учитывает:

  • локаль;
  • grouping;
  • decimal separator;
  • NaN;
  • Infinity;
  • numbering systems.

Генерация файла runtime

Запись в compiled.js

const fs = require("fs");

const output = compiler.compile(...);

fs.writeFileSync(
    "./compiled/formatters.js",
    output
);

Использование готового runtime

Подключение

const Globalize = require("globalize-runtime");

const compiled = require("./compiled/formatters");

Вызов

compiled.numberFormatter(12345.67);

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

Runtime-подход

const formatter = Globalize("ru").dateFormatter({
    skeleton: "yMMMd"
});

Compile-time подход

compiler.compile([
    {
        path: "dateFormatter",
        locale: "ru",
        args: [
            {
                skeleton: "yMMMd"
            }
        ]
    }
]);

Особенности date formatter compilation

Компилятор заранее вычисляет:

  • календарные шаблоны;
  • month names;
  • timezone patterns;
  • era names;
  • numbering systems;
  • date symbols.

Во время runtime остаётся только подстановка значений.


Компиляция currency formatter

Runtime

Globalize("en").currencyFormatter("USD");

Precompile

compiler.compile([
    {
        path: "currencyFormatter",
        locale: "en",
        args: ["USD"]
    }
]);

Что компилируется внутри

Компилятор заранее определяет:

  • currency symbol;
  • fraction digits;
  • accounting format;
  • spacing rules;
  • plural currency names.

Компиляция plural generator

Runtime

Globalize("ru").plural(5);

Precompile

compiler.compile([
    {
        path: "pluralGenerator"
    }
]);

Сгенерированный plural runtime

Пример упрощённой логики:

function plural(n) {
    if (n % 10 === 1 && n % 100 !== 11) {
        return "one";
    }

    return "other";
}

Для реальных локалей правила значительно сложнее.


Компиляция messageFormatter

ICU MessageFormat

const formatter = Globalize("ru").messageFormatter(
    "{count, plural, one {# файл} few {# файла} many {# файлов}}"
);

Runtime-проблемы messageFormatter

ICU-компиляция — одна из самых тяжёлых операций в Globalize.

Необходимо:

  • парсить ICU AST;
  • вычислять plural categories;
  • генерировать JS-функции;
  • строить вложенные условия.

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

compiler.compile([
    {
        path: "messageFormatter",
        locale: "ru",
        args: [
            "{count, plural, one {# файл} few {# файла} many {# файлов}}"
        ]
    }
]);

Сгенерированный runtime-код

Упрощённый пример:

function(d) {
    const count = d.count;

    if (count === 1) {
        return count + " файл";
    }

    return count + " файлов";
}

Компиляция нескольких локалей

Конфигурация

const locales = ["en", "ru", "de"];

Генерация

locales.forEach(locale => {
    compiler.compile([
        {
            path: "numberFormatter",
            locale,
            args: []
        }
    ]);
});

Разделение по чанкам

Для крупных приложений локали обычно делятся:

compiled/
├── en.js
├── ru.js
├── de.js
└── fr.js

Lazy loading локалей

Динамический импорт

async function loadLocale(locale) {
    return import(`./compiled/${locale}.js`);
}

Интеграция с Webpack

Build script

const webpack = require("webpack");
const GlobalizePlugin = require("globalize-webpack-plugin");

Globalize Webpack Plugin

Плагин автоматизирует:

  • extraction;
  • compilation;
  • CLDR optimization;
  • runtime generation.

Настройка плагина

plugins: [
    new GlobalizePlugin({
        production: true,
        developmentLocale: "en"
    })
]

Что делает плагин

Во время сборки:

  1. Анализирует вызовы Globalize
  2. Извлекает formatter usage
  3. Генерирует runtime-код
  4. Удаляет compiler logic
  5. Оптимизирует CLDR

Tree shaking и precompile

Без предварительной компиляции tree shaking работает плохо, поскольку:

  • formatter factories динамические;
  • используются runtime-зависимости;
  • присутствуют compiler-модули.

После precompile код становится статическим.


Снижение runtime allocations

Dynamic formatter:

Globalize("en").numberFormatter();

может создавать:

  • closure;
  • internal cache;
  • parser state.

Compiled formatter:

compiledNumberFormatter(value);

обычно не требует дополнительных allocations.


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

Runtime cache

Без precompile часто используется:

const cache = {};

function getFormatter(locale) {
    if (!cache[locale]) {
        cache[locale] =
            Globalize(locale).numberFormatter();
    }

    return cache[locale];
}

При precompile кэш не нужен

Поскольку formatter уже готов:

import formatter from "./compiled/en";

formatter(10);

Оптимизация startup performance

На слабых устройствах startup bottleneck часто связан с:

  • parsing;
  • compilation;
  • hydration;
  • locale initialization.

Precompile уменьшает CPU-нагрузку при старте.


Runtime-only сборка

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

const Globalize = require("globalize-runtime");

Runtime package:

  • меньше;
  • быстрее;
  • не содержит compiler logic.

Ограничения runtime-only

Нельзя:

Globalize("en").messageFormatter(...);

если formatter не был предварительно скомпилирован.


Ошибки отсутствующего formatter

Типичная ошибка:

E_MISSING_CLDR

или:

Formatter not compiled

Подход compile once, run everywhere

Сгенерированные formatter-функции можно использовать:

  • в браузере;
  • в Node.js;
  • в Electron;
  • в SSR;
  • в serverless environments.

Предварительная компиляция сообщений приложения

Каталог сообщений

messages/
├── en.json
├── ru.json
└── de.json

Пример файла

{
    "greeting": "Hello {name}"
}

Компиляция message bundles

compiler.compile([
    {
        path: "messageFormatter",
        locale: "en",
        args: [
            "Hello {name}"
        ]
    }
]);

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

Обычно compile step выполняется:

npm run compile-i18n

перед:

npm run build

Incremental compilation

В крупных приложениях компилируются только изменённые локали.

Пример:

git diff
    ↓
changed locale files
    ↓
partial compile

Версионирование CLDR

CLDR регулярно обновляется.

Изменения могут затрагивать:

  • plural rules;
  • currency symbols;
  • numbering systems;
  • calendars.

После обновления CLDR необходима перекомпиляция formatter’ов.


Синхронизация runtime и compiler

Версии должны совпадать:

globalize
globalize-runtime
globalize-compiler

Иначе возможны несовместимости internal API.


Оптимизация размера CLDR

Необязательно подключать полный CLDR.

Можно оставить только:

  • нужные локали;
  • используемые calendars;
  • необходимые numbering systems.

Частичная загрузка данных

Пример минимального набора:

Cldr.load(
    require("cldr-data/main/en/numbers.json")
);

Анализ generated output

Сгенерированный код полезно проверять:

  • размер;
  • наличие дублирования;
  • повторяющиеся formatter-функции;
  • неиспользуемые locale chunks.

Минификация compiled runtime

Compiled output хорошо минифицируется:

terser compiled.js

поскольку содержит:

  • статические функции;
  • inline constants;
  • predictable branches.

Dead code elimination

Предварительная компиляция помогает bundler’ам удалять:

  • unused locales;
  • unused formatter factories;
  • compiler internals.

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

В SSR критична скорость обработки запроса.

Runtime compilation:

request
    ↓
formatter compile
    ↓
render

увеличивает latency.


SSR с precompiled formatter

request
    ↓
ready formatter
    ↓
render

CPU-нагрузка значительно ниже.


Использование в React-приложениях

Без precompile

const formatter =
    Globalize(locale).dateFormatter();

может вызываться внутри компонента.


С precompile

import { dateFormatter } from "./compiled/ru";

dateFormatter(date);

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

Hydration-sensitive приложения выигрывают за счёт:

  • уменьшения JS execution;
  • сокращения parsing time;
  • уменьшения main-thread blocking.

Benchmark-сравнение

Runtime generation

create formatter: 4ms
format call: 0.05ms

Precompiled formatter

create formatter: 0ms
format call: 0.05ms

Основная экономия — устранение стадии генерации.


Когда предварительная компиляция особенно важна

Большие SPA

Особенно при:

  • множестве локалей;
  • сложных ICU messages;
  • большом количестве formatter’ов.

Мобильные браузеры

На low-end Android runtime compilation может быть дорогой.


Electron

Снижается startup time desktop-приложения.


Serverless

Cold start становится меньше.


Когда precompile может быть избыточным

Небольшие проекты с:

  • одной локалью;
  • минимальным количеством formatter’ов;
  • отсутствием ICU messages.

Типичная production-схема

source messages
    ↓
extract
    ↓
compile
    ↓
generate runtime
    ↓
webpack bundle
    ↓
deploy

Практическая структура compiled runtime

compiled/
├── formatters/
│   ├── number.js
│   ├── date.js
│   └── currency.js
├── messages/
│   ├── en.js
│   └── ru.js
└── locales/
    ├── en.js
    └── ru.js

Стратегия разделения formatter’ов

Крупные проекты обычно разделяют:

  • date formatters;
  • number formatters;
  • currency formatters;
  • message bundles.

Это позволяет:

  • lazy load;
  • granular caching;
  • code splitting.

Типичные ошибки

Несовпадение locale data

Missing required CLDR content

Причина:

  • locale не загружен;
  • отсутствует supplemental data.

Runtime/compiler mismatch

Cannot read property of undefined

Причина:

  • разные версии пакетов.

Не скомпилирован formatter

Formatter not found

Причина:

  • formatter не попал в extraction phase.

Рекомендации по production-использованию

Компилировать все ICU messages заранее

Это наиболее тяжёлая часть runtime.


Разделять locale bundles

Особенно для SPA.


Использовать runtime-only пакет

globalize-runtime

вместо полного:

globalize

Минимизировать CLDR

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


Автоматизировать compile step

Через:

  • webpack;
  • Vite;
  • Rollup;
  • CI pipeline.

Общая схема production-оптимизации

CLDR minimization
        +
message precompile
        +
formatter precompile
        +
locale chunking
        +
runtime-only package
        ↓
минимальный i18n overhead