Опция pure в Esbuild предназначена для пометки
определённых вызовов функций как не имеющих побочных
эффектов (pure functions). Такая информация позволяет сборщику
безопасно удалять результаты вызовов этих функций, если они нигде не
используются.
Механизм особенно полезен во время минификации и удаления мёртвого кода (dead code elimination), когда в проекте присутствуют вызовы функций, создающих значения, которые затем игнорируются.
Пример исходного кода:
createObject();
Если результат вызова нигде не применяется, Esbuild обычно не может гарантировать отсутствие побочных эффектов и поэтому оставляет вызов в итоговом бандле.
При указании функции как чистой:
esbuild.build({
entryPoints: ['app.js'],
bundle: true,
minify: true,
pure: ['createObject']
});
Esbuild получает информацию о том, что вызов безопасно удалить.
После оптимизации:
// удалено
Чистая функция обладает двумя основными свойствами:
Примеры чистых функций:
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 |
Все вызовы функции |
| Оба варианта | Помогают удалять неиспользуемый код |
Опция доступна и из командной строки.
Удаление вызовов console.log:
esbuild src/index.js \
--bundle \
--minify \
--pure:console.log
Несколько функций:
esbuild src/index.js \
--bundle \
--minify \
--pure:console.log \
--pure:debug \
--pure:trackEvent
Пример конфигурации:
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'
]
});
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 полезно
убедиться, что она:
Безопасный пример:
function buildConfig(options) {
return {
port: options.port,
host: options.host
};
}
Опасный пример:
function buildConfig(options) {
console.log(options);
return options;
}
Опция pure усиливает эффективность Tree Shaking.
Пример:
import { createTheme } from './theme.js';
createTheme();
Если:
pure: ['createTheme']
и результат вызова не используется, Esbuild получает возможность удалить не только сам вызов, но и связанные неиспользуемые части модуля.
Это приводит к более агрессивному сокращению размера сборки.
Наибольшую пользу опция приносит для:
Грамотное использование pure позволяет существенно
повысить эффективность минификации и удаления мёртвого кода, особенно в
крупных приложениях, где тысячи вызовов отладочных функций попадают в
production-сборку.