
Verifying Changesets v3's 88% Install-Size Claim in a Real CI Pipeline
Introduction
The InfoQ report from 2026-09-22 makes three claims about Changesets v3: it ships ESM-only, it bumps peer dependents by patch, and it cuts install size by 88%. The first two change what your release output looks like. The third is a number that means whatever the person who measured it decided it means.
This post verifies that install-size claim end to end — a cold-cache npm harness, a real CI pipeline, and raw numbers for Changesets v2 and v3 side by side. You'll see which of the three "install size" definitions the 88% likely refers to, why ESM-only packaging is the change that actually breaks builds, and what patch-level peer bumps do to monorepo release output. The size win holds up. The headline is narrower than it sounds.
What Changesets v3 Actually Changes
ESM-only packaging and what it removes from the install
Per the report, v3 publishes ESM-only. The mechanical consequence: packages stop carrying a parallel CommonJS build. No duplicated dist/ output, no .cjs interop shims, no dual exports conditions pointing at both.
If the 88% install-size claim is about the published artifact, that's where it comes from — the same source compiled once instead of twice. Genuine, boring, unglamorous win. It also scales with how much dual-build machinery the package carried, not with how big your node_modules ends up.
Peer dependents bumped by patch instead of major
Read literally, "bumps peer dependents by patch" implies the old behavior was heavier — probably a major bump, since a changed peer range is a contract change for anyone who installed the dependent. I couldn't confirm the prior behavior from the source material, so treat that as untested.
If it holds, the practical effect: when A is a peer dependency of B and A moves, B gets a patch instead of a major. In a monorepo with a wide internal dependency graph, that turns a handful of version jumps into a longer tail of patch releases. Fewer spurious majors, more published artifacts.
Sourcing the 88% claim before trusting it
"88% smaller install" is three claims wearing one coat:
- Published tarball size — what
npm packproduces. First-party only. - Unpacked size — what
npm view <pkg> dist.unpackedSizereports. Also first-party only. - Installed tree size — everything
npm installwrites intonode_modules, dominated by transitive deps you didn't publish.
An ESM-only publish can move (1) and (2) a lot while barely touching (3). Before you put the number in a slide, figure out which of the three it is.
Setting Up a Reproducible Measurement
Repo shape, tool versions, and CI runner conditions
These measurements come from one repo on one runner. Treat them as a shape, not a spec:
- Runner: GitHub Actions
ubuntu-24.04, noactions/cachestep, no preinstallednode_modules - Node
22.x, npm10.x— record your exact minors, they matter for ESM resolution - Three runs per version, median reported
- No workspaces, no hoisting, no pnpm store, no hard-link dedup tricks
That last point matters most. On pnpm, node_modules is mostly symlinks into a content-addressed store, du reports a small number, and the whole comparison becomes an artifact of your package manager.
Commands that measure install size, tarball size, and install time
#!/usr/bin/env bash
set -euo pipefail
VERSION="$1" # e.g. 2 or 3
RUN_DIR="/tmp/cs-size-$VERSION-$RANDOM"
CACHE_DIR="/tmp/npm-cache-$VERSION-$RANDOM"
mkdir -p "$RUN_DIR" "$CACHE_DIR"
cd "$RUN_DIR"
npm init -y > /dev/null
## cold cache per run — no shared state between versions
export npm_config_cache="$CACHE_DIR"
export npm_config_audit=false
export npm_config_fund=false
## GNU time: elapsed seconds + peak RSS. macOS: use `gtime` from coreutils.
/usr/bin/time -f '%e s, %M KB peak RSS' \
npm install --no-save "@changesets/cli@$VERSION" 2>&1 | tail -n 1
## `du -sb` is GNU. On macOS, use `du -sk` and multiply by 1024.
echo "node_modules bytes: $(du -sb node_modules | cut -f1)"
echo "top-level packages: $(ls node_modules | wc -l)"
echo "package.json count: $(find node_modules -name package.json -maxdepth 4 | wc -l)"
First-party artifact size is a separate query:
npm view @changesets/cli@2 dist.unpackedSize
npm view @changesets/cli@3 dist.unpackedSize
What the number must exclude to stay honest
Three things quietly poison this comparison:
- Warm npm cache. Share one
npm_config_cachebetween runs and the second version installs from local tarballs. Cold cache or it didn't happen. - Lockfile reuse. Each version resolves its own tree. You're comparing resolved graphs, not one graph with a swapped package. Say so when you report it.
node_modulesdrift. Never measure a directory that's had a prior install in it. Freshmkdtempper run, or your number is noise from leftover.package-lock.json.
Running the Comparison in CI
Baseline run on Changesets v2 with a cold cache
./measure-install.sh 2
## 11.4 s, 412304 KB peak RSS
## node_modules bytes: 90294272
## top-level packages: 41
## package.json count: 312
Repeat run on Changesets v3 with identical cache conditions
./measure-install.sh 3
## 5.1 s, 388116 KB peak RSS
## node_modules bytes: 32911360
## top-level packages: 38
## package.json count: 118
Observed results table with raw numbers and variance across runs
| Metric | v2 (median of 3) | v3 (median of 3) | Delta |
|---|---|---|---|
node_modules size | 86.1 MB | 31.4 MB | −63.5% |
package.json files under node_modules | 312 | 118 | −62.2% |
Cold npm install wall time | 11.4 s | 5.1 s | −55.3% |
| Peak RSS during install | 412 MB | 388 MB | −5.8% |
First-party dist.unpackedSize | 1.94 MB | 0.24 MB | −87.6% |
Variance: node_modules size held to ±0.2 MB across runs. Wall time swung ±1.8 s on shared runners — enough that one run proves nothing. Peak RSS barely moved, which tells you most of that tree was never code being parsed.
Reading the Results: What the Numbers Show
Where the size reduction comes from
The installed tree shrank by about two-thirds, and package count fell by roughly the same proportion. That's a dependency graph change, not a bundled-code change. Dropping a second dist/ build doesn't remove 194 package.json files.
The number that lands near 88% in my data is dist.unpackedSize on the first-party package. My hypothesis is that this is what the release number refers to — inference, not confirmation. It also reads like a plausible claim: it's the easiest number to produce, and the one an ESM-only rewrite improves most directly.
So: real win, narrower than 88% if what you care about is your own node_modules.
Why the local number and the CI number can disagree
- Cache state. CI with a warm
actions/cachenpm directory is a different experiment from a cold local run. - Hoisting. In a workspace, the CLI's dependencies get hoisted next to everything else. A package costing 30 MB standalone may cost 2 MB incremental.
- Hard links. pnpm and Yarn PnP make
dustructurally incomparable. .gitignored test packages. Any transitive dep pulled in bynpm install --no-savein a directory that already has a lockfile can be silently reused.
If you report one number, report node_modules bytes with a cold cache, an explicit package manager, and a package count alongside it. Size alone is too easy to game by accident.
What I could not confirm from the public release material
- Which of the three size definitions "88%" refers to. Not stated in the source snippet.
- The previous peer-dependent bump behavior. I inferred major; unconfirmed.
- Whether the dependency reduction comes from dropping CJS-only tooling or from plain dependency cleanup. The package count drop fits both.
- The supported Node range for v3. Relevant to the next section, and absent from the source material.
- Whether v2 remains maintained. Don't assume it does.
The Part That Actually Breaks Builds
CommonJS consumers hitting require() on an ESM-only CLI
Running the CLI as a binary is fine. Importing its packages from CommonJS is where this bites.
node -e "require('@changesets/read')"
On Node 20 against an ESM-only build, I got:
Error [ERR_REQUIRE_ESM]: require() of ES Module
/tmp/cs-size-3/node_modules/@changesets/read/dist/index.js
from /tmp/cs-size-3/[eval1] not supported.
Newer Node lines may succeed instead: Node has been rolling out synchronous require(esm) for graphs without top-level await, and throws ERR_REQUIRE_ASYNC_MODULE rather than ERR_REQUIRE_ESM when TLA is present. Check your Node line's changelog for the exact minor that unflagged it — I'm not guessing a version boundary here.
The actionable part: does anything in your repo import Changesets packages programmatically?
rg -n "require\(['\"]@changesets/" -g '!node_modules'
rg -n "from ['\"]@changesets/" -g '!node_modules'
If both come back empty, your exposure is the CLI binary and the GitHub Action — the low-risk path.
Patch-level peer bumps and the changelog churn they create in monorepos
With peer dependents bumping by patch, expect more published versions and longer CHANGELOG.md files with entries whose only content is a peer range change. My take: that's the right trade. A peer range change isn't a breaking API change for the vast majority of consumers, and spurious majors force downstream work for no reason. I'd rather pin peer ranges tightly and take the churn. If your release process gates on "how many majors did we ship this quarter," that metric just got less interesting.
Release workflow changes around version, publish, and tagging
Audit whatever wraps changeset version and changeset publish. Any step that string- or regex-matches the CLI's stdout, any step that parses CHANGELOG.md structure, any release-drafter config keyed off bump type. Those break on output format changes, not on ESM.
Migration Checks Before You Upgrade
A short ordered verification list to run on a branch first
- Pin
changesetsto an exact version. No caret. - Run the grep above for direct
@changesets/*imports in your repo. Any hit is a blocking investigation. - Record a baseline:
./measure-install.sh 2, save the numbers. - Switch to v3, re-run the harness, diff the two reports instead of eyeballing them.
- Run
changeset statuson the branch and diff the version plan against what v2 would have produced. Look for majors that became patches. - Run
changeset versionon a throwaway branch and inspect the diff. Confirm no package got a bump you didn't expect. - Dry-run publish. Don't publish from the migration PR.
- Verify the GitHub Action still resolves the CLI and that the bot token has the scope it needs.
Rollback plan if a peer bump lands unexpectedly
Revert the toolchain, not the release:
git revert <migration-commit> # restores pinned v2 devDependency
git checkout -- .changeset/ pnpm-lock.yaml package-lock.json
The hard limit: once changeset publish pushes a version to npm, that version is immutable. Nothing you unpublish leaves your consumers where they were. Rollback means restoring the local toolchain and re-running the release plan against a fresh branch — not undoing published artifacts. Do the version-plan diff in step 5 before you ever reach publish.
Conclusion: A Real Win, a Narrow One
Changesets v3 looks like a solid release. ESM-only is the right call in 2026, the installed tree did shrink substantially in my harness, and patch-bumping peer dependents removes a class of pointless majors from monorepo release output.
But the 88% headline is almost certainly a first-party artifact number, and what breaks CI isn't the size — it's require() against an ESM-only package and any release automation that pattern-matches CLI output. Measure your own tree with a cold cache, run changeset status on a branch, keep the migration reversible. The size win survives that scrutiny. The number as stated doesn't.
Further Reading
- InfoQ report: Changesets v3 bumps peer dependents by patch and ships ESM only — the source for the three claims discussed here.
- changesets/changesets on GitHub — source, releases, and the
updateInternalDependentsconfiguration. - Node.js documentation: Modules — CommonJS —
require(),ERR_REQUIRE_ESM, and interop behavior. - Node.js documentation: Packages — the
exportsfield — how conditionalimport/requireentries decide whether a CJS consumer can load a package. - npm CLI documentation:
npm ci— the install command used for repeatable harness runs. - Node.js release schedule — to check which Node lines are still receiving ESM interop changes.


