CI بطيء؟ npm cache في GitHub Actions ينزل install لـ 55 ثانية
لو كل Pull Request عندك بيستنى 4 دقائق على npm ci، المقال ده هيخليك تقلل الانتظار غالبًا لأقل من دقيقة في التشغيلات المتكررة، بدون ما تخبي تغييرات dependencies.
مستوى القارئ: متوسط
المشكلة باختصار
اللي بيحصل فعلاً إن GitHub Actions بيبدأ غالبًا من runner نظيف. معنى كده إن npm ci بينزل packages من جديد، حتى لو package-lock.json متغيرش من آخر run. في مشروع React أو Next.js متوسط، خطوة التثبيت ممكن تاخد من 3 إلى 5 دقائق، بينما الاختبارات نفسها تاخد أقل من دقيقة.
الطريقة الشائعة الغلط هي إنك تعمل cache لـ node_modules كله. الطريقة دي بتفشل لما نظام التشغيل أو نسخة Node أو lockfile يتغيروا. البديل الأفضل: خلّي actions/setup-node يعمل cache لمجلد npm العالمي، وسيب npm ci يبني node_modules بشكل نظيف من lockfile.
مثال واقعي قبل الحل
افترض إن عندك repo فيه 900 dependency، وبيطلع 25 Pull Request في اليوم. لو npm ci بياخد 250 ثانية في كل run، فأنت بتصرف حوالي 104 دقائق يوميًا في تثبيت مكرر. بعد cache hit مستقر، الرقم ممكن ينزل إلى 55–65 ثانية. ده وفر تقريبي 75% في خطوة التثبيت، وليس في كامل الـ pipeline.
ركز في النقطة دي: الـ cache لا يسرّع الاختبارات نفسها. هو يقلل وقت تحميل tarballs وإعادة بناء جزء من dependency install. لو test suite عندك بطيء 12 دقيقة، cache npm مش هيحل أصل المشكلة.
الإعداد العملي
أفضل طريقة في npm هي استخدام actions/setup-node مع cache: 'npm'. الأداة تستخدم lockfile لتوليد cache key، وتستدعي actions/cache تحت الغطاء. مثال workflow كامل:
name: ci
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 24
cache: npm
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
لو عندك monorepo، متحطش مسار واحد بالغلط. استخدم مسارات lockfiles الفعلية:
- uses: actions/setup-node@v6
with:
node-version: 24
cache: npm
cache-dependency-path: |
apps/web/package-lock.json
packages/ui/package-lock.json
الافتراض هنا إنك تستخدم npm مع package-lock.json. لو بتستخدم pnpm أو Yarn، نفس الفكرة موجودة، لكن cache input والمسارات تختلف.
إزاي تقيس قبل وبعد
متقولش “الـ CI بقى أسرع” وخلاص. افتح آخر 5 runs قبل التعديل، وسجّل زمن خطوة Install dependencies. بعد التعديل، شغّل pipeline مرتين. أول run غالبًا cache miss وهيكون قريب من القديم. ثاني وثالث run هم اللي يهموك.
مثال قياس معقول:
- قبل cache: 248s، 252s، 246s.
- أول run بعد التعديل: 251s بسبب cache miss.
- بعد cache hit: 62s، 55s، 58s.
لو ما شفتش فرق، راجع هل cache-dependency-path صحيح، وهل workflow بيشتغل على نفس branch أو بيرجع للـ default branch cache حسب قواعد GitHub.
الـ trade-off هنا
المكسب واضح: زمن أقل في كل PR وتكلفة Actions أقل لو بتدفع بالدقائق. الثمن: cache storage ممكن يوصل للحد الافتراضي، وGitHub قد يحذف caches غير المستخدمة بعد مدة. كمان cache key غلط ممكن يعمل cache thrashing، يعني caches كتير بتتعمل وتتطرد بدون فائدة.
علشان كده، لا تستخدم key واسع جدًا زي npm-${{ runner.os }} فقط. خليه مربوط بـ lockfile. ولما تغير dependencies، cache جديد يتولد طبيعيًا. ده بالظبط المطلوب.
متى لا تستخدم هذه الطريقة
لا تستخدمها لو install step أصلاً أقل من 20 ثانية. التعقيد هنا مش مستاهل. لا تعتمد عليها كحل لمشكلة tests بطيئة أو build بطيء؛ دي مشاكل مختلفة. ولا تعمل cache لـ node_modules في مشروع بيتغير فيه Node version أو native modules كتير، لأن احتمالية الأعطال هتزيد.
مصادر اعتمدت عليها
- actions/setup-node: caching npm dependencies
- GitHub Actions dependency caching reference
- npm ci documentation
الخطوة التالية
افتح workflow الأساسي عندك، وضيف cache: npm وcache-dependency-path. بعد 3 runs، قارن زمن خطوة npm ci فقط، مش زمن الـ pipeline كله.