| 1 | # Bundle Size Development Workflow |
| 2 | |
| 3 | This package contains the internal bundle-size tooling used to measure Effect |
| 4 | entrypoints with Rollup, minification, and gzip. |
| 5 | |
| 6 | ## AI Agent Instructions |
| 7 | |
| 8 | Use this workflow when a user wants to quickly understand how a local source |
| 9 | change affects bundle size for one or more explicitly selected fixture files. |
| 10 | |
| 11 | Do not scan `scratchpad/` or `packages/tools/bundle/fixtures/` for this workflow. |
| 12 | Only measure files that the user explicitly names, or files you create |
| 13 | specifically for the current investigation. |
| 14 | |
| 15 | Use the default cleanup mode. Pass `--keep-base` only when repeated measurements |
| 16 | are explicitly requested or clearly necessary, then clean up after the final |
| 17 | run. |
| 18 | |
| 19 | If the user asks what a generated bundle is made of, use the bundle composition |
| 20 | workflow instead of the size comparison workflow. |
| 21 | |
| 22 | ### Analyze Bundle Composition |
| 23 | |
| 24 | Use this workflow when the user gives an explicit fixture and wants to |
| 25 | understand which modules make up the produced bundle. |
| 26 | |
| 27 | Agent procedure: |
| 28 | |
| 29 | 1. Confirm the exact fixture path or paths to analyze. Do not scan directories. |
| 30 | 2. Run `pnpm bundle-analyze` with only those explicit paths. |
| 31 | 3. Read the printed Markdown table and identify the generated `*.raw-data.json` |
| 32 | path for each fixture. |
| 33 | 4. Analyze `*.raw-data.json` first. Use `*.treemap.html` only when the user wants |
| 34 | a visual artifact to open. |
| 35 | 5. Report the largest modules, notable dependency groups, and any surprising |
| 36 | inclusions. Mention the generated artifact paths in the answer. |
| 37 | |
| 38 | Prefer the repository-level wrapper: |
| 39 | |
| 40 | ```sh |
| 41 | pnpm bundle-analyze scratchpad/my-fixture.ts |
| 42 | ``` |
| 43 | |
| 44 | You can pass multiple explicit files: |
| 45 | |
| 46 | ```sh |
| 47 | pnpm bundle-analyze \ |
| 48 | scratchpad/schema-codec.ts \ |
| 49 | scratchpad/schema-arbitrary.ts |
| 50 | ``` |
| 51 | |
| 52 | The wrapper builds the current checkout with `pnpm build` before generating |
| 53 | analysis artifacts, so the bundle is not produced from stale `dist` files. |
| 54 | |
| 55 | By default, artifacts are written to `tmp/bundle-analysis`. Use `--output-dir` |
| 56 | to choose another directory: |
| 57 | |
| 58 | ```sh |
| 59 | pnpm bundle-analyze --output-dir tmp/schema-analysis scratchpad/my-fixture.ts |
| 60 | ``` |
| 61 | |
| 62 | For each selected fixture, the tool writes: |
| 63 | |
| 64 | - `<name>.min.js`: the generated Rollup output for inspection; |
| 65 | - `<name>.treemap.html`: an interactive treemap for human inspection; |
| 66 | - `<name>.raw-data.json`: raw visualizer data suitable for AI analysis. |
| 67 | |
| 68 | The analysis build disables identifier mangling so module and export names stay |
| 69 | readable in the visualizer output. Use `bundle-compare-selected` or `report` |
| 70 | when exact size measurement is the goal. |
| 71 | |
| 72 | After running the command, inspect the Markdown table it prints to find the exact |
| 73 | paths. For an AI analysis, read the `*.raw-data.json` file first and summarize |
| 74 | the largest modules and dependency groups. Use the `*.treemap.html` file when |
| 75 | the user wants to inspect the bundle visually. |
| 76 | |
| 77 | The raw data contains a tree plus module metadata. For a quick size-oriented |
| 78 | summary, rank module parts by `renderedLength` or `gzipLength`, then map each |
| 79 | part through `metaUid` to `nodeMetas[metaUid].id`. |
| 80 | |
| 81 | Example inspection command: |
| 82 | |
| 83 | ```sh |
| 84 | node -e 'const data = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")); console.log(Object.entries(data.nodeParts).map(([uid, part]) => ({ uid, id: data.nodeMetas[part.metaUid]?.id, rendered: part.renderedLength, gzip: part.gzipLength })).sort((a, b) => b.rendered - a.rendered).slice(0, 20))' tmp/bundle-analysis/my-fixture.raw-data.json |
| 85 | ``` |
| 86 | |
| 87 | Do not treat the generated `.min.js` size from this workflow as the exact bundle |
| 88 | size comparison number. The analysis build keeps names readable for the |
| 89 | visualizer. Use `bundle-compare-selected` or `report` for exact size reporting. |
| 90 | |
| 91 | Do not use this workflow to compare against a base ref. Use |
| 92 | `bundle-compare-selected` for size impact and `bundle-analyze` for current bundle |
| 93 | composition. |
| 94 | |
| 95 | ### Compare Explicit Scratchpad Fixtures |
| 96 | |
| 97 | Prefer the repository-level wrapper: |
| 98 | |
| 99 | ```sh |
| 100 | pnpm bundle-compare-selected --base main scratchpad/my-fixture.ts |
| 101 | ``` |
| 102 | |
| 103 | You can pass multiple explicit files: |
| 104 | |
| 105 | ```sh |
| 106 | pnpm bundle-compare-selected --base main \ |
| 107 | scratchpad/schema-codec.ts \ |
| 108 | scratchpad/schema-arbitrary.ts |
| 109 | ``` |
| 110 | |
| 111 | If `--base` is omitted, the script compares against `main`. |
| 112 | |
| 113 | By default, the wrapper removes `tmp/bundle-base` before exiting, including on |
| 114 | failure. This avoids leaving extra git worktree state after a one-off |
| 115 | measurement. |
| 116 | |
| 117 | For repeated measurements where the user explicitly wants a faster feedback loop, |
| 118 | pass `--keep-base`: |
| 119 | |
| 120 | ```sh |
| 121 | pnpm bundle-compare-selected --base main --keep-base scratchpad/my-fixture.ts |
| 122 | ``` |
| 123 | |
| 124 | When using `--keep-base`, clean up `tmp/bundle-base` after the last measurement. |
| 125 | The explicit final cleanup command is: |
| 126 | |
| 127 | ```sh |
| 128 | git worktree remove --force tmp/bundle-base |
| 129 | ``` |
| 130 | |
| 131 | The wrapper: |
| 132 | |
| 133 | - builds the current checkout with `pnpm build`; |
| 134 | - creates or reuses `tmp/bundle-base` at the requested base ref; |
| 135 | - builds the base checkout when needed and writes |
| 136 | `tmp/bundle-base/.bundle-build-stamp` only after a successful build; |
| 137 | - reuses a kept base checkout only when its HEAD and `.bundle-build-stamp` |
| 138 | both match the requested base ref; |
| 139 | - copies only the selected files into the base checkout at the same relative |
| 140 | paths; |
| 141 | - invokes the internal bundle CLI to compare current vs base sizes. |
| 142 | |
| 143 | The selected files are copied into the base checkout so the report isolates |
| 144 | source changes instead of changes to the fixture text. |
| 145 | |
| 146 | Expected output is a Markdown table: |
| 147 | |
| 148 | ```md |
| 149 | | File Name | Current Size | Previous Size | Difference | |
| 150 | | :------------------------- | :----------: | :-----------: | :---------------: | |
| 151 | | `scratchpad/my-fixture.ts` | 42.10 KB | 40.80 KB | +1.30 KB (+3.19%) | |
| 152 | ``` |
| 153 | |
| 154 | ### Internal CLI |
| 155 | |
| 156 | Use the internal CLI directly only when the base checkout is already prepared |
| 157 | and built: |
| 158 | |
| 159 | ```sh |
| 160 | node packages/tools/bundle/src/bin.ts compare-selected \ |
| 161 | --base-dir tmp/bundle-base \ |
| 162 | scratchpad/my-fixture.ts |
| 163 | ``` |
| 164 | |
| 165 | The `--base-dir` value must be the root of the base checkout, not the base |
| 166 | fixture directory. |
| 167 | |
| 168 | ### Current Size Only |
| 169 | |
| 170 | If the user only asks for the current bundled size, use: |
| 171 | |
| 172 | ```sh |
| 173 | pnpm build |
| 174 | pnpm --dir packages/tools/bundle report ../../../scratchpad/my-fixture.ts |
| 175 | ``` |
| 176 | |
| 177 | Run the build immediately before the report because the direct CLI resolves |
| 178 | Effect packages from `dist`. This does not compare against a base ref. |
| 179 | |
| 180 | ### Fixture Guidance |
| 181 | |
| 182 | Keep temporary fixtures in `scratchpad/`. |
| 183 | |
| 184 | Good temporary fixtures are small, focused entrypoints that import public |
| 185 | package APIs: |
| 186 | |
| 187 | ```ts |
| 188 | import * as Effect from "effect/Effect" |
| 189 | import * as Schema from "effect/Schema" |
| 190 | |
| 191 | const schema = Schema.Struct({ |
| 192 | name: Schema.String |
| 193 | }) |
| 194 | |
| 195 | Schema.decodeUnknownEffect(schema)({ name: "effect" }).pipe(Effect.runFork) |
| 196 | ``` |
| 197 | |
| 198 | Prefer self-contained fixtures. The compare-selected workflow copies only the |
| 199 | explicit entry files into the base checkout. If a fixture imports local relative |
| 200 | helpers, either avoid that shape or make the entry fixture self-contained before |
| 201 | measuring. |
| 202 | |
| 203 | Do not add temporary fixtures to `packages/tools/bundle/fixtures/`. That |
| 204 | directory is for stable comparison fixtures used by the regular bundle-size |
| 205 | workflow. |
| 206 | |
| 207 | ### Cleanup |
| 208 | |
| 209 | The wrapper cleans up `tmp/bundle-base` by default. If `--keep-base` was used for |
| 210 | a multi-run investigation, remove the worktree when the user is done. |
| 211 | |
| 212 | To remove it: |
| 213 | |
| 214 | ```sh |
| 215 | git worktree remove --force tmp/bundle-base |
| 216 | ``` |
| 217 | |
| 218 | To verify that cleanup succeeded: |
| 219 | |
| 220 | ```sh |
| 221 | git worktree list |
| 222 | ``` |
| 223 | |
| 224 | Only the main repository worktree should remain. |