# compressed-size-action puts a compressed size table on every pull request

> Preact's GitHub Action builds your PR and the target branch, compares the compressed size of the built files and posts the difference as a pull request comment, without uploading artefacts anywhere. The tricky parts are fork permissions and hashed filenames.

**preactjs/compressed-size-action** — GitHub Action that adds compressed size changes to your PRs.

- Repository: https://github.com/preactjs/compressed-size-action
- Website: https://github.com/marketplace/actions/compressed-size-action
- Stars: 653 · Forks: 108
- Language: JavaScript
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/preactjs-compressed-size-action

## Two builds, one comparison table

compressed-size-action is a GitHub action that reports changes in compressed file sizes on your pull requests. The mechanism is a double build. It builds your PR, then builds the target and compares between the two, which means the comparison is made against real build output on both sides rather than against a checked-in size field. The report arrives as a markdown comment on the pull request, so it lands where the review already happens. Two properties of the design are worth naming because they are unusual. The action doesn't upload anything or rely on centralized storage, so no artefacts are sent to a third party service and there is no service account to configure. And the whole thing is a JavaScript action, with the repository containing action.yml, index.js, src/ and tests/, built with microbundle and tested with jest.

## The package manager is chosen from your lockfiles

By default, the action installs dependencies according to which lockfiles are present, if any. It automatically uses yarn, pnpm, bun, deno, or npm ci when the relevant lockfile is in the repository, so a multi-package repository does not need to tell the action which tool it uses. When the default is wrong, there is an install-script option that takes a custom script instead. The documented example uses npm ci with the workspace option to install only one package of a monorepo:

```diff
    steps:
    - uses: actions/checkout@v2
    - uses: preactjs/compressed-size-action@v2
      with:
+       install-script: "npm ci --workspace=packages/my-subpackage"
```

The build itself follows the same logic. The action tries to build the PR by running the build npm script from package.json, and if you need a different script you set a build-script option, with ci given as the example. Between the two steps there is room for two more hooks. A postinstall npm script runs after dependencies are installed but before the build, which is the documented slot for a Lerna bootstrap in a monorepo, and a prebuild npm script runs after postinstall but before build.

## Fork pull requests get stdout instead of a comment

There is one permission constraint that shapes how you deploy this. Due to GitHub's permission model, the action cannot safely create comments when it is triggered by a pull request from a fork. It will, however, still generate the size comparison and print the comment it would have posted to the stdout of the action, which allows manual checking and lets a maintainer copy and paste it into a comment. The workaround is built into the design. The markdown comment that the action posts is also exposed as the comment-body output, which later workflow steps can use to archive the report, pass it to another job, or post the comment from a separate workflow with the permissions you need:

```yaml
- id: compressed-size
  uses: preactjs/compressed-size-action@v2

- name: Save compressed size comment
  env:
    COMMENT_BODY: ${{ steps.compressed-size.outputs.comment-body }}
  run: printf '%s\n' "$COMMENT_BODY" > compressed-size-comment.md
```

If your repository accepts outside contributions, you will want that second workflow, and it is worth deciding where the report is stored, since the action itself keeps nothing.

## pattern and exclude decide what is measured

By default, the action tracks the size of all JavaScript files within dist directories, anywhere in the repository and not just at the root. The list of files can be changed with the pattern and exclude options, both of which are minimatch patterns, and the defaults are a pattern that matches any JavaScript file inside a dist directory, and an exclusion list that always ignores SourceMaps and node_modules.

The collection rule is that files are found by matching pattern, and then any of those that match exclude are ignored, which is why most projects do not need to modify exclude. The override example in the documentation shows a project whose output lives elsewhere, pointing pattern at a build-output folder for JavaScript, CSS, HTML and JSON and adding a manifest.json path to the exclusion list.

Note what the default leaves out. If your package ships CSS, HTML or JSON as the main payload, the default measurement will not see it, and you have to widen the pattern before the report means anything.

## strip-hash makes hashed filenames comparable

A bundler that emits content hashed filenames will otherwise produce a table where every file appears to be new on every pull request. The strip-hash option, added in v2, takes a custom regular expression pattern that is used to remove hashes from filenames, and the un-hashed filenames are used both for the size comparison and for display. The characters matched by the regex are removed by default, so a filename of foo.abcde.js is converted to foo.js with this pattern:

```yaml
strip-hash: "\\b\\w{5}\\."
```

It can go further with parens, which create submatches that mark where a hash occurs and are replaced with asterisks. That is useful when a mix of hashed and unhashed filenames is present, so a filename of foo.abcde.chunk.js is displayed with the hash replaced by asterisks when the pattern anchors on the chunk suffix:

```yaml
strip-hash: "\\.(\\w{5})\\.chunk\\.js$"
```

If you skip this on a project with hashed output, the size comparison stops being a comparison, since every entry looks added and removed.

## Ordering, cleanup and the rest of the workflow options

The results table is sorted by filename in ascending order by default, and the sort-by option changes that using a column:direction format, where the column is one of Filename, Size or Change and the direction is asc or desc, so Size:desc sorts by file size descending. The cleanup option is the one that matters in unusual repositories. For repositories or custom monorepo setups where files are modified in ways that are not reset by npm ci and npm run build, it may be necessary to define a custom clean script. That script resets any file modifications after the upstream, meaning the target, build ends and your PR code, meaning HEAD, is checked out, but before dependencies for HEAD are installed:

```diff
    - uses: preactjs/compressed-size-action@v2
      with:
        repo-token: "${{ secrets.GITHUB_TOKEN }}"
+       clean-script: "clean"
```

The paired package.json example shows what such a script looks like, deleting an inner node_modules directory between the two builds. Two workflows also accept a repo-token, which is what the action uses when it needs to talk to GitHub, and the whole configuration surface is small enough to read in one sitting: install-script, build-script, clean-script, pattern, exclude, sort-by and strip-hash.

## Where this action is the wrong tool

Three limits define the boundary. The first is reproducibility. The action builds the target and your PR and compares them, so if your build embeds timestamps, hashes of the environment or anything else that differs between two checkouts of different commits, the report will show noise that is not a real size change. The second is coverage. The default measurement is JavaScript files inside dist directories, so an application that ships images, fonts or a large JSON payload gets no signal unless you rewrite the pattern, and the action reports compressed size rather than uncompressed size, which is a deliberate proxy but not the number users experience on a slow connection. The third is the fork limitation, which turns the most valuable output, an automatic comment on every pull request, into manual work on exactly the contributions from strangers you would most want to see sized. An alternative worth considering is to keep the action for the measurement and post the comment yourself from a separate workflow using the comment-body output, which the documentation explicitly supports, so the comparison runs on every pull request while the reporting happens with the permissions your repository policy allows.

## Conclusion

Adopt compressed-size-action if you publish a JavaScript bundle and want reviewers to see the size consequence of a pull request before merging, since it reports on the built output rather than on source changes. Do not adopt it if your build is not reproducible between two checkouts, or if your assets are not JavaScript files under dist directories, since the default pattern tracks only those. Before you enable it, decide how you will handle pull requests from forks, because the action cannot comment there and only prints the report to stdout, and set strip-hash if your build emits hashed filenames, otherwise every file looks new on every pull request.

## FAQ

### How do I add compressed-size-action to a pull request workflow?

Add a workflow file that runs on pull_request with a build job on ubuntu-latest, and use the action after a checkout step. The documented example uses actions/checkout@v2 followed by preactjs/compressed-size-action@v2.

### Why does compressed-size-action not comment on pull requests from forks?

Because of GitHub's permission model, the action cannot safely create comments when triggered by a pull request from a fork. It still generates the size comparison and prints the comment to the stdout of the action so it can be checked and copied manually.

### How do I control which files compressed-size-action measures?

Use the pattern and exclude options, which are minimatch patterns. The defaults track any JavaScript files within dist directories and always ignore SourceMaps and node_modules, and files are collected by matching pattern before any matching exclude is ignored.

### What does the strip-hash option do in compressed-size-action?

It takes a regular expression whose matches are removed from filenames, and the un-hashed names are used for both the size comparison and the display. Parens create submatches that mark a hash and are replaced with asterisks, so foo.abcde.chunk.js can be shown as foo.*****.chunk.js.

## Sources

- [License: MIT](https://github.com/preactjs/compressed-size-action/blob/master/LICENSE)
- [preactjs/compressed-size-action on GitHub](https://github.com/preactjs/compressed-size-action)
- [Project website](https://github.com/marketplace/actions/compressed-size-action)
- [README](https://github.com/preactjs/compressed-size-action/blob/master/README.md)
- [Releases](https://github.com/preactjs/compressed-size-action/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/preactjs-compressed-size-action
