Опция pure: пометка функций как чистых

Опция pure в Esbuild предназначена для пометки определённых вызовов функций как не имеющих побочных эффектов (pure functions). Такая информация позволяет сборщику безопасно удалять результаты вызовов этих функций, если они нигде не используются.

Механизм особенно полезен во время минификации и удаления мёртвого кода (dead code elimination), когда в проекте присутствуют вызовы функций, создающих значения, которые затем игнорируются.

Пример исходного кода:

createObject();

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

При указании функции как чистой:

esbuild.build({
  entryPoints: ['app.js'],
  bundle: true,
  minify: true,
  pure: ['createObject']
});

Esbuild получает информацию о том, что вызов безопасно удалить.

После оптимизации:

// удалено

Что считается чистой функцией

Чистая функция обладает двумя основными свойствами:

  1. Не изменяет внешнее состояние.
  2. При одинаковых входных данных всегда возвращает одинаковый результат.

Примеры чистых функций:

function sum(a, b) {
  return a + b;
}

function square(x) {
  return x * x;
}

Примеры функций с побочными эффектами:

function logMessage(text) {
  console.log(text);
}

function saveData(data) {
  database.save(data);
}

function incrementCounter() {
  counter++;
}

Если подобные функции ошибочно объявить как чистые, Esbuild может удалить важную логику приложения.


Базовый синтаксис

Опция принимает массив строк.

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  pure: [
    'console.log',
    'debug',
    'trackEvent'
  ]
});

Каждая строка представляет имя функции или метода, вызовы которого следует считать чистыми.


Работа с глобальными функциями

Предположим, существует функция для отладочного вывода:

debug('Application started');

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

esbuild.build({
  minify: true,
  pure: ['debug']
});

Результат:

// вызов удалён

Если функция использовалась только ради побочного эффекта, такой код полностью исчезнет из финального бандла.


Работа с методами объектов

Поддерживается указание цепочек свойств.

Исходный код:

console.log('Debug information');

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

esbuild.build({
  minify: true,
  pure: ['console.log']
});

После минификации:

// удалено

Это распространённый способ убрать отладочные сообщения из production-сборки.


Пример с аналитикой

Иногда в проекте присутствует большое количество диагностических вызовов:

analytics.debug('User loaded');
analytics.debug('Settings opened');
analytics.debug('Modal shown');

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

esbuild.build({
  minify: true,
  pure: ['analytics.debug']
});

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


Взаимодействие с минификацией

Наиболее заметный эффект опция даёт вместе с:

minify: true

Пример:

createObject();

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

esbuild.build({
  minify: true,
  pure: ['createObject']
});

Результат:

Без минификации оптимизация может не выполняться в полном объёме.

Типичная production-конфигурация:

esbuild.build({
  bundle: true,
  minify: true,
  pure: [
    'console.log',
    'console.debug',
    'debug'
  ]
});

Удаление только вызова, а не аргументов

Важно понимать, что Esbuild анализирует не только сам вызов функции, но и её аргументы.

Рассмотрим пример:

debug(getData());

Даже если debug объявлена как чистая:

pure: ['debug']

Esbuild обязан проверить, имеют ли аргументы побочные эффекты.

Если функция:

function getData() {
  console.log('Loading...');
  return {};
}

содержит побочный эффект, вызов может быть преобразован примерно так:

getData();

В этом случае удаляется только внешний вызов debug, а вычисление аргумента сохраняется.


Полное удаление цепочки вычислений

Если все элементы выражения признаны безопасными:

debug(createObject());

и обе функции отмечены как чистые:

pure: [
  'debug',
  'createObject'
]

результат может быть полностью удалён:

Так достигается максимальное сокращение размера бандла.


Отличие от аннотации /* @__PURE__ */

Esbuild поддерживает специальную аннотацию:

/* @__PURE__ */
createObject();

или

const value =
  /* @__PURE__ */
  createObject();

Она сообщает оптимизатору, что конкретный вызов функции является чистым.

Опция pure действует иначе:

pure: ['createObject']

Она распространяется сразу на все вызовы данной функции во всём проекте.

Сравнение подходов:

Метод Область действия
/* @__PURE__ */ Конкретный вызов
pure Все вызовы функции
Оба варианта Помогают удалять неиспользуемый код

Использование через CLI

Опция доступна и из командной строки.

Удаление вызовов console.log:

esbuild src/index.js \
  --bundle \
  --minify \
  --pure:console.log

Несколько функций:

esbuild src/index.js \
  --bundle \
  --minify \
  --pure:console.log \
  --pure:debug \
  --pure:trackEvent

Использование через JavaScript API

Пример конфигурации:

import * as esbuild from 'esbuild';

await esbuild.build({
  entryPoints: ['src/index.js'],
  outfile: 'dist/app.js',
  bundle: true,
  minify: true,
  pure: [
    'console.log',
    'console.info',
    'console.debug'
  ]
});

Использование через Go API

result := api.Build(api.BuildOptions{
    EntryPoints: []string{"src/index.js"},
    Bundle: true,
    MinifyWhitespace: true,
    MinifyIdentifiers: true,
    MinifySyntax: true,
    Pure: []string{
        "console.log",
        "console.debug",
    },
})

Практический сценарий: удаление логирования

Во многих проектах встречается код:

console.log('Start');
console.log(user);
console.debug(config);
console.info(version);

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

pure: [
  'console.log',
  'console.debug',
  'console.info'
]

После production-сборки подобные вызовы исчезают, что позволяет:

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

Практический сценарий: библиотеки логирования

Допустим, используется собственный логгер:

logger.debug(data);
logger.trace(result);
logger.verbose(state);

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

pure: [
  'logger.debug',
  'logger.trace',
  'logger.verbose'
]

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


Потенциальные ошибки

Наиболее опасная ошибка — пометка функции как чистой, когда она фактически изменяет состояние программы.

Пример:

function saveUser(user) {
  database.save(user);
}

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

pure: ['saveUser']

Код:

saveUser(user);

После оптимизации вызов может исчезнуть.

Результат:

// пользователь не сохранится

Поэтому в список pure должны попадать только функции, отсутствие побочных эффектов которых гарантировано.


Проверка безопасности функции

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

  • не вызывает сетевые запросы;
  • не пишет в базу данных;
  • не изменяет глобальные переменные;
  • не работает с файловой системой;
  • не вызывает методы логирования;
  • не генерирует события;
  • не изменяет DOM;
  • не использует таймеры.

Безопасный пример:

function buildConfig(options) {
  return {
    port: options.port,
    host: options.host
  };
}

Опасный пример:

function buildConfig(options) {
  console.log(options);
  return options;
}

Совместное использование с Tree Shaking

Опция pure усиливает эффективность Tree Shaking.

Пример:

import { createTheme } from './theme.js';

createTheme();

Если:

pure: ['createTheme']

и результат вызова не используется, Esbuild получает возможность удалить не только сам вызов, но и связанные неиспользуемые части модуля.

Это приводит к более агрессивному сокращению размера сборки.


Когда применение наиболее оправдано

Наибольшую пользу опция приносит для:

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

Грамотное использование pure позволяет существенно повысить эффективность минификации и удаления мёртвого кода, особенно в крупных приложениях, где тысячи вызовов отладочных функций попадают в production-сборку.