Перейти к содержанию
Innovialabs

Как ускорить CI-пайплайн: кеширование, параллелизм и другое

Практика3 мин

Коротко

Сначала измерьте, какие шаги занимают время, затем кешируйте зависимости, разбейте работу на параллельные задачи, запускайте только затронутые тесты и уменьшите Docker-образы.

Короткий ответ

Медленный CI почти всегда тратит время на одно и то же: скачивание зависимостей, последовательный запуск всего подряд и сборку тяжёлых образов. Порядок действий:

  1. Измерить, где уходит время.
  2. Кешировать зависимости и артефакты сборки.
  3. Распараллелить независимые задачи.
  4. Запускать только то, что затронуто изменением.
  5. Уменьшить образы и раннер-окружение.

Шаг 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'ы, где установка зависимостей дольше самой работы.

Частые вопросы

С кеширования зависимостей и отмены устаревших запусков через concurrency. Это несколько строк конфигурации, а эффект заметен почти в любом проекте.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.