Published on

Speed Up CI with cached NPM Installs

Authors

Shaving a minute off your build time saves more than the couple pennies charged by your CI provider. It means critical hotfixes land sooner. It also means developers are less likely to get distracted while waiting, start browsing the web, and forget they had a build happening in the first place. Recently, I had the task of reducing our build times down from 45 minutes. I got it down to under 5. One of the biggest offenders was installing node dependencies, so I started there.

The slow code

- name: Cache node modules
  id: cache-npm
  uses: actions/cache@v3
  env:
    cache-name: cache-node-modules
  with:
    # npm cache files are stored in `~/.npm` on Linux/macOS
    path: ~/.npm
    key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-build-${{ env.cache-name }}-
      ${{ runner.os }}-build-
      ${{ runner.os }}-

Above we see the code that is recommended by GitHub. It caches the .npm directory (or .yarn if you prefer) that is stored in the home directory. This works fine to cache remote fetches, but it has a downfall-- it still has to perform an install on each dependency. That adds up, especially if your project includes a lot of dependencies with .node binaries that have to get compiled.

The Fast Code

- name: Get cached node modules
  id: cache
  uses: actions/cache@v3
  with:
    path: |
      **/node_modules
    key: node_modules-${{ runner.arch }}-${{ env.NODE_VERSION }}-${{ hashFiles('yarn.lock') }}
```

To improve upon this, we cache all the `node_modules` directories _after_ the build took place.
That means no installation necessary!
So what's the catch? Builds are now specific to the architecture and node version.
To allieviate this problem, we simply include those variables as the cache key.
Just like that, builds are now much faster.