У сучасних високонавантажених монорепозиторіях і масштабних корпоративних застосунках на базі 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 |
+---------------------------+.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Часта помилка при міграції з 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.
Аналіз тривалості фаз поточного 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]).