perf(export): replace jszip with client-zip - 302 -> 211 kB bundle - #55
Merged
Conversation
The Download ZIP button bundles three short text files (the two Factorio JSON documents plus the exchange string) for the headless CLI route. It was costing 97 kB of the shipped bundle to do it. `jszip`'s `browser` field resolves `./lib/index` to `dist/jszip.min.js`, a 97.6 kB pre-minified **browserify** bundle. That is an opaque IIFE with its own module registry, so it cannot be tree-shaken, and the pako 1.0.11 baked inside it cannot dedupe against the app's pako 3.0.1. The app was shipping two pako majors plus `readable-stream`, `setimmediate` and eight more Node shims in order to write three text files. Measured, by building both ways: | | raw | gzip | | --- | --- | --- | | before (jszip) | 302.07 kB | 99.81 kB | | after (client-zip) | 211.44 kB | 73.11 kB | | delta | **-90.63 kB (-30%)** | **-26.70 kB (-27%)** | Isolating the libraries: a build with the zip feature stubbed out entirely is 204.95 kB, so `client-zip` costs **6.49 kB** where `jszip` cost 97.1 kB - about 15x smaller for this use. Confirmed gone from the bundle rather than assumed: `JSZip`, `jszip`, `setImmediate`, `readable-stream` and `_tr_stored_block` markers all drop to zero, and pako's `incorrect header check` string goes from 2 occurrences to 1 - the codec's pako 3.0.1 remains, the duplicate is gone. `vp build` still prints no warnings at all, which is the standing invariant. **The output is materially unchanged, and the reason is worth recording: jszip was not compressing either.** Both writers emit `Stored` entries at 0% - `generateAsync` without an explicit `compression` option does not deflate. So the whole pako 1.x payload was dead weight for this path. The downloaded archive goes 7888 -> 7936 bytes (+48 bytes, +0.6%), which is the central-directory layout differing slightly, not a compression regression. `jszip` moves to devDependencies and stays the **reader** in `test/zipExport.spec.ts`. That is deliberate: checking a client-zip archive with client-zip would prove self-consistency, not correctness. Validated against a third implementation too - `unzip -t` (Info-ZIP) reports "No errors detected" and lists all three entries with correct names, sizes and CRCs. Note `pnpm add -D` will not relocate a package whose installed version already satisfies the range - it prints "Already up to date" and leaves it where it was - so the dependencies -> devDependencies move was made by editing `package.json` directly and re-running `pnpm install`. The spec gains two cases, because changing the *writer* means entry presence is no longer a sufficient assertion: one parses both JSON entries back and checks real fields, and asserts the exchange string survives verbatim against the fixture; one covers preset-name-derived entry naming and the exact entry set. 1249 -> 1251 tests. Not adopted for currency: `client-zip`'s last human commit is 2025-03-14 (the newer repo push is a Dependabot branch). This swap buys bundle size and API fit, not fresher maintenance. What it does improve is backlog - 8 open issues against jszip's 412 and 40 unanswered PRs. Gate: `pnpm run verify` exit 0 - 314 files formatted, 301 clean, app 1251 passed / 3 skipped, worker 12, container 3. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GjMSePtN9aTgr8EtZp66k1
wormeyman
force-pushed
the
perf/client-zip
branch
from
July 30, 2026 05:52
af183fe to
39a5890
Compare
wormeyman
added a commit
that referenced
this pull request
Jul 30, 2026
…RULE, not the fields (#57) Verified locally and in CI, and the anti-vacuity control was tested by breaking it. ## The `+3` control is genuinely load-bearing - proven, not assumed This repo has a documented history of confirmations that turned out vacuous, so I sabotaged the substitution rather than trusting the assertion. Replacing the game-field lookup with one that never matches - i.e. simulating every lookup silently falling through to our own field: | test | with substitution dead | | --- | --- | | "substituting the game's own elevation and cliffiness does not move a single cell" | **still passes** | | "and the substitution really is live - a `+3` elevation bias does move cells" | **FAILS**: `AssertionError: expected 0 to be greater than 5` | So the headline assertion *would* have passed vacuously, and the `+3` control is exactly what catches it. Restored and re-confirmed 9/9 green. `expect(game.size).toBeGreaterThan(500)` is a second, independent guard against an empty placement set making the equality trivial. ## Provenance `test/fixtureProvenance.spec.ts` passes (4/4). Both new fixtures carry entries with `factorioVersion: "2.1.12"` and concrete `evidence` naming the capture script and recording that `refs:sync --check` reported in-sync at capture time - so neither lands as `unknown`, and the spec's `unknown` cap is not touched. ## Gate - Local `pnpm run verify` exit **0** in 60s on the exact tree that lands (`5399b6f`): format 316, lint+type 301, `check:vue` clean, app **1255 passed / 3 skipped**, worker 12. - CI `verify`: **pass** (4m41s). The branch was updated **twice** so the run tests the real target - it now contains #51, #52, #55 and #59, not the older `main` the first run saw. - No `src/` change, so nothing shipped to users; the new spec needs no Factorio at runtime because the fixtures are committed, which is why CI can run it. ## Two notes, neither blocking - The description reads as though `test/vulcanusOreCliffSeparation.spec.ts` is new. It is not - it already existed on `main` at 222 lines and this extends it to 597 (3 -> 7 `it` blocks at the top level, 9 tests total), which is why the file count stays 143 and the suite moves +4 rather than +9. - Re-deriving the binary reading and the statistics was explicitly out of scope for this pass, so the `0x1016229b4` / `0x101625038` disassembly claims and the Poisson figures are **recorded as the author's measurements, not independently reproduced here**. What was checked is that the code is gated, the fixtures have provenance, and the assertions can fail.
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.
The Download ZIP button bundles three short text files - the two Factorio JSON documents the headless CLI consumes (
--map-gen-settings/--map-settings) plus the exchange string. It was costing 97 kB of the shipped bundle to do that.Why jszip was so expensive
Its
browserfield resolves./lib/indextodist/jszip.min.js, a 97.6 kB pre-minified browserify bundle. That is an opaque IIFE with its own module registry, so it cannot be tree-shaken - and the pako 1.0.11 baked inside it cannot dedupe against the app's pako 3.0.1. The app was shipping two pako majors, plusreadable-stream,setimmediateand eight more Node shims, to write three text files.Measured, by building both ways
Isolating the libraries: a build with the zip feature stubbed out entirely is 204.95 kB, so
client-zipcosts 6.49 kB wherejszipcost 97.1 kB - roughly 15x smaller for this use.Confirmed gone rather than assumed. Marker counts in the shipped bundle:
JSZip/jszipsetImmediatereadable-stream_tr_stored_block(pako deflate)incorrect header check(pako)vp buildstill prints no warnings at all, which is the standing invariant.The output is materially unchanged - and jszip wasn't compressing either
Worth recording, because it explains why the pako payload was pure waste: both writers emit
Storedentries at 0%.generateAsyncwithout an explicitcompressionoption does not deflate.The downloaded archive goes 7888 → 7936 bytes (+48, +0.6%) - central-directory layout, not a compression regression.
jszip stays, as the test oracle
It moves to
devDependenciesand remains the reader intest/zipExport.spec.ts. Deliberate: checking a client-zip archive with client-zip would prove self-consistency, not correctness.Validated against a third implementation too - Info-ZIP's
unzip -t:Gotcha for the next person:
pnpm add -Dwill not relocate a package whose installed version already satisfies the range - it prints "Already up to date" and leaves it independencies. The move was made by editingpackage.jsondirectly, thenpnpm install.Test changes
Changing the writer means "the entry exists" stops being a sufficient assertion, so the spec gains two cases: one parses both JSON entries back and checks real fields, and asserts the exchange string survives verbatim against the fixture; one covers preset-name-derived entry naming and the exact entry set. 1249 → 1251 tests.
What this does not buy
Not fresher maintenance.
client-zip's last human commit is 2025-03-14 - the more recent repo push is a Dependabot branch. This swap buys bundle size and API fit. What it does improve is backlog health: 8 open issues against jszip's 412 open issues and 40 unanswered PRs (newest, #967 from 2026-07-15, still has zero comments).client-zipis MIT, zero-dependency, browser-native ESM, and requires ES2020 BigInt - fine against this project'sESNexttarget.Gate
pnpm run verifyexit 0 - 314 files formatted, 301 clean, app 1251 passed / 3 skipped, worker 12, container 3.Follows the dependency audit of 2026-07-29.
🤖 Generated with Claude Code
https://claude.ai/code/session_01GjMSePtN9aTgr8EtZp66k1