Get started with 33% off your first certification using code: 33OFFNEW

Caching `npm i` on GitHub Actions for faster build times

4 min read
Published on 16th May 2023

GitHub Actions is a CI/CD pipeline by GitHub. It is built into the repository, and allows you to define a set of actions to perform on certain triggers. These actions include things like automatic merging of pull requests, automatic code format fixing, linting, asset generation and even deployment.

GitHub Actions works in a similar way to most CI/CD solutions; it spawns a virtual machine, and does whatever you ask it to, which is defined as a series of sequential tasks within a YAML file.

GitHub Actions can be slow

This isn't GitHub's fault, but many build times are measured in minutes long. Billing for GitHub Actions is measured in minutes, so keeping the build time to a minimum will save money as well as frustrations.

Obviously it depends on what your Action is doing as to why it is slow, but one of the main factors in a slow running GitHub Action is npm install (or npm ci) which installs all dependencies within your JavaScript application.

In our example Laravel application npm install took around 2 minutes on GitHub Actions, which is slow compared to a dev machine which does it in around 10 seconds.

So how do we speed it up? We cache npm install.

How to cache npm install

GitHub Actions is stateless. Every run is done from fresh and at the end of the run your virtual machine is destroyed. This means that caching could be problematic.

GitHub Action have a suite of prebuilt 'actions' which are available to install against your own Action. One such action is called actions/cache which allows you to cache data between Action runs.

In our case we'll be building our node_modules folder and caching it for use in later runs. You'll probably want to invalidate this from time to time.

Caching npm install on GitHub Actions: A Step-by-Step Guide

To cache npm install on GitHub Actions, follow these steps:

Set up your GitHub Actions workflow

Create a new GitHub Actions workflow or modify an existing one by adding a .github/workflows/main.yml file in your repository.

Here's a simple example of a GitHub Actions workflow:

name: Node.js CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout repository
      uses: actions/checkout@v2

This workflow is triggered whenever there's a push or pull request to the main branch. It runs on an ubuntu-latest environment and checks out the repository using the actions/checkout action. You'll want to modify this as appropriate and keep it as close to your production environment as possible.

2. Add Node.js to your workflow

Next, you need to add Node.js to your GitHub Actions environment. Use the actions/setup-node action to configure the desired Node.js version:

    - name: Set up Node.js
      uses: actions/setup-node@v2
      with:
        node-version: 14

Again, you'll want to mimic your production environment with the version of node you're running.

3. Cache the npm dependencies

To cache the npm dependencies, use the actions/cache action. First, add the following step to your workflow:

    - name: Cache npm dependencies
      uses: actions/cache@v2
      with:
        path: ~/.npm
        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
        restore-keys: |
          ${{ runner.os }}-node-

This step configures the cache for the npm dependencies. The path is set to ~/.npm, which is where npm stores its cache on the GitHub Actions runner. The key is generated based on the current operating system and a hash of the package-lock.json file. This ensures that the cache is only used if the package-lock.json file hasn't changed. The restore-keys option allows for partial cache matches if an exact match isn't found.

4. Run npm install

After configuring the cache, add a step to run npm install:

    - name: Install dependencies
      run: npm ci

In this example, we use npm ci instead of npm install. The npm ci command is recommended for CI/CD environments because it provides a more deterministic and faster installation process. It ensures that the installed dependencies match the package-lock.json file exactly and removes the node_modules folder before installation, ensuring a clean state.

5. Add any additional steps

Now that you've configured caching for npm install, you can add any additional steps to your workflow, such as running tests or deploying your application.

Here's the complete example of a GitHub Actions workflow that caches npm install:

name: Node.js CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout repository
      uses: actions/checkout@v2

    - name: Set up Node.js
      uses: actions/setup-node@v2
      with:
        node-version: 14

    - name: Cache npm dependencies
      uses: actions/cache@v2
      with:
        path: ~/.npm
        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
        restore-keys: |
          ${{ runner.os }}-node-

    - name: Install dependencies
      run: npm ci

    # Add any additional steps, such as running tests or deploying your application.

Caching npm install on GitHub Actions can significantly improve your CI/CD workflow by reducing build times and saving resources. By following this guide and using the actions/cache action, you can easily configure caching for your npm dependencies and optimize your builds.