В процессе разработки JavaScript-код обычно хранится в удобочитаемом виде: с отступами, комментариями, понятными именами переменных и разделением на множество файлов. Для публикации приложения в production-окружении код часто подвергается оптимизации:
После таких преобразований исходная структура проекта теряется. Это повышает производительность загрузки, но существенно усложняет отладку. Для решения данной проблемы используются source maps, позволяющие сопоставить оптимизированный код с оригинальными исходниками.
Esbuild предоставляет встроенные механизмы как для генерации source maps, так и для различных вариантов минификации.
Source map — специальный файл, содержащий информацию о соответствии между исходным кодом и результирующим бандлом.
Предположим, существует исходный файл:
function calculateTotal(price, tax) {
return price + tax;
}
console.log(calculateTotal(100, 20));
После минификации он может выглядеть следующим образом:
function o(n,t){return n+t}console.log(o(100,20));
Без дополнительных данных определить, где именно возникла ошибка, становится сложно. Source map позволяет браузеру или инструментам разработки восстановить исходное положение кода.
Например, ошибка в строке:
return price + tax;
будет отображаться именно в исходном файле, а не в минифицированной версии.
Базовая генерация карт исходников выполняется через параметр
sourcemap.
CLI-вариант:
esbuild src/index.js \
--bundle \
--outfile=dist/app.js \
--sourcemap
JavaScript API:
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outfile: 'dist/app.js',
sourcemap: true
});
После сборки будут созданы два файла:
dist/
├── app.js
└── app.js.map
В конец основного файла автоматически добавляется ссылка:
//# sourceMappingURL=app.js.map
Браузер обнаруживает её и загружает карту исходников при открытии инструментов разработчика.
Esbuild поддерживает несколько режимов генерации карт исходников.
Режим по умолчанию.
esbuild src/index.js \
--bundle \
--sourcemap=linked
Создаются:
app.js
app.js.map
В основном файле присутствует ссылка на карту.
Подходит для большинства production- и development-сборок.
Создаёт внешний файл карты без автоматической ссылки.
esbuild src/index.js \
--bundle \
--sourcemap=external
Результат:
app.js
app.js.map
Однако строка:
//# sourceMappingURL=app.js.map
не добавляется.
Такой подход используется, когда ссылка на карту должна вставляться вручную либо карты предоставляются отдельным механизмом.
Карта внедряется непосредственно в итоговый JavaScript-файл.
esbuild src/index.js \
--bundle \
--sourcemap=inline
В конец файла записывается большая строка вида:
//# sourceMappingURL=dat a:application/json;base64,...
Преимущества:
.map файл;Недостатки:
Одновременное создание внешней и встроенной карты.
esbuild src/index.js \
--bundle \
--sourcemap=both
Формируются:
app.js
app.js.map
При этом карта также встраивается в сам бандл.
Используется редко, обычно для специальных сценариев отладки.
Source map содержит несколько важных разделов:
{
"version": 3,
"sources": [
"../src/index.js"
],
"sourcesContent": [
"console.log('Hello');"
],
"mappings": "AAAA..."
}
Версия формата source map.
Список исходных файлов.
"sources": [
"src/index.js",
"src/utils.js"
]
Исходный код файлов.
"sourcesContent": [
"...код..."
]
Благодаря этому DevTools могут отображать оригинальный код даже без доступа к файловой системе.
Закодированная информация о соответствии строк и колонок.
"mappings": "AAAA,SAAS..."
Этот раздел обычно не редактируется вручную.
После генерации карты разработчик может открыть DevTools.
Вкладка Sources покажет структуру оригинального проекта:
src/
├─ index.js
├─ api.js
└─ utils.js
Несмотря на то что браузер исполняет:
dist/app.js
отладка выполняется по исходным файлам.
Доступны:
Esbuild часто применяется для сборки TypeScript.
Исходный файл:
interface User {
name: string;
}
const user: User = {
name: 'Alex'
};
console.log(user.name);
Команда:
esbuild src/index.ts \
--bundle \
--outfile=dist/app.js \
--sourcemap
В браузере отладка будет происходить по TypeScript-коду, хотя выполняется уже скомпилированный JavaScript.
Это особенно важно для крупных приложений.
Аналогично работают React-проекты.
Исходный код:
function App() {
return <h1>Hello</h1>;
}
После трансформации JSX превращается в вызовы функций.
Без карты исходников стек вызовов становится менее понятным. С source maps ошибки отображаются непосредственно в JSX-файлах.
Карты исходников могут содержать:
Например:
// TODO: remove debug endpoint
Поэтому многие команды не публикуют карты в открытом доступе.
Распространённые варианты:
Минификация представляет собой набор преобразований, направленных на уменьшение объёма кода.
Основные цели:
Esbuild выполняет минификацию чрезвычайно быстро благодаря реализации на Go.
CLI:
esbuild src/index.js \
--bundle \
--minify \
--outfile=dist/app.js
JavaScript API:
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
minify: true,
outfile: 'dist/app.js'
});
Рассмотрим исходный код:
function greet(name) {
console.log("Hello " + name);
}
greet("John");
После минификации:
function o(e){console.log("Hello "+e)}o("John");
Размер файла уменьшается за счёт нескольких видов оптимизаций.
Исходный вариант:
if (isAdmin) {
console.log("admin");
}
Минифицированный:
isAdmin&&console.log("admin");
Удаляются:
Исходный код:
function calculateMonthlyPayment(amount) {
return amount * 2;
}
После минификации:
function n(t){return t*2}
Длинные имена заменяются короткими.
Это существенно уменьшает размер крупных проектов.
Пример:
function test() {
return 10;
console.log("never");
}
После оптимизации:
function test(){return 10}
Недостижимая инструкция удаляется.
Исходный вариант:
const value = true ? 1 : 2;
После преобразования:
const value=1;
Константные выражения вычисляются заранее.
Esbuild позволяет управлять компонентами минификации независимо.
Удаляет лишние пробелы.
await esbuild.build({
minifyWhitespace: true
});
Пример:
let x = 10;
Преобразуется в:
let x=10;
Сокращает имена переменных.
await esbuild.build({
minifyIdentifiers: true
});
Пример:
function calculateTotal(price) {
return price;
}
Станет:
function n(r){return r}
Упрощает конструкции языка.
await esbuild.build({
minifySyntax: true
});
Например:
if (true) {
run();
}
Может превратиться в:
run();
Полный аналог minify: true:
await esbuild.build({
minifyWhitespace: true,
minifyIdentifiers: true,
minifySyntax: true
});
Каждая оптимизация может включаться независимо.
Esbuild умеет минифицировать не только JavaScript.
Исходный CSS:
.container {
padding: 20px;
margin: 10px;
}
Сборка:
esbuild src/styles.css \
--minify \
--outfile=dist/styles.css
Результат:
.container{padding:20px;margin:10px}
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
minify: true,
outfile: 'dist/app.js'
});
Если импортируются стили:
import './styles.css';
то соответствующий CSS также будет минифицирован.
Для крупных приложений сокращение размера файлов может достигать десятков процентов.
Пример условного проекта:
| Состояние | Размер |
|---|---|
| Исходный код | 1.8 MB |
| После bundle | 1.2 MB |
| После minify | 720 KB |
| После gzip | 180 KB |
Уменьшаются:
Наиболее распространённая production-конфигурация:
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
minify: true,
sourcemap: true,
outfile: 'dist/app.js'
});
Результат:
dist/
├── app.js
└── app.js.map
Пользователь получает компактный минифицированный код:
function o(n){return n*n}
При этом разработчик продолжает видеть исходную версию:
function square(number) {
return number * number;
}
Такое сочетание обеспечивает одновременно высокую производительность приложения и удобство диагностики ошибок.
Во время разработки обычно используются карты исходников без минификации:
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
sourcemap: true,
outfile: 'dist/app.js'
});
Преимущества:
Для публикации чаще применяется следующая схема:
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
minify: true,
sourcemap: 'external',
outfile: 'dist/app.js'
});
Особенности: