Best Practices for Caching npm vs Yarn vs pnpm in CI

Inefficient dependency resolution and redundant network I/O remain primary bottlenecks in modern frontend pipelines. Implementing deterministic cache strategies requires precise directory mapping and strict parity enforcement. Proper cache hygiene directly accelerates CI/CD Pipeline Architecture & Fundamentals while reducing compute costs and queue latency.

This guide details exact configurations, failure recovery workflows, and performance trade-offs. It ensures seamless integration with broader Artifact Management Strategies for Frontend Builds.

Cache Directory Mapping & Lockfile Hashing

Identify exact cache paths per package manager to prevent cross-contamination. Map lockfile checksums to cache keys for deterministic invalidation. Caching incorrect directories introduces silent corruption or massive storage bloat.

  • npm: ~/.npm and node_modules/.cache
  • Yarn Classic: ~/.cache/yarn/v6
  • Yarn Berry (v2+): .yarn/cache
  • pnpm: ~/.local/share/pnpm/store/v3

Implementation requires hashing the exact lockfile. Use hashFiles('**/package-lock.json') or equivalent. Never cache node_modules directly. Cache the package manager’s global store instead to preserve integrity checks.

Step-by-Step CI Implementation

Configure runner-specific cache directives with exact restore and save syntax. Implement fallback keys to handle partial cache hits gracefully. Always pair cache restoration with a frozen lockfile install.

GitHub Actions Configuration

- name: Cache npm dependencies
  uses: actions/cache@v3
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-npm-
- name: Install dependencies
  run: npm ci || { echo "Dependency resolution failed"; exit 1; }

Apply equivalent logic for Yarn and pnpm by adjusting the path and key hash targets.

GitLab CI Configuration

cache:
  key:
    files:
      - package-lock.json
  paths:
    - ~/.npm
  policy: pull-push

Run npm ci --frozen-lockfile immediately after cache restoration. Fail fast if the exit code is non-zero.

CircleCI Configuration

- restore_cache:
    keys:
      - v1-deps-{{ checksum "package-lock.json" }}
      - v1-deps-
- run:
    name: Install Dependencies
    command: npm ci || exit 1

Fallback keys ensure partial matches still reduce network overhead.

Rollback & Parity Safeguards

Prevent cache poisoning and environment drift through strict validation. Automated fallback mechanisms protect against corrupted stores.

  • Verify lockfile integrity using npm ci, yarn install --frozen-lockfile, or pnpm install --frozen-lockfile.
  • Trigger platform-specific binary recompilation for native modules like node-gyp or sharp.
  • Enforce cache TTLs and document manual purge workflows.

Implement a pre-flight check comparing cached store size against a known baseline. If the size mismatch exceeds 15%, force a full reinstall and invalidate the current cache key. This prevents silent dependency drift.

Performance Trade-offs & Disk I/O Optimization

Balance cache hit rates against storage costs and extraction latency. Evaluate hardlink versus symlink strategies carefully.

  • pnpm utilizes a content-addressable store, eliminating flat duplication across projects.
  • npm and Yarn duplicate packages per project, increasing I/O latency and storage overhead.
  • Ephemeral runner disk limits require strict eviction policies.

Monitor du -sh on cache directories to optimize retention windows. pnpm delivers the highest cache efficiency but demands runner OS compatibility. npm and Yarn offer broader compatibility at the cost of storage.

Common Failures & Resolution

Address these frequent pipeline failures with explicit recovery steps.

  • Symptom: Cache restore succeeds but install fails with ENOENT. Cause: Platform-specific native modules cached from mismatched OS/architecture. Resolution: Include runner.os and runner.arch in the cache key. Execute npm rebuild post-restore.
  • Symptom: Partial cache hit triggers full network download. Cause: Restore key fallback too broad or lockfile hash mismatch. Resolution: Implement granular restore-keys with exact prefix matching. Validate lockfile sync before cache lookup.
  • Symptom: CI runner disk quota exceeded. Cause: Unbounded cache growth or duplicate node_modules + store caching. Resolution: Exclude node_modules from cache. Set expire_in or max-age limits. Run a pre-job cleanup script.
  • Symptom: Stale dependencies persist after lockfile update. Cause: Cache key not invalidated on lockfile change. Resolution: Enforce strict hashFiles('**/*lock*') in the primary key. Verify frozen-lockfile exit codes.

FAQ

Should I cache node_modules or the package manager store?

Always cache the global store (~/.npm, ~/.cache/yarn, ~/.local/share/pnpm/store). Caching node_modules bypasses integrity checks, breaks native module compilation, and increases cache size by 3–5x.

How do I handle cache invalidation for monorepos with multiple lockfiles?

Use a composite hash key combining all workspace lockfiles: hashFiles('packages/**/package-lock.json', 'pnpm-lock.yaml'). Alternatively, implement a root-level lockfile to maintain a single cache key.

Why does pnpm cache perform better than npm/Yarn in CI?

pnpm uses a content-addressable store with hardlinks. Identical package versions across projects share disk blocks. npm and Yarn duplicate packages per project, increasing I/O latency and storage consumption.

What is the recommended fallback strategy for cache misses?

Implement a multi-tier restore key: exact lockfile hash → OS-specific prefix → generic fallback. Pair with npm ci or --frozen-lockfile to guarantee deterministic resolution. Log cache hit/miss metrics for pipeline observability.