В средах serverless каждая миллисекунда запуска влияет на стоимость выполнения и пользовательский опыт. Для функций, работающих в таких платформах, как AWS Lambda, Azure Functions, Google Cloud Functions и других аналогичных сервисах, размер итогового пакета и скорость инициализации приобретают критическое значение.
Esbuild хорошо подходит для подобных задач благодаря нескольким характеристикам:
В отличие от клиентских приложений, где часто используются сложные пайплайны обработки ресурсов, серверные функции обычно требуют максимально компактного и быстрого пакета.
Каждый запуск функции проходит несколько этапов:
Чем больше размер проекта и количество подключаемых модулей, тем дольше выполняется этап инициализации.
Например, функция может использовать всего одну утилиту из большой библиотеки:
import { debounce } from "lodash";
Без оптимизации в пакет может попасть значительная часть библиотеки. При использовании Esbuild и корректно настроенного tree shaking в итоговую сборку войдет только реально используемый код.
Пример простой функции:
export async function handler(event) {
return {
statusCode: 200,
body: JSON.stringify({
message: "Hello Lambda"
})
};
}
Файл:
src/handler.js
Конфигурация сборки:
const esbuild = require("esbuild");
esbuild.build({
entryPoints: ["src/handler.js"],
bundle: true,
platform: "node",
target: "node20",
outfile: "dist/handler.js"
});
После выполнения:
node build.js
получается единый файл:
dist/
└─ handler.js
Именно этот файл обычно загружается в облачную платформу.
Для Lambda-функций необходимо указывать:
platform: "node"
Пример:
esbuild.build({
entryPoints: ["src/index.ts"],
bundle: true,
platform: "node",
outfile: "dist/index.js"
});
Это влияет на:
Без данной настройки Esbuild может использовать правила, предназначенные для браузеров.
Каждая среда выполнения Lambda поддерживает конкретную версию Node.js.
Для Node.js 18:
target: "node18"
Для Node.js 20:
target: "node20"
Полный пример:
esbuild.build({
entryPoints: ["src/handler.ts"],
bundle: true,
platform: "node",
target: "node20",
outfile: "dist/handler.js"
});
Чем новее версия среды, тем меньше транспиляции требуется выполнять.
Одной из главных возможностей Esbuild для serverless является bundling.
Исходный код:
import axios from "axios";
import { v4 as uuid } from "uuid";
export async function handler() {
const id = uuid();
const response = await axios.get(
"https://api.example.com"
);
return {
id,
data: response.data
};
}
Сборка:
esbuild.build({
entryPoints: ["src/handler.js"],
bundle: true,
platform: "node",
outfile: "dist/handler.js"
});
В результате:
src/
node_modules/
↓
dist/
└─ handler.js
Все необходимые зависимости окажутся внутри итогового файла.
Рассмотрим библиотеку:
import {
functionA,
functionB,
functionC
} from "./utils.js";
functionA();
Если используются только необходимые экспорты, Esbuild исключит лишний код:
functionB();
functionC();
не попадут в итоговую сборку.
Для Lambda-функций это особенно важно, поскольку уменьшает:
Дополнительное уменьшение размера достигается минификацией.
esbuild.build({
entryPoints: ["src/handler.js"],
bundle: true,
minify: true,
platform: "node",
outfile: "dist/handler.js"
});
Минификация:
Для production-развертываний практически всегда используется:
minify: true
Во время отладки полезно сохранять карты исходников.
esbuild.build({
entryPoints: ["src/handler.ts"],
bundle: true,
sourcemap: true,
outfile: "dist/handler.js"
});
Результат:
dist/
├─ handler.js
└─ handler.js.map
Ошибки в облаке будут ссылаться на исходный TypeScript-код вместо сгенерированного JavaScript.
Исходный код:
interface User {
id: string;
name: string;
}
export async function handler(): Promise<User> {
return {
id: "1",
name: "Alex"
};
}
Сборка:
esbuild.build({
entryPoints: ["src/handler.ts"],
bundle: true,
platform: "node",
target: "node20",
outfile: "dist/handler.js"
});
Esbuild автоматически:
Иногда пакет не требуется включать в сборку.
Например:
external: ["aws-sdk"]
Конфигурация:
esbuild.build({
entryPoints: ["src/handler.js"],
bundle: true,
external: ["aws-sdk"],
outfile: "dist/handler.js"
});
Импорт останется внешним:
require("aws-sdk");
Такой подход полезен, когда библиотека уже присутствует в среде выполнения или поставляется через Lambda Layer.
Крупные зависимости часто выносятся в отдельные слои.
Структура:
layer/
└─ nodejs/
└─ node_modules/
└─ sharp/
function/
└─ handler.js
Конфигурация:
external: ["sharp"]
В этом случае:
Типичный проект может содержать десятки обработчиков.
Структура:
src/
├─ users.js
├─ orders.js
└─ products.js
Сборка:
esbuild.build({
entryPoints: [
"src/users.js",
"src/orders.js",
"src/products.js"
],
bundle: true,
platform: "node",
outdir: "dist"
});
Результат:
dist/
├─ users.js
├─ orders.js
└─ products.js
Каждая функция собирается отдельно.
При большом количестве функций возникает проблема дублирования кода.
Пример:
import axios from "axios";
используется в десяти обработчиках.
Каждая сборка будет содержать собственную копию axios.
Для оптимизации применяются:
external;Современные версии Lambda поддерживают ES Modules.
Исходный код:
export async function handler() {
return {
statusCode: 200
};
}
Сборка:
esbuild.build({
entryPoints: ["src/handler.js"],
bundle: true,
format: "esm",
platform: "node",
outfile: "dist/handler.mjs"
});
Полученный файл:
export async function handler() {
return {
statusCode: 200
};
}
Это позволяет использовать современный синтаксис модулей без преобразования в CommonJS.
При использовании CommonJS:
exports.handler = async function () {
};
или
module.exports.handler = async function () {
};
Для ESM:
export async function handler() {
}
Настройка Lambda обычно выглядит так:
handler.handler
где:
Вместо API можно использовать командную строку.
esbuild src/handler.ts \
--bundle \
--platform=node \
--target=node20 \
--outfile=dist/handler.js
С минификацией:
esbuild src/handler.ts \
--bundle \
--minify \
--platform=node \
--target=node20 \
--outfile=dist/handler.js
Подобный подход часто используется в CI/CD-конвейерах.
Esbuild способен создавать подробную статистику.
esbuild.build({
entryPoints: ["src/handler.js"],
bundle: true,
metafile: true,
outfile: "dist/handler.js"
});
Получение данных:
const result = await esbuild.build({
bundle: true,
metafile: true,
write: false,
entryPoints: ["src/handler.js"]
});
console.log(result.metafile);
Метафайл помогает определить:
Cold start представляет собой первый запуск функции после создания нового контейнера.
На скорость cold start влияют:
Практики оптимизации с использованием Esbuild:
esbuild.build({
bundle: true,
minify: true,
treeShaking: true,
platform: "node",
target: "node20"
});
Дополнительно рекомендуется:
Пример конфигурации:
service: demo
provider:
name: aws
runtime: nodejs20.x
functions:
users:
handler: dist/users.handler
Сценарий сборки:
{
"scripts": {
"build": "node build.js",
"deploy": "serverless deploy"
}
}
Последовательность:
npm run build
npm run deploy
Esbuild формирует оптимизированные артефакты, которые затем загружаются через Serverless Framework.
Структура проекта:
src/
template.yaml
build.js
Сборка:
node build.js
Развертывание:
sam deploy
Полученные файлы из каталога dist используются как
исходники для публикации функции.
Некоторые функции поставляются как контейнеры.
Сборка Esbuild:
esbuild.build({
entryPoints: ["src/index.ts"],
bundle: true,
minify: true,
platform: "node",
outfile: "dist/index.js"
});
Dockerfile:
FROM node:20
COPY dist/index.js .
CMD ["node", "index.js"]
Даже при использовании контейнеров Esbuild остается полезным инструментом, поскольку существенно сокращает размер приложения и ускоряет запуск контейнера.
Пример сценария:
const esbuild = require("esbuild");
const handlers = [
"users",
"orders",
"products"
];
Promise.all(
handlers.map(name =>
esbuild.build({
entryPoints: [`src/${name}.ts`],
bundle: true,
minify: true,
platform: "node",
target: "node20",
outfile: `dist/${name}.js`
})
)
);
Подобный подход широко применяется в крупных serverless-проектах, содержащих десятки и сотни функций, каждая из которых получает отдельный оптимизированный пакет с минимальным размером и быстрым временем инициализации.