Как ускорить CI-пайплайн: кеширование, параллелизм и другое
Практика3 минРедакция Innovialabs
Коротко
Сначала измерьте, какие шаги занимают время, затем кешируйте зависимости, разбейте работу на параллельные задачи, запускайте только затронутые тесты и уменьшите Docker-образы.
Короткий ответ
Медленный CI почти всегда тратит время на одно и то же: скачивание зависимостей, последовательный запуск всего подряд и сборку тяжёлых образов. Порядок действий:
- Измерить, где уходит время.
- Кешировать зависимости и артефакты сборки.
- Распараллелить независимые задачи.
- Запускать только то, что затронуто изменением.
- Уменьшить образы и раннер-окружение.
Шаг 1. Измерьте, прежде чем оптимизировать
Откройте несколько последних запусков и выпишите длительность каждого шага. Обычно сразу видно 1–2 шага, которые занимают большую часть времени. Оптимизировать остальное бессмысленно, пока они не решены.
Что смотреть:
- Ожидание раннера — задача стоит в очереди, а не выполняется.
- Установка зависимостей —
npm ci,pip install,go mod download. - Тесты — какие наборы самые долгие, есть ли медленные отдельные тесты.
- Сборка образов — работает ли кеш слоёв.
Шаг 2. Кеширование
Кеш — самый дешёвый выигрыш. Ключ кеша строится из lock-файла: пока зависимости не меняются, они берутся из кеша.
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
Что ещё стоит кешировать:
- кеш компилятора и сборщика (например,
.next/cache, кеш Gradle, кеш Go build); - слои Docker через BuildKit (
cache-from/cache-to); - скачанные браузеры для e2e-тестов.
Не кешируйте то, что быстрее скачать, чем восстановить, и следите, чтобы ключ кеша менялся при изменении зависимостей.
Шаг 3. Параллелизм
Линтер, проверка типов, unit-тесты и сборка обычно не зависят друг от друга — запускайте их отдельными job'ами одновременно.
Большой набор тестов можно разделить на части (sharding) через matrix:
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx playwright test --shard=${{ matrix.shard }}/4
Учтите: каждая параллельная задача тратит время на старт и установку зависимостей. Дробить имеет смысл, пока выигрыш больше этих накладных расходов.
Шаг 4. Запускайте только затронутое
Если изменён только README, гонять полный набор тестов незачем.
- Фильтры путей (
paths,paths-ignore) отключают workflow для нерелевантных изменений. - В монорепозиториях инструменты вроде Nx и Turborepo определяют затронутые пакеты по графу зависимостей и кешируют результаты задач.
- Jest умеет
--changedSince=origin/main.
Полный прогон оставьте для main-ветки или ночного запуска, чтобы ничего не пропустить.
Шаг 5. Лёгкие образы и окружение
- Используйте slim/alpine базовые образы там, где это не ломает зависимости.
- Multi-stage build: в финальный образ — только то, что нужно для запуска.
- Если инструменты ставятся на каждом запуске, соберите свой образ-раннер с ними заранее.
fetch-depth: 1при checkout, если история Git не нужна.
Частые ошибки
- Оптимизировать вслепую, не посмотрев на длительность шагов.
- Нестабильные (flaky) тесты с автоматическими перезапусками — они маскируют проблему и съедают время.
- Отмена дублей не настроена. Используйте
concurrencyсcancel-in-progress, чтобы новый пуш отменял старый запуск той же ветки. - Слишком дробные job'ы, где установка зависимостей дольше самой работы.