Кеш сборки в CI: что действительно ускоряет пайплайн
Кеш в CI не ускоряет сборку сам по себе: он может сократить время установки зависимостей и пересборки Docker-слоёв, а может добавить минуты на передачу бесполезного архива. Разбираем, что кешировать и как измерять эффект.

Кеш в CI способен убрать повторную установку зависимостей и пересборку Docker-слоёв, но иногда сам становится самым долгим шагом пайплайна. На примере GitHub Actions, Node.js 20 и Docker BuildKit разберём, какие данные кешировать, как выбрать ключ и почему высокий процент попаданий ещё не гарантирует ускорение.
Что именно стоит кешировать
У кеша в CI есть простой критерий: восстановление и сохранение данных должны занимать меньше времени, чем повторное получение или построение этих данных. Поэтому кешировать весь рабочий каталог проекта обычно плохая идея. В нём есть исходники, временные файлы, логи и результаты, которые проще создать заново.
| Что кешировать | Что это ускоряет | Главный риск |
|---|---|---|
| Кеш менеджера пакетов | npm ci, установка повторяющихся архивов | Кеш не заменяет установку и не должен использоваться как готовый node_modules |
| Слои Docker BuildKit | Повторную сборку неизменившихся шагов Dockerfile | Большой кеш может долго скачиваться и загружаться |
| Кеш компилятора | Повторную компиляцию неизменившихся файлов | Нужна корректная привязка к версии компилятора и флагам |
| Результаты тестов и сборки | Повторный запуск дорогих вычислений | Это часто артефакты, а не кеш: результат нужно скачать пользователю или передать на следующий job |
Хороший кеш убирает повторную работу. Плохой кеш просто добавляет к ней скачивание большого архива.
Сноска. Cache hit — попадание в кеш, когда найдено сохранённое значение для текущего ключа. Cache miss — промах: данные приходится создавать заново, после чего CI может сохранить новый набор.
Первый слой: кеш зависимостей
Для Node.js в GitHub Actions разумная отправная точка — кешировать хранилище npm, а не каталог node_modules. Пакетный менеджер всё равно должен выполнить установку и проверить дерево зависимостей, но ему не придётся заново скачивать каждый архив из реестра.
В примере используется GitHub Actions с actions/checkout@v4 и actions/setup-node@v4. Версия Node.js указана явно, чтобы кеш не смешивался между несовместимыми средами.
name: test
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
Параметр cache: npm включает кеш глобального хранилища npm. Файл package-lock.json участвует в вычислении ключа: изменение зависимостей создаёт новый набор, а старый не используется ошибочно.
Проверка результата — посмотреть в логе job сообщения Cache restored successfully или сообщение о промахе. Для честного сравнения запишите длительность шагов Set up Node.js и Install dependencies на чистом запуске и на повторном запуске.
Почему кешировать node_modules обычно невыгодно
Готовый node_modules кажется более быстрым решением, но он привязан к операционной системе, архитектуре, версии Node.js и иногда к нативным библиотекам. Архив может оказаться огромным, а восстановление — сравнимым по времени с обычным npm ci.
Такой подход особенно хрупок в матрице, где один workflow запускается на Linux, macOS и Windows или на разных версиях Node.js. Если всё же нужен кеш этого каталога, в ключ должны входить как минимум ОС, архитектура, версия Node.js и хеш lock-файла. Но сначала измерьте этот вариант против кеша npm: универсального выигрыша нет.
Второй слой: кеш Docker BuildKit
Если CI собирает образ, кеш npm не спасает от повторной работы внутри Dockerfile. BuildKit видит каждый шаг как слой и может использовать результат, если входные файлы команды не изменились.
Критический приём — копировать файлы зависимостей раньше исходного кода. Тогда изменение src/ не заставит Docker повторять установку пакетов.
FROM node:20-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
Для GitHub Actions кеш BuildKit можно хранить через backend GitHub Actions. Ниже фрагмент job, который использует официальные actions версий docker/setup-buildx-action@v3 и docker/build-push-action@v6.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image
uses: docker/build-push-action@v6
with:
context: .
push: false
tags: example/app:ci
cache-from: type=gha
cache-to: type=gha,mode=max
mode=max сохраняет больше промежуточных данных, поэтому повторная сборка может найти больше слоёв. Цена — больший объём загрузки и выгрузки. Для небольшого Dockerfile это может дать минимальный эффект или даже замедлить job.
Не путайте кеш BuildKit с готовым образом. Если следующий job должен запустить или передать образ, сохраните образ отдельным артефактом либо отправьте его в реестр. Кеш предназначен для ускорения следующей сборки и может быть удалён системой.
Как выбрать между кешами
Кеши можно использовать одновременно, но они решают разные задачи. В проекте с Docker кеш npm на хосте не ускорит команду RUN npm ci внутри образа: у контейнера собственная файловая система и собственный контекст сборки.
- Обычный Node.js job без Docker. Начните с кеша npm через
setup-node. Это самый дешёвый для проверки вариант. - Сборка Docker-образа. Сначала исправьте порядок
COPYв Dockerfile, затем добавьте кеш BuildKit. Иначе кеш будет сбрасываться после любого изменения исходников. - Монорепозиторий. Указывайте конкретные lock-файлы в
cache-dependency-pathили в правилах ключа. Общий ключ для всех пакетов создаёт лишние промахи и смешивает независимые проекты. - Нативные зависимости. Не переносите кеш между разными ОС, архитектурами и версиями Node.js без проверки совместимости.
Что ломается первым
Ключ не меняется после обновления зависимостей
Если ключ строится только по имени ветки, после изменения package-lock.json CI может восстановить старый кеш. Для зависимостей частью ключа должен быть lock-файл. В GitHub Actions это уже учитывается при использовании встроенного кеша setup-node.
Кешируют то, что дешевле скачать заново
Архив на несколько гигабайт может восстанавливаться дольше, чем повторная компиляция. Сравнивайте не только время сборки, но и длительность шагов restore и save. Если сохранение кеша регулярно продлевает job, уменьшите его состав или отключите кеш для этого job.
Сохраняют кеш после неуспешной сборки
Неудачный запуск может оставить неполный набор файлов. Для зависимостей безопаснее использовать lock-файл и проверку менеджера пакетов, а для сборки — не подменять проверяемые артефакты кешем. Кеш не должен быть единственным источником файлов, без которых тесты проходят.
Скрывают промахи за общим временем job
Среднее время workflow мало что говорит. Один запуск может попасть в кеш, другой — нет. Смотрите отдельно время восстановления, установки, сборки и сохранения, а также размер передаваемого кеша. Если платформа не показывает эти данные удобно, добавьте в CI измерение времени вокруг команд и сравните серию запусков на одинаковом runner.
Как измерить реальный выигрыш
Не сравнивайте один случайный запуск с другим. Возьмите один commit и выполните несколько запусков с очищенным кешем, затем несколько повторных запусков с прогретым кешем. Отдельно запишите:
- время установки зависимостей или сборки без кеша;
- время восстановления кеша;
- время самой команды после восстановления;
- время сохранения кеша;
- размер кеша и долю попаданий.
Формула полезного эффекта проста: кеш ускоряет pipeline только тогда, когда время повторно выполненной работы больше суммы восстановления и сохранения кеша. Если кеш используется только в pull request job, его нужно оценивать вместе с частотой запуска и стоимостью runner, а не только с числом секунд в одном запуске.
Что делать дальше
Начните с одного дорогого шага — например, npm ci или сборки Docker-образа. Включите только соответствующий кеш, зафиксируйте пять запусков без прогрева и пять с прогретым кешем. Затем проверьте, не растут ли размер архива и время сохранения после добавления новых зависимостей.
Для проекта с Docker следующим шагом будет проверка порядка слоёв и содержимого build context. Для обычного Node.js job — контроль lock-файла, версии Node.js и совместимости runner. Инвалидацию кеша при выпуске новой версии инструментов лучше планировать заранее: это отдельная задача, здесь важны только ускорение повторной работы и измерение его цены.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: GitHub Actions: кеширование зависимостей, Docker Build cache backends
Частые вопросы
Кеш npm ускоряет npm ci?
Можно ли кешировать node_modules в GitHub Actions?
Почему Docker BuildKit не использует кеш после изменения одного файла?
Безопасно ли использовать кеш как источник зависимостей?
Что делать, если кеш стал замедлять CI?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.