import { Vector2d } from "melonjs" pulls 184 kB gzip. The class itself is
about 1 kB.
Measured at 20.2.0 against a real installed package — node_modules/melonjs
containing the published build/ and the real package.json (so sideEffects
is honoured), bundled with esbuild --bundle --minify --format=esm:
| imported |
gzip |
{ Vector2d } |
183.9 kB |
{ Application } |
230.5 kB |
* as me (everything) |
246.1 kB |
So tree-shaking recovers about 25% at best. For a real game it recovers almost
nothing: the platformer example's actual 31 imports come to 234 kB against
246 kB for the whole engine, a saving of ~5%.
It is structural, not intrinsic
Bundling the same import from the source module graph instead of the published
artifact:
| entry |
gzip |
{ Vector2d } from src/math/vector2d.ts |
1.1 kB |
{ Vector2d } from src/index.ts (barrel) |
133 kB |
{ Vector2d } from the published build/index.js |
184 kB |
Same class, three orders of magnitude apart depending on the path in. An esbuild
metafile for the barrel case shows 258 of 258 modules retained — howler
(35 kB), the glTF parser, the Canvas renderer, even loader/melonjs_logo.png.
Nothing is dropped.
Two contributing factors, as far as I got:
- The package ships as a single pre-bundled file.
files is
["build", "package.json", "README.md", "CHANGELOG"], and everything under
build/ other than index.js is .d.ts. So a consumer's bundler is handed
one 2.1 MB module and has to shake within it, rather than across a module
graph where whole files can be dropped.
- The barrel re-exports everything. Touching any one export brings
index.ts in, and from there the entire graph.
Worth noting what is not the cause, to save anyone re-checking: sideEffects
is declared in the permissive array form, there are no import cycles back
through the barrel (0 modules import it), a bare import "melonjs" with nothing
used shakes to 0 kB, and the polyfill side-effect import is only 8 kB.
Separately: the sideEffects globs point at paths that are not published
"files": ["build", "package.json", "README.md", "CHANGELOG"],
"sideEffects": ["./src/polyfill/**"]
src/ is not in the tarball (confirmed with npm pack --dry-run), so the glob
matches nothing for consumers. The array form means everything else is treated
as side-effect-free, so this is permissive rather than harmful — but it does not
express what it looks like it expresses, and if the intent was to protect the
polyfill's side effects then that protection is not there.
Possible directions
Not prescriptive, and each has a cost worth weighing:
- Emit a preserved module graph (per-module output) in addition to or instead of
the single bundle, so bundlers can drop whole files.
- Add subpath exports (
melonjs/math, melonjs/physics, …) so consumers who
want a slice can ask for it directly. src/math/vector2d.ts is already 1.1 kB
standalone, so the granularity exists.
- Point the
sideEffects globs at what actually ships.
Why it matters now
The README used to claim the engine was tree-shakeable "so you only pay for what
you use". That claim was removed in #1614 because these numbers do not support
it. If this is fixed, it can go back.
import { Vector2d } from "melonjs"pulls 184 kB gzip. The class itself isabout 1 kB.
Measured at 20.2.0 against a real installed package —
node_modules/melonjscontaining the published
build/and the realpackage.json(sosideEffectsis honoured), bundled with esbuild
--bundle --minify --format=esm:{ Vector2d }{ Application }* as me(everything)So tree-shaking recovers about 25% at best. For a real game it recovers almost
nothing: the platformer example's actual 31 imports come to 234 kB against
246 kB for the whole engine, a saving of ~5%.
It is structural, not intrinsic
Bundling the same import from the source module graph instead of the published
artifact:
{ Vector2d }fromsrc/math/vector2d.ts{ Vector2d }fromsrc/index.ts(barrel){ Vector2d }from the publishedbuild/index.jsSame class, three orders of magnitude apart depending on the path in. An esbuild
metafile for the barrel case shows 258 of 258 modules retained — howler
(35 kB), the glTF parser, the Canvas renderer, even
loader/melonjs_logo.png.Nothing is dropped.
Two contributing factors, as far as I got:
filesis["build", "package.json", "README.md", "CHANGELOG"], and everything underbuild/other thanindex.jsis.d.ts. So a consumer's bundler is handedone 2.1 MB module and has to shake within it, rather than across a module
graph where whole files can be dropped.
index.tsin, and from there the entire graph.Worth noting what is not the cause, to save anyone re-checking:
sideEffectsis declared in the permissive array form, there are no import cycles back
through the barrel (0 modules import it), a bare
import "melonjs"with nothingused shakes to 0 kB, and the polyfill side-effect import is only 8 kB.
Separately: the
sideEffectsglobs point at paths that are not publishedsrc/is not in the tarball (confirmed withnpm pack --dry-run), so the globmatches nothing for consumers. The array form means everything else is treated
as side-effect-free, so this is permissive rather than harmful — but it does not
express what it looks like it expresses, and if the intent was to protect the
polyfill's side effects then that protection is not there.
Possible directions
Not prescriptive, and each has a cost worth weighing:
the single bundle, so bundlers can drop whole files.
melonjs/math,melonjs/physics, …) so consumers whowant a slice can ask for it directly.
src/math/vector2d.tsis already 1.1 kBstandalone, so the granularity exists.
sideEffectsglobs at what actually ships.Why it matters now
The README used to claim the engine was tree-shakeable "so you only pay for what
you use". That claim was removed in #1614 because these numbers do not support
it. If this is fixed, it can go back.