Optimize npm installs in GitHub Actions
A fast npm install in GitHub Actions restores the npm cache through actions/setup-node's own cache input, caches large secondary downloads like browser binaries separately, and skips installing tools a job never runs; skipping any of the three means npm ci and its trailing install steps redo work from zero on every run.
ci.cache.npm-installstatic · checkable from the repoAI agents open the PR
StarSling agents run this exact audit on your workflows, apply the fix, and open a reviewable PR automatically.
Do this
actions/setup-node's cache input is the single biggest lever here: turn it on and the action wraps actions/cache around npm's local cache directory automatically, keyed on your lockfile, so npm ci restores previously-downloaded packages instead of refetching every tarball from the registry. That cache only covers npm's own package cache, though. A step like npx playwright install downloads browser binaries through a completely separate path, so it still refetches every run unless it is cached on its own key. And a job that runs an install step for a tool it never invokes, a unit-test job installing Playwright browsers it never launches, is pure waste layered on top: time spent downloading something no later step in that job touches. In one matrix of six vitest jobs that each installed Playwright browsers despite only running unit tests, removing the unused install step alone saved roughly 270 seconds of total runtime across the matrix.
name: CI
on: [pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run test:unit
# no Playwright install here: this job never launches a browser
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- run: npx playwright install --with-deps chromium
- run: npm run test:e2eAvoid this
name: CI
on: [pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install
- run: npx playwright install --with-deps chromium
- run: npm run test:unit
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install
- run: npx playwright install --with-deps chromium
- run: npm run test:e2eHow to detect it
Check every actions/setup-node step for a
cacheinput:grep -A5 'actions/setup-node' .github/workflows/*.yml- a step with nocache: npm(orcache: 'npm') restores nothing between runs.Search for browser or SDK installs that are never cached:
grep -n 'playwright install' .github/workflows/*.yml, then check whether anactions/cachestep with a matching path (for example~/.cache/ms-playwright) appears earlier in the same job.Search for tool installs that outlive their purpose: for each
playwright install(or similar) hit, check whether that same job later runsplaywright testor imports@playwright/test; if it only runsvitest,jest, or another unit-test runner, the install is unused.Search for
npm installwherenpm ciwould do:grep -rn 'npm install' .github/workflows/*.ymlin jobs that only need to reproduce an existingpackage-lock.json, not change it.
Tradeoffs and safety
actions/setup-node's cache input needs a lockfile to key on; a repository with no package-lock.json (or one that is gitignored) gets no cache and no error, so confirm the lockfile is committed before relying on it.
npm ci is not a drop-in swap for npm install: it deletes node_modules first and fails if package-lock.json is missing or out of sync with package.json. That failure is the point in CI (it catches an unlocked dependency change) but it will break a workflow that was relying on npm install to quietly reconcile a stale lockfile.
Keying the Playwright browser cache on package-lock.json works because the exact @playwright/test version is locked there; if a repository pins Playwright some other way (a Dockerfile ARG, an environment variable), key the cache on that value instead so an upgrade actually busts the cache.
Before deleting a tool-install step, confirm no other step in the same job depends on it indirectly, for example a global CLI installed once and reused by a later script. Removing an install that looks unused from the job's own test command can still break a step that shells out to it.
Turn on setup-node's own cache
actions/setup-node's cache input is not actions/cache configured for you behind the scenes for convenience; it is the single highest-impact change available here, because most npm-based workflows already call setup-node and are one line away from using it. Set cache to npm and the action hashes your lockfile, restores npm's package cache when that hash matches a prior run, and saves it again afterward, all without a separate cache step to write and maintain.
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npmCache downloads npm's own cache does not cover
npm's cache only holds packages it installs through the registry. A postinstall step or separate CLI call that fetches something else, browser binaries, a compiler toolchain, an SDK, is invisible to it and refetches on every run unless you cache that path yourself with actions/cache, keyed on whatever value determines the download's version (the lockfile, a Dockerfile ARG, an explicit version pin).
Skip installs a job never uses
The fastest install step is the one that does not run. A unit-test job that installs Playwright browsers it never launches, or a lint job that installs a database client it never connects to, pays the download cost of a tool with no later step to justify it. Audit each job's install steps against what that job's own commands actually invoke, and remove what does not match.
Verify it worked
Hand this prompt to your coding agent (Claude Code, Cursor, and the like) to run this audit and open the fix as a reviewable PR.
Audit this repository's npm install steps in GitHub Actions and speed them up.
1. grep .github/workflows/ for every actions/setup-node step. For each one that installs
npm dependencies afterward, check whether it sets `cache: npm` (or `cache: 'npm'`); if
not, add it, and confirm a committed package-lock.json exists for it to key on.
2. Where a job uses `npm install` only to reproduce an existing lockfile (not to change
it), switch it to `npm ci`. Do not make this change in a job whose purpose is updating
the lockfile itself.
3. Find tool-install steps (for example `npx playwright install`) and check two things per
job: (a) does this same job later run that tool, and if not, remove the install step;
(b) if it does run the tool, is the download cached with actions/cache keyed on the
lockfile hash or an equivalent version pin, and if not, add that cache step before the
install.
4. Read the actions/setup-node, npm ci, and Playwright CI caching docs linked on this page
before editing.
5. Show the full diff and open a pull request; do not apply changes blindly. In the PR
body, list each change with file and job name, and state how to verify: re-run the same
commit and confirm the setup-node and any tool-cache steps report a cache hit.Confirm the change landed
Re-run the workflow on an unchanged commit and check the setup-node step's log: it should read a cache restore (for example "Cache restored from key") instead of "Cache not found for input keys".
For the e2e job, re-run on the same commit and confirm the Playwright browser cache step reports a hit and the subsequent
playwright installstep completes without downloading browser binaries.Compare each job's total step duration before and after: the unit job should drop by however long the removed browser install took, and both jobs' install steps should shrink once the npm cache is warm.
Go further
One fix, all of them, or forever.
You have the prompt for this one practice. Here is how much further you can take it, each step doing more for you than the last.
Fix this one thing
Copy the prompt above
Hand OPT1 / OPT2 / OPT9 to your coding agent and fix it in your repo today.
Fix everything, once
Install the ci-speedup skill
One prompt audits your whole repo against all 73 ci-speedup patterns (these 3 plus 70 more) and hands your agent every fix at once. Open source, MIT, runs locally.
Keep it fixed, forever
Install the StarSling GitHub App
Connect GitHub and the fixes stay applied as your CI evolves, with agents that keep inspecting your workflows and opening optimization PRs you review.
Sources
1actions/setup-node: caching global packages data (opens in new tab)
2npm docs: npm ci (opens in new tab)
3GitHub Actions: caching dependencies to speed up workflows (opens in new tab)
Last updated 2026-08-21