Reduce SchemaBinary bundle size - #7377
Merged
tim-smart merged 1 commit intoAug 20, 2026
Merged
Conversation
`EMPTY_READER_VIEW` was built from `EMPTY_READER_BUFFER.buffer`. A member access in argument position is not effect-free to a bundler, so the `@__PURE__` annotation the build adds could not be honoured and the pair survived tree shaking in every consumer that imports `effect/unstable/encoding` without ever touching this module. Both placeholders now read one named `ArrayBuffer`, which keeps `EMPTY_READER_VIEW.buffer === EMPTY_READER_BUFFER.buffer` exactly as before. Add a tracked bundle fixture so the module's size is compared on every PR by the existing Bundle job. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bundle-size pass over
effect/unstable/encoding/SchemaBinary, on top of merged #7371.Closes EFF-784
Result
One byte-level defect, and evidence that the module itself is already close to its practical minimum.
EMPTY_READER_VIEWwas built asnew DataView(EMPTY_READER_BUFFER.buffer). The build annotates that with@__PURE__, but a member access in argument position is not effect-free to a bundler, so Rollup could not honour the annotation and kept the pair alive. The consequence was module-scoped: anyone importingeffect/unstable/encodingforNdjson,Msgpack,Tomlor anything else shipped a fragment ofSchemaBinaryeven though they never referenced it.Both placeholders now read one named
ArrayBuffer, soEMPTY_READER_VIEW.buffer === EMPTY_READER_BUFFER.bufferholds exactly as before andReader.reset's buffer-identity check is unchanged.Verified by bundling with
preserveModules: the retained chunk in a barrel consumer went fromto nothing at all.
SchemaBinarynow contributes exactly zero bytes to a barrel consumer that does not use it.The trade is honest in both directions and small: a barrel consumer that does not use the module saves 31 minified / 19 gzip bytes, and a consumer that does use it pays 16 minified / 7 gzip bytes for the extra
const.The second change is a tracked fixture,
packages/tools/bundle/fixtures/schema-binary.ts, so the module's size is compared on every PR by the existing Bundle job. It deep-importseffect/unstable/encoding/SchemaBinaryrather than the barrel on purpose: through the barrel roughly a quarter of the bundle is msgpackr, and the fixture would track that instead of this module.Measurement method
Rollup with the repository's own plugin pipeline (
packages/tools/bundle/src/Plugins.ts: localdistresolution,NODE_ENV=production, esbuildnode20, terser compress + mangle), then gzip -9 and brotli -11 over the emitted chunk. That is whatpnpm bundle-comparemeasures, with exact byte counts instead of rounded kB. Composition is attributed withvisualize-selected'sraw-data.json, which reports unmangled rendered bytes per module.Base is
418f9851, the merge commit of #7371.What SchemaBinary costs
Every fixture is the same three-field struct, so the numbers subtract cleanly.
Schema.toCodecJsonbaselineSchemaBinary.toCodecSchemaBinary.parserSchemaBinary.fieldIdalonetoCodeccosts 48,991 min / 14,958 gzip / 13,212 brotli over a schema that was already there. Where that goes, in unmangled rendered bytes against the same baseline:SchemaBinary.jsinternal/dateTime.jsDuration.jsEncoding.jsBigDecimal.jsinternal/result.js,Cause,Chunk,HashMap,HashSet,Redacted, restThe
DateTime,DurationandBigDecimalweight is the leaf kinds, not overhead:DateTime.makeZonedUnsafealone bundles to 18,345 min / 6,423 gzip in isolation, and the format has adateTimeZonedkind.isCyclicreferencingChunk,HashMap,HashSetandRedactedwas raised as a bundle question when #7371 landed. Measured, it is a non-issue: those four cost 768 rendered bytes between them, because only theisXguards are reached and each shakes down to a prototype check.Tree shaking
fieldIdalone retainsfieldIdand nothing else. Export-level shaking inside the module is exact.toCodecandparserdo not drag each other in: 15,471 min bytes are codec-only, 4,582 parser-only, 92,335 shared.WriterandReadermethod has a caller, and no module-level binding is unreferenced.new TextEncoder(),new TextDecoder(...),makeOutputArena(...),new Writer(),new Reader(),Symbol.for(...)and theBigIntconstants all disappear when unused.EMPTY_READER_VIEWwas the only one whose argument defeated the annotation.What is not worth changing
expectedandErrorliterals in the built module saves 1,376 min / 376 gzip. That is the ceiling for deleting the entire error surface, so deduplicating the repeats is worth well under 200 bytes. Not worth touching code that is the documented error contract.KandFtables. Inlining them would save a few hundred bytes and would delete the two tables that state the wire kinds and the fingerprint tags in one readable place.layoutFingerprint, both positional struct paths,decodeUnionPositionalandsentinelSetHashsaves 4,569 min / 1,365 gzip, about 4% of the bundle.{ fingerprint: true }is a runtime option, so making that conditional means splitting the entry point, which is a public API change and out of scope here.The barrel costs more than anything in this module
Worth recording separately, because it is four times larger than every in-module opportunity combined and none of it is SchemaBinary's code.
from "effect/unstable/encoding/SchemaBinary"from "effect/unstable/encoding"The 28,625 min / 9,992 gzip difference is entirely msgpackr's
pack.jsandunpack.js, pulled in because the barrel re-exportsMsgpack, which importsmsgpackrat module scope. msgpackr declares no"sideEffects"and its top-level code is genuinely effectful (try { decoder = new TextDecoder() } catch {},let defaultPackr = new Packr(...)), so no bundler can drop it once the module is in the graph. Every consumer of the barrel pays it, whichever encoding module they actually wanted.Nothing here can fix that without changing when
Msgpackloads msgpackr, which is a different module's public behaviour. Flagging it rather than fixing it. Note thatSchemaBinary's own JSDoc examples use the barrel form.Verification
Behaviour. Differential harness against the base source: 33 schema/value pairs in both wire modes covering every leaf kind, optional and full struct presence, tuples with null and packed slots, records with index signatures, unions,
Option/Result/Exit, suspended recursion and JSON; each decoded at every truncation offset and with a surplus trailing byte; plus parser byte-at-a-time and bulk feeds, leftover bytes,maxFrameSize, a garbage envelope in both modes, fingerprint mismatch, missing required keys aterrors: "first"and"all", and cycle detection. 1,044 lines of hex, decoded values, issue text and issue paths. Samemd5sumas base, not merely diff-clean.Benchmarks.
benchmark/schema/SchemaBinary.ts, four interleaved base/head pairs, 78 SchemaBinary throughput rows: median -0.10%, mean +0.11%, range -2.08% to +2.35%, symmetric around zero. Every payload size row is identical at 0.00%.Checks.
pnpm lintclean,tsc -b tsconfig.jsonclean apart from the pre-existingNodeWorker.tswarning, 117 encoding tests, 8,710 effect tests.Also folded in the trailing comment fix left over from the #7371 review: the note above
missingKeyIssuestill described the old accumulator.