Skip to content

Кэширование байт-кода — это оптимизация на этапе сборки которая значительно улучшает время запуска приложения за счет предварительной компиляции JavaScript в байт-код. Например при компиляции TypeScript tsc с включенным байт-кодом время запуска улучшается в 2 раза.

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

Базовое использование

Включите кэширование байт-кода с флагом --bytecode:

bash
bun build ./index.ts --target=bun --bytecode --outdir=./dist

Это генерирует два файла:

  • dist/index.js - Ваш связанный JavaScript
  • dist/index.jsc - Файл кэша байт-кода

Во время выполнения Bun автоматически обнаруживает и использует файл .jsc:

bash
bun ./dist/index.js  # Автоматически использует index.jsc

С автономными исполняемыми файлами

При создании исполняемых файлов с --compile байт-код встраивается в бинарный файл:

bash
bun build ./cli.ts --compile --bytecode --outfile=mycli

Полученный исполняемый файл содержит как код так и байт-код обеспечивая максимальную производительность в одном файле.

Комбинирование с другими оптимизациями

Байт-код отлично работает с минификацией и картами исходного кода:

bash
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 является исключением:

js
// Это предотвращает кэширование байт-кода
const data = await fetch("https://api.example.com");
export default data;

Почему: Top-level await требует асинхронного вычисления модуля что невозможно представить в CommonJS. Граф модулей становится асинхронным и модель функции-обертки CommonJS нарушается.

Обходной путь: Переместите асинхронную инициализацию в функцию:

js
async function init() {
  const data = await fetch("https://api.example.com");
  return data;
}

export default init;

Теперь модуль экспортирует функцию которую потребитель может ожидать при необходимости.

Совместимость версий

Байт-код не переносим между версиями Bun. Формат байт-кода привязан к внутреннему представлению JavaScriptCore которое меняется между версиями.

При обновлении Bun вы должны регенерировать байт-код:

bash
# После обновления Bun
bun build --bytecode ./index.ts --outdir=./dist

Если байт-код не соответствует текущей версии Bun он автоматически игнорируется и ваш код возвращается к парсингу исходного JavaScript. Ваше приложение все еще работает — вы просто теряете оптимизацию производительности.

Лучшая практика: Генерируйте байт-код как часть процесса CI/CD. Не коммитьте файлы .jsc в git. Регенерируйте их при каждом обновлении Bun.

Исходный код все еще требуется

  • Файл .js (ваш связанный исходный код)
  • Файл .jsc (файл кэша байт-кода)

Во время выполнения:

  1. Bun загружает файл .js видит прагму @bytecode и проверяет файл .jsc
  2. Bun загружает файл .jsc
  3. Bun проверяет что хеш байт-кода соответствует исходному коду
  4. Если верно Bun использует байт-код
  5. Если неверно Bun возвращается к парсингу исходного кода

Байт-код не является обфускацией

Байт-код не скрывает ваш исходный код. Это оптимизация а не мера безопасности.

Развертывание в продакшене

Docker

Включите генерацию байт-кода в ваш Dockerfile:

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

Генерируйте байт-код во время вашего конвейера сборки:

yaml
# GitHub Actions
- name: Build with bytecode
  run: |
    bun install
    bun build --bytecode --minify \
      --outdir=./dist \
      --target=bun \
      ./src/index.ts

Отладка

Проверка использования байт-кода

Проверьте что файл .jsc существует:

bash
ls -lh dist/
txt
-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 в вашей среде.

При успехе будет залогировано что-то вроде:

txt
[Disk cache] cache hit for sourceCode

Если вы увидите промах кэша будет залогировано что-то вроде:

txt
[Disk cache] cache miss for sourceCode

Нормально что логирование промаха кэша происходит несколько раз так как Bun в настоящее время не кэширует байт-код для JavaScript-кода используемого во встроенных модулях.

Распространенные проблемы

Байт-код молча игнорируется: Обычно вызвано обновлением версии Bun. Версия кэша не совпадает поэтому байт-код отклоняется. Регенерируйте для исправления.

Размер файла слишком большой: Это ожидаемо. Рассмотрите:

  • Использование --minify для уменьшения размера кода перед генерацией байт-кода
  • Сжатие файлов .jsc для сетевой передачи (gzip/brotli)
  • Оценку стоит ли выигрыш в времени запуска увеличения размера

Top-level await: Не поддерживается. Рефакторите для использования асинхронных функций инициализации.

Что такое байт-код?

Когда вы запускаете JavaScript движок JavaScript не выполняет ваш исходный код напрямую. Вместо этого он проходит несколько шагов:

  1. Парсинг: Движок читает ваш исходный код JavaScript и преобразует его в абстрактное синтаксическое дерево (AST)
  2. Компиляция байт-кода: AST компилируется в байт-код — представление более низкого уровня которое выполняется быстрее
  3. Выполнение: Байт-код выполняется интерпретатором движка или JIT-компилятором

Байт-код — это промежуточное представление — он находится на более низком уровне чем исходный код JavaScript но на более высоком уровне чем машинный код. Думайте о нем как о языке ассемблера для виртуальной машины. Каждая инструкция байт-кода представляет одну операцию такую как «загрузить эту переменную» «сложить два числа» или «вызвать эту функцию».

Это происходит каждый раз когда вы запускаете код. Если у вас есть CLI-инструмент который запускается 100 раз в день ваш код парсится 100 раз. Если у вас есть серверная функция с частыми холодными запусками парсинг происходит при каждом холодном запуске.

С кэшированием байт-кода Bun переносит шаги 1 и 2 на этап сборки. Во время выполнения движок загружает предварительно скомпилированный байт-код и переходит сразу к выполнению.

Почему ленивый парсинг делает это еще лучше

Современные движки JavaScript используют умную оптимизацию называемую ленивым парсингом. Они не парсят весь код заранее — вместо этого функции парсятся только когда они впервые вызываются:

js
// Без кэширования байт-кода:
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 большие целые числа и т.д.).

Метаданные функции (для каждой функции в вашем коде):

  • Распределение регистров: Сколько регистров (локальных переменных) нужно функции — thisRegister scopeRegister numVars numCalleeLocals numParameters.
  • Особенности кода: Битовая маска характеристик функции: является ли она конструктором? стрелочной функцией? использует ли super? есть ли у нее хвостовые вызовы? Это влияет на то как выполняется функция.
  • Лексически ограниченные особенности: Строгий режим и другой лексический контекст.
  • Режим парсинга: Режим в котором была распарсена функция (normal async generator async generator).

Вложенные структуры:

  • Объявления и выражения функций: Каждая вложенная функция получает свой собственный блок байт-кода рекурсивно. Файл со 100 функциями имеет 100 отдельных блоков байт-кода все вложенные в структуру.
  • Обработчики исключений: Блоки try/catch/finally с их границами и адресами обработчиков предварительно вычислены.
  • Информация о выражениях: Отображает позиции байт-кода обратно в расположения исходного кода для отчетности об ошибках и отладки.

Что НЕ содержит байт-код

Важно что байт-код не встраивает ваш исходный код. Вместо этого:

  • Исходный код JavaScript хранится отдельно (в файле .js)
  • Байт-код хранит только хеш и длину исходного кода
  • Во время загрузки Bun проверяет соответствие байт-кода текущему исходному коду

Вот почему вам нужно развертывать как файлы .js так и .jsc. Файл .jsc бесполезен без соответствующего файла .js.

Компромисс: размер файла

Файлы байт-кода значительно больше чем исходный код — обычно в 2-8 раз больше.

Почему байт-код такой большой?

Инструкции байт-кода многословны: Одна строка минифицированного JavaScript может компилироваться в десятки инструкций байт-кода. Например:

js
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 загружает байт-код:

  1. Извлекает версию кэша из файла .jsc
  2. Вычисляет текущую версию JavaScriptCore
  3. Если они не совпадают байт-код молча отклоняется
  4. Bun возвращается к парсингу исходного кода .js

Ваше приложение все еще работает — вы просто теряете оптимизацию производительности.

Грациозная деградация: Этот дизайн означает что кэширование байт-кода «неудачно открыто» — если что-то идет не так (несоответствие версий поврежденный файл отсутствующий файл) ваш код все еще работает нормально. Вы можете увидеть более медленный запуск но не увидите ошибок.

Несвязанный и связанный байт-код

JavaScriptCore делает важное различие между «несвязанным» и «связанным» байт-кодом. Это разделение — то что делает кэширование байт-кода возможным:

Несвязанный байт-код (что кэшируется)

Байт-код сохраненный в файлах .jsc — это несвязанный байт-код. Он содержит:

  • Скомпилированные инструкции байт-кода
  • Структурную информацию о коде
  • Константы и идентификаторы
  • Информацию о потоке управления

Но он не содержит:

  • Указатели на фактические объекты времени выполнения
  • JIT-компилированный машинный код
  • Данные профилирования из предыдущих запусков
  • Информацию о ссылке вызова (какие функции какие вызывают)

Несвязанный байт-код неизменяем и доступен для совместного использования. Несколько выполнений одного и того же кода могут ссылаться на один и тот же несвязанный байт-код.

Связанный байт-код (выполнение во время выполнения)

Когда Bun выполняет байт-код он «связывает» его — создавая обертку времени выполнения которая добавляет:

  • Информацию о ссылке вызова: Когда ваш код выполняется движок узнает какие функции какие вызывают и оптимизирует эти места вызова.
  • Данные профилирования: Движок отслеживает сколько раз выполняется каждая инструкция какие типы значений проходят через код шаблоны доступа к массивам и т.д.
  • Состояние JIT-компиляции: Ссылки на базовый JIT или оптимизирующий JIT (DFG/FTL) скомпилированные версии горячего кода.
  • Объекты времени выполнения: Указатели на фактические объекты JavaScript прототипы области видимости и т.д.

Это связанное представление создается заново каждый раз когда вы запускаете код. Это позволяет:

  1. Кэшировать дорогую работу (парсинг и компиляция в несвязанный байт-код)
  2. Все еще собирать данные профилирования времени выполнения для руководства оптимизациями
  3. Все еще применять оптимизации JIT на основе фактических шаблонов выполнения

Кэширование байт-кода переносит дорогую работу (парсинг и компиляцию в байт-код) из времени выполнения во время сборки. Для приложений которые часто запускаются это может сократить время запуска вдвое за счет больших файлов на диске.

Для продакшн CLI и серверных развертываний комбинация --bytecode --minify --sourcemap дает вам лучшую производительность сохраняя возможность отладки.

Bun от www.bunjs.com.cn