Кэширование байт-кода — это оптимизация на этапе сборки которая значительно улучшает время запуска приложения за счет предварительной компиляции JavaScript в байт-код. Например при компиляции TypeScript tsc с включенным байт-кодом время запуска улучшается в 2 раза.
Использование
Базовое использование
Включите кэширование байт-кода с флагом --bytecode:
bun build ./index.ts --target=bun --bytecode --outdir=./distЭто генерирует два файла:
dist/index.js- Ваш связанный JavaScriptdist/index.jsc- Файл кэша байт-кода
Во время выполнения Bun автоматически обнаруживает и использует файл .jsc:
bun ./dist/index.js # Автоматически использует index.jscС автономными исполняемыми файлами
При создании исполняемых файлов с --compile байт-код встраивается в бинарный файл:
bun build ./cli.ts --compile --bytecode --outfile=mycliПолученный исполняемый файл содержит как код так и байт-код обеспечивая максимальную производительность в одном файле.
Комбинирование с другими оптимизациями
Байт-код отлично работает с минификацией и картами исходного кода:
bun build --compile --bytecode --minify --sourcemap ./cli.ts --outfile=mycli--minifyуменьшает размер кода перед генерацией байт-кода (меньше кода -> меньше байт-кода)--sourcemapсохраняет отчетность об ошибках (ошибки все еще указывают на исходный код)--bytecodeустраняет накладные расходы на парсинг
Влияние на производительность
Улучшение производительности масштабируется с размером вашей кодовой базы:
| Размер приложения | Типичное улучшение запуска |
|---|---|
| Малый CLI (< 100 КБ) | В 1,5-2 раза быстрее |
| Средне-большое приложение (> 5 МБ) | В 2,5-4 раза быстрее |
Большие приложения выигрывают больше потому что у них больше кода для парсинга.
Когда использовать байт-код
Отлично подходит для:
CLI-инструментов
- Вызываются часто (линтеры форматтеры git-хуки)
- Время запуска — это весь пользовательский опыт
- Пользователи замечают разницу между 90 мс и 45 мс запуска
- Пример: компилятор TypeScript Prettier ESLint
Инструментов сборки и запуска задач
- Запускаются сотни или тысячи раз во время разработки
- Миллисекунды сэкономленные за запуск быстро накапливаются
- Улучшение опыта разработчика
- Пример: скрипты сборки тестовые раннеры генераторы кода
Автономных исполняемых файлов
- Распространяются среди пользователей которым важна быстрая производительность
- Однофайловая дистрибуция удобна
- Размер файла менее важен чем время запуска
- Пример: CLI распространяемые через npm или как бинарные файлы
Пропустите для:
- ❌ Маленьких скриптов
- ❌ Кода который запускается один раз
- ❌ Сборок для разработки
- ❌ Ограниченных по размеру сред
- ❌ Кода с top-level await (не поддерживается)
Ограничения
Только CommonJS
Кэширование байт-кода в настоящее время работает с форматом вывода CommonJS. Бандлер Bun автоматически конвертирует большинство ESM-кода в CommonJS но top-level await является исключением:
// Это предотвращает кэширование байт-кода
const data = await fetch("https://api.example.com");
export default data;Почему: Top-level await требует асинхронного вычисления модуля что невозможно представить в CommonJS. Граф модулей становится асинхронным и модель функции-обертки CommonJS нарушается.
Обходной путь: Переместите асинхронную инициализацию в функцию:
async function init() {
const data = await fetch("https://api.example.com");
return data;
}
export default init;Теперь модуль экспортирует функцию которую потребитель может ожидать при необходимости.
Совместимость версий
Байт-код не переносим между версиями Bun. Формат байт-кода привязан к внутреннему представлению JavaScriptCore которое меняется между версиями.
При обновлении Bun вы должны регенерировать байт-код:
# После обновления Bun
bun build --bytecode ./index.ts --outdir=./distЕсли байт-код не соответствует текущей версии Bun он автоматически игнорируется и ваш код возвращается к парсингу исходного JavaScript. Ваше приложение все еще работает — вы просто теряете оптимизацию производительности.
Лучшая практика: Генерируйте байт-код как часть процесса CI/CD. Не коммитьте файлы .jsc в git. Регенерируйте их при каждом обновлении Bun.
Исходный код все еще требуется
- Файл
.js(ваш связанный исходный код) - Файл
.jsc(файл кэша байт-кода)
Во время выполнения:
- Bun загружает файл
.jsвидит прагму@bytecodeи проверяет файл.jsc - Bun загружает файл
.jsc - Bun проверяет что хеш байт-кода соответствует исходному коду
- Если верно Bun использует байт-код
- Если неверно Bun возвращается к парсингу исходного кода
Байт-код не является обфускацией
Байт-код не скрывает ваш исходный код. Это оптимизация а не мера безопасности.
Развертывание в продакшене
Docker
Включите генерацию байт-кода в ваш Dockerfile:
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun build --bytecode --minify --sourcemap \
--target=bun \
--outdir=./dist \
--compile \
./src/server.ts --outfile=./dist/server
FROM oven/bun:1 AS runner
WORKDIR /app
COPY --from=builder /dist/server /app/server
CMD ["./server"]Байт-код не зависит от архитектуры.
CI/CD
Генерируйте байт-код во время вашего конвейера сборки:
# GitHub Actions
- name: Build with bytecode
run: |
bun install
bun build --bytecode --minify \
--outdir=./dist \
--target=bun \
./src/index.tsОтладка
Проверка использования байт-кода
Проверьте что файл .jsc существует:
ls -lh dist/-rw-r--r-- 1 user staff 245K index.js
-rw-r--r-- 1 user staff 1.1M index.jscФайл .jsc должен быть в 2-8 раз больше чем файл .js.
Для логирования использования байт-кода установите BUN_JSC_verboseDiskCache=1 в вашей среде.
При успехе будет залогировано что-то вроде:
[Disk cache] cache hit for sourceCodeЕсли вы увидите промах кэша будет залогировано что-то вроде:
[Disk cache] cache miss for sourceCodeНормально что логирование промаха кэша происходит несколько раз так как Bun в настоящее время не кэширует байт-код для JavaScript-кода используемого во встроенных модулях.
Распространенные проблемы
Байт-код молча игнорируется: Обычно вызвано обновлением версии Bun. Версия кэша не совпадает поэтому байт-код отклоняется. Регенерируйте для исправления.
Размер файла слишком большой: Это ожидаемо. Рассмотрите:
- Использование
--minifyдля уменьшения размера кода перед генерацией байт-кода - Сжатие файлов
.jscдля сетевой передачи (gzip/brotli) - Оценку стоит ли выигрыш в времени запуска увеличения размера
Top-level await: Не поддерживается. Рефакторите для использования асинхронных функций инициализации.
Что такое байт-код?
Когда вы запускаете JavaScript движок JavaScript не выполняет ваш исходный код напрямую. Вместо этого он проходит несколько шагов:
- Парсинг: Движок читает ваш исходный код JavaScript и преобразует его в абстрактное синтаксическое дерево (AST)
- Компиляция байт-кода: AST компилируется в байт-код — представление более низкого уровня которое выполняется быстрее
- Выполнение: Байт-код выполняется интерпретатором движка или JIT-компилятором
Байт-код — это промежуточное представление — он находится на более низком уровне чем исходный код JavaScript но на более высоком уровне чем машинный код. Думайте о нем как о языке ассемблера для виртуальной машины. Каждая инструкция байт-кода представляет одну операцию такую как «загрузить эту переменную» «сложить два числа» или «вызвать эту функцию».
Это происходит каждый раз когда вы запускаете код. Если у вас есть CLI-инструмент который запускается 100 раз в день ваш код парсится 100 раз. Если у вас есть серверная функция с частыми холодными запусками парсинг происходит при каждом холодном запуске.
С кэшированием байт-кода Bun переносит шаги 1 и 2 на этап сборки. Во время выполнения движок загружает предварительно скомпилированный байт-код и переходит сразу к выполнению.
Почему ленивый парсинг делает это еще лучше
Современные движки JavaScript используют умную оптимизацию называемую ленивым парсингом. Они не парсят весь код заранее — вместо этого функции парсятся только когда они впервые вызываются:
// Без кэширования байт-кода:
function rarely_used() {
// Эта функция из 500 строк парсится только
// когда она фактически вызывается
}
function main() {
console.log("Starting app");
// rarely_used() никогда не вызывается поэтому не парсится
}Это означает что накладные расходы на парсинг — это не только стоимость запуска — они происходят в течение времени жизни вашего приложения когда выполняются различные пути кода. С кэшированием байт-кода все функции предварительно компилируются даже те которые парсятся лениво. Работа по парсингу происходит один раз во время сборки вместо того чтобы распределяться по выполнению приложения.
Формат байт-кода
Внутри файла .jsc
Файл .jsc содержит сериализованную структуру байт-кода. Понимание того что внутри помогает объяснить как преимущества производительности так и компромисс размера файла.
Секция заголовка (проверяется при каждой загрузке):
- Версия кэша: Хеш привязанный к версии фреймворка JavaScriptCore. Это гарантирует что байт-код сгенерированный с одной версией Bun работает только с этой точной версией.
- Тег типа блока кода: Идентифицирует является ли это блоком кода Program Module Eval или Function.
SourceCodeKey (проверяет соответствие байт-кода исходному коду):
- Хеш исходного кода: Хеш оригинального исходного кода JavaScript. Bun проверяет это соответствие перед использованием байт-кода.
- Длина исходного кода: Точная длина исходного кода для дополнительной проверки.
- Флаги компиляции: Критический контекст компиляции такой как строгий режим является ли это скриптом или модулем тип контекста eval и т.д. Один и тот же исходный код скомпилированный с разными флагами дает разный байт-код.
Инструкции байт-кода:
- Поток инструкций: Фактические опкоды байт-кода — скомпилированное представление вашего JavaScript. Это последовательность инструкций байт-кода переменной длины.
- Таблица метаданных: Каждый опкод имеет связанные метаданные — такие как счетчики профилирования подсказки типов и счетчики выполнения (даже если еще не заполнены).
- Цели переходов: Предварительно вычисленные адреса для потока управления (if/else циклы switch).
- Таблицы переключений: Оптимизированные таблицы поиска для операторов switch.
Константы и идентификаторы:
- Пул констант: Все литеральные значения в вашем коде — числа строки булевы значения null undefined. Они хранятся как фактические значения JavaScript (JSValues) поэтому их не нужно парсить из исходного кода во время выполнения.
- Таблица идентификаторов: Все имена переменных и функций используемые в коде. Хранятся как дедуплицированные строки.
- Маркеры представления исходного кода: Флаги указывающие как должны представляться константы (как целые числа doubles большие целые числа и т.д.).
Метаданные функции (для каждой функции в вашем коде):
- Распределение регистров: Сколько регистров (локальных переменных) нужно функции —
thisRegisterscopeRegisternumVarsnumCalleeLocalsnumParameters. - Особенности кода: Битовая маска характеристик функции: является ли она конструктором? стрелочной функцией? использует ли
super? есть ли у нее хвостовые вызовы? Это влияет на то как выполняется функция. - Лексически ограниченные особенности: Строгий режим и другой лексический контекст.
- Режим парсинга: Режим в котором была распарсена функция (normal async generator async generator).
Вложенные структуры:
- Объявления и выражения функций: Каждая вложенная функция получает свой собственный блок байт-кода рекурсивно. Файл со 100 функциями имеет 100 отдельных блоков байт-кода все вложенные в структуру.
- Обработчики исключений: Блоки try/catch/finally с их границами и адресами обработчиков предварительно вычислены.
- Информация о выражениях: Отображает позиции байт-кода обратно в расположения исходного кода для отчетности об ошибках и отладки.
Что НЕ содержит байт-код
Важно что байт-код не встраивает ваш исходный код. Вместо этого:
- Исходный код JavaScript хранится отдельно (в файле
.js) - Байт-код хранит только хеш и длину исходного кода
- Во время загрузки Bun проверяет соответствие байт-кода текущему исходному коду
Вот почему вам нужно развертывать как файлы .js так и .jsc. Файл .jsc бесполезен без соответствующего файла .js.
Компромисс: размер файла
Файлы байт-кода значительно больше чем исходный код — обычно в 2-8 раз больше.
Почему байт-код такой большой?
Инструкции байт-кода многословны: Одна строка минифицированного JavaScript может компилироваться в десятки инструкций байт-кода. Например:
const sum = arr.reduce((a, b) => a + b, 0);Компилируется в байт-код который:
- Загружает переменную
arr - Получает свойство
reduce - Создает стрелочную функцию (которая сама имеет байт-код)
- Загружает начальное значение
0 - Настраивает вызов с правильным количеством аргументов
- Фактически выполняет вызов
- Сохраняет результат в
sum
Каждый из этих шагов — отдельная инструкция байт-кода со своими метаданными.
Пулы констант хранят все: Каждый строковый литерал число имя свойства — все хранится в пуле констант. Даже если ваш исходный код имеет "hello" сто раз пул констант хранит его один раз но ссылки на идентификаторы и константы добавляют накладные расходы.
Метаданные на функцию: Каждая функция — даже маленькие однострочные функции — получает свои полные метаданные:
- Информация о распределении регистров
- Битовая маска особенностей кода
- Режим парсинга
- Обработчики исключений
- Информация о выражениях для отладки
Файл с 1000 маленькими функциями имеет 1000 наборов метаданных.
Структуры данных профилирования: Даже хотя данные профилирования еще не заполнены структуры для хранения данных профилирования выделены. Это включает:
- Слоты профиля значений (отслеживание каких типов проходят через каждую операцию)
- Слоты профиля массивов (отслеживание шаблонов доступа к массивам)
- Слоты профиля бинарной арифметики (отслеживание типов чисел в математических операциях)
- Слоты профиля унарной арифметики
Они занимают место даже когда пустые.
Предварительно вычисленный поток управления: Цели переходов таблицы переключений и границы обработчиков исключений все предварительно вычислены и сохранены. Это делает выполнение быстрее но увеличивает размер файла.
Стратегии смягчения
Сжатие: Байт-код чрезвычайно хорошо сжимается с gzip/brotli (сжатие 60-70%). Повторяющаяся структура и метаданные сжимаются эффективно.
Сначала минификация: Использование --minify перед генерацией байт-кода помогает:
- Более короткие идентификаторы → меньшая таблица идентификаторов
- Устранение мертвого кода → меньше генерируемого байт-кода
- Сворачивание констант → меньше констант в пуле
Компромисс: Вы обмениваете файлы в 2-4 раза больше на запуск в 2-4 раза быстрее. Для CLI это обычно того стоит. Для долгоживущих серверов где несколько мегабайт дискового пространства не имеют значения это еще меньше проблем.
Версионирование и переносимость
Переносимость между архитектурами: ✅
Байт-код не зависит от архитектуры. Вы можете:
- Собирать на macOS ARM64 развертывать на Linux x64
- Собирать на Linux x64 развертывать на AWS Lambda ARM64
- Собирать на Windows x64 развертывать на macOS ARM64
Байт-код содержит абстрактные инструкции которые работают на любой архитектуре. Оптимизации специфичные для архитектуры происходят во время JIT-компиляции во время выполнения а не в кэшированном байт-коде.
Переносимость между версиями: ❌
Байт-код не стабилен между версиями Bun. Вот почему:
Формат байт-кода меняется: Формат байт-кода JavaScriptCore эволюционирует. Новые опкоды добавляются старые удаляются или меняются структуры метаданных меняются. Каждая версия JavaScriptCore имеет разный формат байт-кода.
Проверка версии: Версия кэша в заголовке файла .jsc — это хеш фреймворка JavaScriptCore. Когда Bun загружает байт-код:
- Извлекает версию кэша из файла
.jsc - Вычисляет текущую версию JavaScriptCore
- Если они не совпадают байт-код молча отклоняется
- Bun возвращается к парсингу исходного кода
.js
Ваше приложение все еще работает — вы просто теряете оптимизацию производительности.
Грациозная деградация: Этот дизайн означает что кэширование байт-кода «неудачно открыто» — если что-то идет не так (несоответствие версий поврежденный файл отсутствующий файл) ваш код все еще работает нормально. Вы можете увидеть более медленный запуск но не увидите ошибок.
Несвязанный и связанный байт-код
JavaScriptCore делает важное различие между «несвязанным» и «связанным» байт-кодом. Это разделение — то что делает кэширование байт-кода возможным:
Несвязанный байт-код (что кэшируется)
Байт-код сохраненный в файлах .jsc — это несвязанный байт-код. Он содержит:
- Скомпилированные инструкции байт-кода
- Структурную информацию о коде
- Константы и идентификаторы
- Информацию о потоке управления
Но он не содержит:
- Указатели на фактические объекты времени выполнения
- JIT-компилированный машинный код
- Данные профилирования из предыдущих запусков
- Информацию о ссылке вызова (какие функции какие вызывают)
Несвязанный байт-код неизменяем и доступен для совместного использования. Несколько выполнений одного и того же кода могут ссылаться на один и тот же несвязанный байт-код.
Связанный байт-код (выполнение во время выполнения)
Когда Bun выполняет байт-код он «связывает» его — создавая обертку времени выполнения которая добавляет:
- Информацию о ссылке вызова: Когда ваш код выполняется движок узнает какие функции какие вызывают и оптимизирует эти места вызова.
- Данные профилирования: Движок отслеживает сколько раз выполняется каждая инструкция какие типы значений проходят через код шаблоны доступа к массивам и т.д.
- Состояние JIT-компиляции: Ссылки на базовый JIT или оптимизирующий JIT (DFG/FTL) скомпилированные версии горячего кода.
- Объекты времени выполнения: Указатели на фактические объекты JavaScript прототипы области видимости и т.д.
Это связанное представление создается заново каждый раз когда вы запускаете код. Это позволяет:
- Кэшировать дорогую работу (парсинг и компиляция в несвязанный байт-код)
- Все еще собирать данные профилирования времени выполнения для руководства оптимизациями
- Все еще применять оптимизации JIT на основе фактических шаблонов выполнения
Кэширование байт-кода переносит дорогую работу (парсинг и компиляцию в байт-код) из времени выполнения во время сборки. Для приложений которые часто запускаются это может сократить время запуска вдвое за счет больших файлов на диске.
Для продакшн CLI и серверных развертываний комбинация --bytecode --minify --sourcemap дает вам лучшую производительность сохраняя возможность отладки.