В современных высоконагруженных монорепозиториях и крупных корпоративных приложениях на базе Next.js 15+ время прохождения пайплайна непрерывной интеграции (Continuous Integration) напрямую влияет на Time-to-Market и производительность инженерной команды. Классический «наивный» CI-пайплайн, выполняющий чистую установку зависимостей через npm install и полную компиляцию проекта с нуля, в среднем занимает от 8 до 15 минут. Основные узкие места такого подхода кроются в архитектуре эфемерных раннеров (Ephemeral Runners) GitHub Actions:
ubuntu-latest с 2 vCPU и 7 ГБ RAM). Сетевая загрузка гигабайтов node_modules и повторный парсинг tar-архивов упираются в лимиты I/O дисковой подсистемы Azure, выделенной для виртуальной машины..next/cache процессор тратит сотни секунд на повторный расчет неизмененных файлов.@repo/ui не изменялся, повторный запуск его сборки и тестирования представляет собой нерациональный расход вычислительных ресурсов.Оптимизация CI-пайплайна строится на трех уровнях кэширования:
npm или yarn classic, которые копируют зависимости в каждую директорию, pnpm использует контентно-адресуемое глобальное хранилище (Content-Addressable Storage, CAS). В раннере необходимо кэшировать именно системное хранилище виртуального стора ~/.local/share/pnpm/store/v3, а не саму директорию node_modules. При наличии кэша развертывание дерева зависимостей осуществляется через создание жестких ссылок (hardlinks), что снижает время фазы pnpm install с 3 минут до 8–15 секунд..next/cache). Внутренний кэш Next.js содержит результаты работы SWC, чанки вебпака/турбопака и предрассчитанные страницы SSG. Восстановление этого каталога позволяет компилятору переиспользовать кэшированные абстрактные синтаксические деревья и дельты изменений.>>> FULL TURBO).Взаимодействие компонентов пайплайна можно представить как многоуровневый фильтр с проверкой инвалидации кэша перед передачей управления вычислительным ядрам раннера.
[ GitHub Event: Pull Request / Push ]
|
v
+------------------------------+
| GitHub Actions Runner Init |
| (Ephemeral VM: ubuntu-latest)|
+------------------------------+
|
[ Git Checkout v4 ]
|
v
+-------------------------------------------------------------------+
| 1. Восстановление кэша зависимостей pnpm |
| Ключ: setup-pnpm-store-${{ runner.os }}-${{ hash(pnpm-lock) }} |
| Match? ---> [ HIT: Линк из CAS за 10с ] |
| ---> [ MISS: Скачивание из registry + запись в CAS ] |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| 2. Восстановление инкрементального кэша Next.js |
| Ключ: nextjs-${{ runner.os }}-${{ hash(lock) }}-${{ commit }} |
| Restore-keys: nextjs-${{ runner.os }}-${{ hash(lock) }}- |
| Match? ---> [ HIT / PARTIAL: Восстановление .next/cache ] |
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| 3. Запуск Turborepo с Remote Caching |
| Расчет SHA-256 хэша графа задач (Inputs -> Hash) |
| Match в Remote Cache? |
| ├── [ HIT: Загрузка артефактов из S3 / Vercel -> 0 сек ] |
| └── [ MISS: Локальная компиляция -> Пуш в Remote Cache ] |
+-------------------------------------------------------------------+
|
v
+---------------------------+
| Success / Pipeline Status |
+---------------------------+Анализ длительности фаз текущего workflow. Измерение времени pnpm install, компиляции Next.js и проверка объема загружаемых артефактов.
Замена прямого кэширования node_modules на кэширование ~/.local/share/pnpm/store/v3 с флагами --frozen-lockfile и --prefer-offline.
Изоляция каталога .next/cache с ключами на базе pnpm-lock.yaml и github.sha с fallback restore-keys для инкрементальной дельты.
Генерация TURBO_TOKEN, добавление TURBO_TEAM в секреты репозитория и строгая настройка секций env/outputs в turbo.json.
Настройка отмены устаревших запусков cancel-in-progress и запуск задач только для измененных пакетов через git diff фильтры (...[origin/main]).
Почему в CI-пайплайне на базе pnpm необходимо кэшировать директорию ~/.local/share/pnpm/store/v3, а не саму папку node_modules?
.next/cache обеспечивает сохранение результатов пререндеринга статических страниц и кэша компилятора SWC, предотвращая деградацию квоты бесплатных минут GitHub Actions.@repo/ui Turborepo пересобирает исключительно затронутое клиентское приложение, в то время как 5 других сервисов получают мгновенный статус [FULL TURBO] из кэша.Для достижения максимальной производительности пайплайн строится с соблюдением строгих правил изоляции, детерминизма и правильного выбора кэш-ключей.
Типичная ошибка большинства инженеров — попытка напрямую кэшировать директорию node_modules. В случае pnpm это приводит к катастрофическим последствиям: разрушаются символические и жесткие ссылки виртуального стора, а объем архива может превышать несколько гигабайт, перегружая сеть.
Правильный подход — кэшировать глобальное контентно-адресуемое хранилище pnpm (pnpm store path), используя официальный action pnpm/action-setup либо стандартный actions/cache@v4.
- name: Установка pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
run_install: false
- name: Определение пути к pnpm store
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Восстановление кэша зависимостей pnpm
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Установка зависимостей с жестким контролем lockfile
run: pnpm install --frozen-lockfile --prefer-offlineФлаг --frozen-lockfile гарантирует, что CI упадет с ошибкой, если pnpm-lock.yaml рассинхронизирован с package.json. Флаг --prefer-offline инструктирует pnpm сначала проверять локальный CAS-стор перед отправкой HTTP-запросов к реестру npmjs.
Компилятор Next.js сохраняет состояние сборки в директорию <app-dir>/.next/cache. Сюда входят:
Ключ кэша должен учитывать хэш pnpm-lock.yaml (так как обновление мажорной версии Next.js инвалидирует бинарный кэш) и хэш коммита:
- name: Восстановление инкрементального кэша Next.js
uses: actions/cache@v4
with:
path: |
${{ github.workspace }}/apps/web/.next/cache
# Ключ включает хэш lockfile и хэш коммита для детерминизма
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
# При отсутствии точного совпадения подтягивается самый свежий кэш ветки/репозитория
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-При выполнении команды turbo run build утилита рассчитывает композитный хэш:
Hash = SHA256(TaskDefinition + SourceFilesContent + EnvVars + DependenciesOutputs)
Для работы с Remote Cache в CI раннере передаются переменные окружения TURBO_TOKEN и TURBO_TEAM (для Vercel Remote Cache) либо TURBO_API (для self-hosted решений, например duesee/turbocache).
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"globalEnv": ["NODE_ENV", "NEXT_PUBLIC_API_URL"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": [
"$TURBO_DEFAULT$",
".env.production"
],
"outputs": [
".next/**",
"!.next/cache/**",
"dist/**"
],
"env": [
"STRIPE_PUBLIC_KEY",
"NEXT_PUBLIC_ANALYTICS_ID"
]
},
"lint": {
"outputs": []
},
"typecheck": {
"outputs": []
}
}
}Обратите внимание на директиву "outputs": [".next/**", "!.next/cache/**"]. Каталог .next/cache исключается из артефактов Turborepo, так как он управляется отдельным механизмом actions/cache, что снижает размер артефакта Turborepo с 600 МБ до 50 МБ.
Выделение виртуальной машины ubuntu-latest, мелкий клон репозитория через actions/checkout с глубиной fetch-depth: 2 для корректного diff.
Поиск в CAS-хранилище по хэшу lockfile. В 95% случаев дает Cache HIT, пропуская сетевую загрузку tar-архивов.
Загрузка артефактов компилятора SWC и чанков Webpack по restore-keys предыдущего успешного билда в ветке.
Расчет графа задач. Задачи lint, typecheck и build запускаются параллельно. Неизмененные пакеты получают статус FULL TURBO.
При изменении зависимостей или новых исходниках обновленный кэш Next.js и pnpm упаковывается и загружается в хранилище GitHub Actions.
Ниже представлен полностью готовый, промышленный workflow .github/workflows/ci.yml, реализующий трехуровневое кэширование, разделение ответственности по джобам, контроль конкурентности и безопасную обработку токенов.
name: CI Pipeline
on:
push:
branches:
- main
- develop
pull_request:
types:
- opened
- synchronize
- reopened
# Предотвращение параллельных запусков для одной ветки (экономия ресурсов)
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
NODE_OPTIONS: "--max-old-space-size=6144"
jobs:
validate:
name: Validation & Typecheck
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
actions: write
steps:
- name: Checkout репозитория
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Установка среды Node.js
uses: actions/setup-node@v4
with:
node-version: 22.x
- name: Установка pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0
run_install: false
- name: Получение пути к глобальному хранилищу pnpm
id: pnpm-cache-dir
shell: bash
run: |
echo "dir=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Восстановление кэша pnpm Store
uses: actions/cache@v4
id: pnpm-cache
with:
path: ${{ steps.pnpm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Установка зависимостей
run: pnpm install --frozen-lockfile --prefer-offline
- name: Восстановление кэша сборки Next.js (.next/cache)
uses: actions/cache@v4
with:
path: |
${{ github.workspace }}/apps/*/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-
- name: Запуск Quality Gates через Turborepo
run: pnpm turbo run lint typecheck --filter=...[origin/main]
- name: Сборка приложений с инкрементальным кэшем
run: pnpm turbo run build --filter=...[origin/main]
env:
NEXT_TELEMETRY_DISABLED: 1
NODE_ENV: productionПочему при настройке actions/cache для Next.js критично кэшировать исключительно путь .next/cache, а не весь каталог .next?
Частая ошибка при миграции с npm на pnpm — сохранение в actions/cache директории node_modules. В pnpm директория node_modules заполнена жесткими и символическими ссылками, указывающими на централизованный виртуальный каталог. При упаковке в tar.gz архиватор либо разворачивает симлинки, дублируя гигабайты файлов, либо ломает ссылки при распаковке на другом раннере.
# НЕПРАВИЛЬНО: Попытка кэшировать локальную директорию node_modules
- uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('pnpm-lock.yaml') }}
# ПРАВИЛЬНО: Кэширование системного CAS хранилища pnpm
- name: Экспорт пути store
id: pnpm-cache
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}Попытка кэшировать каталог .next целиком. Каталог .next содержит скомпилированные клиентские и серверные чанки (.next/static, .next/server), манифесты маршрутов и метаданные сборки конкретного коммита. Восстановление всей директории приводит к повреждению манифестов (manifest corruption), несоответствию хэшей чанков и падениям с ошибкой ChunkLoadError в продакшене.
# НЕПРАВИЛЬНО: Кэширование артефактов финальной сборки
- uses: actions/cache@v4
with:
path: apps/web/.next
key: ${{ runner.os }}-next-build-${{ github.sha }}
# ПРАВИЛЬНО: Кэширование исключительно директории промежуточных данных компилятора
- uses: actions/cache@v4
with:
path: apps/web/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-Если сборка Next.js зависит от переменной NEXT_PUBLIC_API_URL, но эта переменная не задекларирована в секциях env или globalEnv файла turbo.json, Turborepo вернет успешный кэш FULL TURBO, собранный для старого или тестового URL. В результате в продакшн уйдет бандл, обращающийся к неверному эндпоинту.
// НЕПРАВИЛЬНО: Переменные окружения не участвуют в расчете хэша
{
"tasks": {
"build": {
"outputs": [".next/**", "!.next/cache/**"]
}
}
}
// ПРАВИЛЬНО: Явная декларация внешних зависимостей
{
"tasks": {
"build": {
"env": ["NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_SENTRY_DSN"],
"outputs": [".next/**", "!.next/cache/**"]
}
}
}При частых коммитах в ветку PR без настройки concurrency.cancel-in-progress: true GitHub Actions запускает несколько параллельных билдов одного и того же PR. Каждый раннер пытается записать кэш размером 500+ МБ, что быстро приводит к превышению квоты кэша репозитория (10 ГБ). При превышении лимита GitHub запускает политику вытеснения LRU (Least Recently Used), удаляя критически важный базовый кэш ветки main.