Как сократить время сборки: замеры до и после
Сборка Node.js-проекта в Docker может тратить минуты на повторную установку зависимостей. Показываем, как измерить узкие места и настроить кеш BuildKit так, чтобы изменение исходников не запускало весь процесс заново.

Если Docker-сборка Node.js-проекта каждый раз заново устанавливает пакеты, даже когда изменился один файл, вы платите временем за неверный порядок слоёв. На примере Node.js 22.x и Docker BuildKit разберём, как измерить этапы сборки, сохранить кеш зависимостей и посчитать выигрыш без выдуманных «ускорений в три раза».
Сначала замерьте, на что уходит время
Общая длительность сборки сама по себе мало что объясняет. Разделите её на этапы: получение исходников, установка зависимостей, компиляция или сборка приложения, создание образа и отправка его в реестр. В Docker BuildKit смотрите длительность отдельных шагов в выводе сборки, а время всей задачи берите из CI.
Сравнивайте не случайные запуски, а одинаковые условия: один и тот же коммит, тип CI-исполнителя, версия Node.js, lock-файл и параметры сборки. Отдельно измеряйте две ситуации:
- Холодная сборка: кеша нет или он недоступен исполнителю.
- Повторная сборка: кеш сохранён, а в исходниках изменился один файл.
- Сборка после изменения зависимостей: обновился package-lock.json и слой установки пакетов нужно пересоздать.
Снимите несколько повторов каждого сценария и сравнивайте медиану, а не самый быстрый запуск. Для небольшого проекта достаточно начать с десяти запусков на сценарий; при высокой вариативности CI увеличьте выборку. Не складывайте длительность параллельных шагов: оценивайте фактическое время от старта задачи до готового образа.
Ускорение имеет смысл только вместе с ответом на вопрос: какой именно этап стал короче и при каких условиях кеш сработал?
Перенесите изменяемые исходники после зависимостей
Кеш слоёв Docker зависит от инструкций и файлов, от которых они зависят. Если сначала скопировать в образ весь проект, а затем выполнить npm ci, любое изменение исходного кода может сбросить кеш установки пакетов. Для Node.js-приложения с package-lock.json сначала копируют манифесты, устанавливают зависимости и только потом добавляют исходники.
Пример рассчитан на Docker BuildKit и Node.js 22.x. В package.json должен быть сценарий build, а в репозитории — package-lock.json:
# syntax=docker/dockerfile:1
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
Директива syntax включает поддержку синтаксиса Dockerfile для BuildKit; сборка должна выполняться с включённым BuildKit. Кешируемый каталог /root/.npm хранит загруженные npm-пакеты между подходящими запусками. Он не заменяет node_modules: после сброса слоя npm ci всё равно собирает зависимости заново, но может повторно использовать сохранённые архивы.
Добавьте в .dockerignore как минимум локальные node_modules и каталог .git, чтобы не копировать их в контекст сборки. Не исключайте package-lock.json: npm ci использует lock-файл и завершится ошибкой, если он отсутствует или расходится с package.json.
Где появляется выигрыш, а где нет
Перестановка инструкций помогает при повторных сборках, когда меняется код, но остаются прежними package.json и package-lock.json. При изменении lock-файла слой npm ci инвалидируется — и это ожидаемое поведение, а не поломка кеша. При холодном запуске на новом исполнителе выигрыш будет зависеть от того, перенесён ли кеш BuildKit между задачами CI.
Здесь важно различать кеш слоя и кеш пакетов npm. Первый позволяет пропустить уже выполненный шаг целиком, если его входы не изменились. Второй помогает повторно использовать загруженные архивы, когда установку всё-таки приходится запускать. Если CI каждый раз создаёт чистую машину и не экспортирует кеш сборки, нельзя считать, что локальная настройка автоматически сохранит его между заданиями.
| Подход | Когда помогает | Цена и ограничение |
|---|---|---|
| Манифесты до исходников | Код меняется чаще, чем зависимости | Нужно перестроить слой при изменении lock-файла |
| Кеш npm в BuildKit | npm ci повторяется, но часть архивов уже загружена | Не отменяет установку и требует работающего BuildKit-кеша |
| Перенос кеша между CI-запусками | Исполнители временные, но сборки регулярно повторяются | Нужно настроить экспорт и загрузку кеша средствами CI; детали зависят от платформы |
Как посчитать эффект без маркетинговых процентов
Представим условный замер, а не результат реального теста: сборка занимает 12 минут, из них 7 минут — установка зависимостей, 3 — npm run build и 2 — остальные этапы. Если повторная сборка действительно пропустила семиминутный слой установки, арифметический потолок экономии в этом сценарии — 7 минут: сборка сократится до 5 минут, то есть примерно на 58%. Это не обещание для любого проекта: кеш может не сохраниться, а компиляция приложения останется прежней.
Для собственного отчёта запишите время каждого этапа до и после изменения и процент изменения: (время до − время после) / время до × 100%. Рядом укажите, был ли запуск холодным или повторным, менялся ли lock-файл и откуда взялся кеш. Если общий показатель улучшился, а длительность npm run build нет, ускорилась установка или доставка образа — не сама сборка приложения.
Слой Docker — результат выполнения одной инструкции Dockerfile вместе с её входами. Если входные файлы и команда совпадают с предыдущей сборкой, Docker может повторно использовать результат; изменение одного из них может заставить выполнить шаг заново.
Что ломается первым
- В CI нет сохранённого кеша. Локально сборка быстрая, а на чистом исполнителе каждый раз начинается с нуля. Проверьте, экспортирует ли ваша CI-система кеш BuildKit и загружает ли его перед сборкой.
- Код скопирован до установки пакетов. Изменение любого файла сбрасывает зависимый слой. Переместите COPY исходников после шага npm ci.
- Кешируют не тот каталог. npm использует каталог кеша, заданный конфигурацией пользователя. В стандартном Node-образе под root это /root/.npm; при запуске сборки от другого пользователя проверьте фактическое расположение.
- Сборка стала «быстрой» за счёт старого результата. Не используйте устаревший образ или пропуск сборочного шага как замену кешу. Проверяйте, что финальный образ содержит результат именно текущего коммита.
Отдельный риск — секреты и приватные зависимости. Не записывайте токены в Dockerfile или аргументы сборки: они могут попасть в историю слоёв или логи. Для закрытого реестра используйте механизм секретов CI и следуйте документации выбранной платформы.
Что делать дальше
Сначала сохраните исходные замеры, затем поменяйте только порядок слоёв и повторите тесты на том же исполнителе. Если ускорилась повторная сборка, но холодная осталась медленной, следующим шагом настройте перенос кеша BuildKit между заданиями вашей CI-системы. После этого отдельно измерьте npm run build: если именно он занимает большую часть времени, оптимизировать нужно уже сборщик и граф зависимостей, а не Docker-кеш.
Инвалидация кеша при обновлении зависимостей и настройка экспорта для конкретной CI-платформы — отдельные темы; здесь речь только о замере и повторном использовании слоёв в Docker-сборке.
Источники: Docker: оптимизация кеширования сборки, npm CLI v10: команда npm ci
Частые вопросы
Насколько быстрее станет сборка после настройки кеша?
Почему на компьютере быстро, а в CI всё ещё медленно?
Что произойдёт, если обновить package-lock.json?
Подойдёт ли пример для другой версии Node.js?
Что делать, если BuildKit не использует кеш?
Есть ли подводные камни для production-сборок?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.