← Main

Speeding up Prettier locally and on your CI with dprint

by David Sherret

Prettier is a JavaScript-based code formatter with support for many languages.

dprint is a Rust-based code formatting platform that formats code with formatting plugins including a Prettier plugin.

Overview

Many developers often run a step to verify their code has been formatted with an automated code formatter like Prettier.

In this post, we'll speed up Prettier's format checking of a TypeScript codebase from ~40s to under 1s on the second run.

Previous Prettier setup

Say we have a Node-based repo with about 520 TypScript files in the src folder for a total size of 16MB. This repo has a package.json with Prettier installed that verifies formatting. It looks similar to the following:

// package.json
{
  // etc...
  "scripts": {
    "fmt-check": "prettier --check --end-of-line lf \"src/**/*.ts\""
  },
  "devDependencies": {
    "prettier": "^2.6.2"
  }
}

Verification is run by executing the fmt-check script:

npm run fmt-check

Our GitHub actions CI executes the following steps:

# .github/workflows/ci.yml
steps:
  # check out the repository
- uses: actions/checkout@v3
  # setup node.js
- uses: actions/setup-node@v3
  with:
    node-version: '16'
  # cache the node_modules folder
- name: Cache
  uses: actions/cache@v3
  with:
    path: |
      ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
  # install the packages in the package.json
- run: npm ci
  # verify formatting
- name: prettier
  run: npm run fmt-check

When running this on our CI, the "prettier" step takes about 41s.

Prettier's workflow step taking 41s to run.

Switching to dprint

We can speed this up by using dprint as our code formatting CLI with the dprint-plugin-prettier plugin.

CLI setup

To begin, run the following commands in the root directory of the project:

# install dprint as a dev dependency
npm install --save-dev dprint
# initialize a dprint.json file
npx dprint init # note: uncheck all the default plugins and press enter
# add the specified GitHub repo to configuration file we just created
npx dprint config add dprint/dprint-plugin-prettier

A dprint.json file will have been created with the Prettier plugin specified:

// dprint.json
{
  "includes": ["**/*.*"],
  "excludes": [],
  "plugins": [
    "https://plugins.dprint.dev/prettier-0.7.0.json@4e846f43b32981258cef5095b3d732522947592e090ef52333801f9d6e8adb33"
  ]
}

For this example, update the includes glob to only have the src directory and match the extensions Prettier supports that you use in your project. In this case, I'm only interested in formatting ts files, but in your case you may specify any of the file extensions Prettier supports (ex. src/**/*.{ts,js,html,css}).

// dprint.json
{
  "includes": ["src/**/*.ts"],
  "excludes": [],
  "prettier": {
    // optionally, add prettier config here
  },
  // etc...
}

Now remove prettier from your package.json and update the fmt-check script to use the dprint check command instead:

// package.json
{
  // etc...
  "scripts": {
    "fmt-check": "dprint check"
  },
  "devDependencies": {
    "dprint": "^0.25.1"
  }
}

Try running npm run fmt-check locally a couple times. The first time will be faster than Prettier because it formats files on multiple threads using Prettier snapshotted in a Deno runtime (via deno_core). The second run should complete almost instantaneously because dprint will skip files it already knows are formatted based on past runs.

CI setup

Although the above changes will be faster than Prettier as-is in the CI, we want to take advantage of dprint's incremental formatting and save its cache.

To do this, we need to update our GitHub actions file to cache the ~/.cache/dprint folder and also use dprint.json as a cache key in order to bust the cache whenever we update the dprint.json file:

# .github/workflows/ci.yml
steps:
  # check out the repository
- uses: actions/checkout@v3
  # setup node.js
- uses: actions/setup-node@v3
  with:
    node-version: '16'
  # cache the node_modules folder
- name: Cache
  uses: actions/cache@v3
  with:
    path: |
      ~/.npm
      ~/.cache/dprint
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json', '**/dprint.json') }}
  # install the packages in the package.json
- run: npm ci
  # verify formatting
- name: dprint w/ dprint-plugin-prettier
  run: npm run fmt-check

Testing it out

On first run, we can see dprint takes 35s to execute:

Dprint's workflow step taking 35s to execute.

This is only slightly better than Prettier because our GitHub action runner only has 2 cores. It's still not great. Looking at the "Post Cache" step output, we can see our cache was saved:

Shows the "Post Cache" workflow step uploading the cache to be downloaded for next time.

Let's test running our CI again by pushing a message only commit:

git commit --allow-empty --only -m "Test out incremental formatting."
git push

Watching the GitHub action workflow run, we can see the cache being loaded:

Shows the "Cache" workflow step downloading the cache we saved from the last workflow run.

...and the formatting verification step now takes under 1 second to run:

Dprint's workflow step now showing as taking 0s to run.

Final notes

We've seen how we can use dprint's CLI to speed up formatting with Prettier. For an example repo, see faster_prettier_example.

This is great for projects already using Prettier that don't want to switch to a new formatter, but the initial format without an incremental cache was still kind of slow. We could speed it up by using dprint's TypeScript and JavaScript Rust-based code formatter instead (see dprint-plugin-typescript) with the caveat that it will format code slightly differently that Prettier (but it has a lot of configuration settings you could tune to try to get it close).

In this scenario, on the CI it takes 10s to run the first time before the cache gets it under 1s:

dprint-plugin-typescript's workflow step taking 10s to run the first time.

Thanks for reading!