Minimal reproduction demonstrating the bundle-size regression introduced
by the Svelte v4 → v5 upgrade when using the package only as a reactive
store library, and the recovery from adding a "sideEffects" field to
Svelte's package.json. The patch is upstream candidate work — see the
motivation section below.
Three packages bundle the exact same 12-line src/index.js (imports
writable / readable / derived / get from svelte/store and uses
each) with webpack + SWC. The only thing that varies is which Svelte
install they pull in:
| package | Svelte version |
|---|---|
test-svelte-v4 |
4.2.20 (pre-regression baseline) |
test-svelte-v5 |
5.55.4 (stock) |
test-svelte-v5-patched |
5.55.4 + sideEffects patch |
| metric | svelte v4 (baseline) | svelte v5 | v5 patched |
|---|---|---|---|
| tree-shaken | 113,590 B | 273,632 B (+140.9%) | 35,913 B (−68.4%) |
| minified | 2,000 B | 7,726 B (+286.3%) | 1,490 B (−25.5%) |
Upgrading from Svelte 4 to Svelte 5 adds 5.7 KB minified to this
bundle — because v5 rebuilt its stores on top of the signals runtime,
which webpack can't fully tree-shake without a sideEffects
declaration. Applying the patch eliminates that regression and lands
510 B below the v4 baseline.
pnpm test
That script installs and builds all three packages, then prints the
comparison above. Each package owns its own pnpm lockfile, so pnpm install stays scoped to that package's deps — the test-svelte-v5-patched
pnpm patch only touches the svelte install under that directory.
playground-svelte-bundle-size/
├── package.json # root: install:all / build:all / test
├── test.mjs # builds all three and prints sizes
└── packages/
├── test-svelte-v4/ # Svelte 4.2.20
│ ├── package.json
│ ├── webpack.config.js
│ └── src/index.js
├── test-svelte-v5/ # Svelte 5.55.4 (stock)
│ ├── package.json
│ ├── webpack.config.js
│ └── src/index.js
└── test-svelte-v5-patched/ # Svelte 5.55.4 + pnpm patch
├── package.json # ← declares pnpm.patchedDependencies
├── patches/[email protected]
├── webpack.config.js
└── src/index.js # byte-identical to the others
All three src/index.js files are byte-identical. All three
webpack.config.js files are byte-identical and use SWC as the
minimizer (matching the real production app whose numbers these line up
with).
packages/test-svelte-v5-patched/patches/[email protected]:
--- a/package.json
+++ b/package.json
@@ -19,6 +19,12 @@
],
"module": "src/index-client.js",
"main": "src/index-client.js",
+ "sideEffects": [
+ "./src/internal/disclose-version.js",
+ "./src/internal/flags/legacy.js",
+ "./src/internal/flags/async.js",
+ "./src/internal/flags/tracing.js"
+ ],
"exports": {
This tells bundlers: every file in Svelte is side-effect-free except
these four flag-setting modules. The four exceptions mutate globals at
import time (disclose-version.js writes window.__svelte; the flag
files toggle runtime modes), so they must be preserved even when no one
imports their named exports.
Svelte's own CI already runs
check-treeshakeability.js
before each publish. It walks package.json#exports, bundles
import "svelte/<entry>" through Rollup, and fails if the result isn't
empty. The upstream contract matches the sideEffects declaration
exactly — the patch just tells webpack what Rollup's CI has been
enforcing all along.
src/index.js mirrors how a private repo uses Svelte: only as a
reactive store library, called from non-Svelte code. Zero .svelte
files. It imports exactly what the real codebase imports from
svelte/store (writable, readable, derived, get) and exercises
each one. The savings come from the patch interacting with webpack and
SWC, not from how elaborate the app code is — a 12-line demo shows the
same delta as a multi-module version.
The biggest factor in reproducing the 6 KB minified savings was matching
the real project's minifier. webpack's default minifier is terser,
which is extremely aggressive at cross-module dead-code elimination — in
a small self-contained bundle, terser can eliminate almost everything
that sideEffects would have eliminated anyway, leaving only ~440 B of
visible savings.
The private repo uses SWC via TerserPlugin.swcMinify. SWC is
faster but less aggressive at DCE across module boundaries. More
unreachable code survives into the final bundle, which is exactly the
code that sideEffects lets webpack remove earlier in the pipeline.
This demo's webpack.config.js uses the same SWC minifier, so the
numbers you see here line up with the real-repo numbers.
A production application's webpack build shows the same pattern, in a
larger chunk that bundles svelte/store:
| build | chunk size | vs v4 baseline |
|---|---|---|
| Svelte v4 (baseline) | 38,255 B | — |
| Svelte v5, no fix | 43,887 B | +5,632 B |
| Svelte v5, fixed | 37,858 B | −397 B |
Both the ~5.6 KB regression and the recovery-below-baseline match this demo's numbers closely — evidence that the mechanism is general, not an artifact of this particular reproduction.
This patch could go directly into
sveltejs/svelte at
packages/svelte/package.json. It's a ~5-line change with:
check-treeshakeability.js) that already
enforces the invariant it declares.sideEffects array form.Existing issue for context: sveltejs/svelte#13855.