perf: add Rollup tree-shaking pre-pass for export * as barrels#22819
Closed
kitlangton wants to merge 3 commits into
Closed
perf: add Rollup tree-shaking pre-pass for export * as barrels#22819kitlangton wants to merge 3 commits into
export * as barrels#22819kitlangton wants to merge 3 commits into
Conversation
Bun's bundler (and esbuild) cannot tree-shake `export * as X from "./mod"` barrels — see evanw/esbuild#1420. Rollup can, using AST-level analysis to drop unused exports and their transitive imports. This adds a Rollup pre-pass to the build pipeline that runs before Bun's compile step. Rollup resolves internal source, eliminates dead code across the 57 `export * as` barrels, and writes tree-shaken ESM to a temp directory. Bun then compiles the pre-processed output into the final binary. - Add `script/treeshake-prepass.ts` with Bun.Transpiler-based Rollup plugin - Integrate into `script/build.ts` (skippable via `--skip-treeshake`) - Add `rollup` as devDependency - Pre-pass completes in ~5s, negligible vs full multi-platform build Current bundle savings are modest (~0.7%) because the single-entrypoint architecture means most code paths are reachable. Savings scale with: - Per-command entry points or lazy loading - Remaining 50 `export namespace` → `export * as` migrations - Future code splitting https://claude.ai/code/session_01R7zMpXjsq1R6uR7xpyJ14i
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a Rollup-based tree-shaking pre-pass to the packages/opencode build pipeline to eliminate dead code introduced by export * as X from "./mod" barrel patterns, then feeds the tree-shaken ESM output into Bun’s compile step.
Changes:
- Add
script/treeshake-prepass.tsto run Rollup with a Bun.Transpiler-backed plugin and emit tree-shaken ESM to.rollup-tmp/. - Integrate the pre-pass into
script/build.ts(opt-out via--skip-treeshake) and clean up.rollup-tmpafter the build. - Add Rollup as a dev dependency and ignore
.rollup-tmp.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/opencode/script/treeshake-prepass.ts | Implements Rollup pre-pass with custom resolution/transpile/asset handling and writes tree-shaken ESM output. |
| packages/opencode/script/build.ts | Runs the pre-pass before Bun.build, uses the resulting entrypoint, and removes the temp directory afterward. |
| packages/opencode/package.json | Adds rollup devDependency required for the pre-pass. |
| packages/opencode/.gitignore | Ignores .rollup-tmp output directory. |
| bun.lock | Locks the added rollup dependency. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| // Path alias mappings from tsconfig.json | ||
| const aliases: Record<string, string> = { | ||
| "@/": path.join(srcDir, "/"), |
| input: absEntries, | ||
| plugins: [bunTranspilePlugin], | ||
| treeshake: { | ||
| moduleSideEffects: false, // equivalent to sideEffects: false |
Comment on lines
+125
to
+130
| transform(code, id) { | ||
| if (!id.endsWith(".ts") && !id.endsWith(".tsx")) return null | ||
| const loader = id.endsWith(".tsx") ? "tsx" : "ts" | ||
| const t = new Bun.Transpiler({ loader, tsconfig: JSON.stringify({ compilerOptions: { jsx: "preserve" } }) }) | ||
| return { code: t.transformSync(code), map: null } | ||
| }, |
Replace 23 static command imports with lazy-loaded dynamic imports. Each command's heavy dependencies (Provider, Session, MCP, TUI, AI SDKs) are now only loaded when that specific command is invoked. Combined with the Rollup tree-shaking pre-pass, this produces 45 chunks instead of a single bundle. The entry chunk (what loads on startup) drops from ~9.8MB to ~120KB — a 98.8% reduction in startup load. - Add `lazyCmd` helper to `cli/cmd/cmd.ts` for type-safe lazy commands - Inline all builder options (yargs metadata) in `index.ts` - Dynamic `import()` in handlers defers heavy module loading - `opencode --help` / `--version` no longer loads AI SDK, MCP, TUI, etc. https://claude.ai/code/session_01R7zMpXjsq1R6uR7xpyJ14i
…tting" This reverts commit e320484.
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.
Summary
Adds a Rollup tree-shaking pre-pass to the build pipeline, responding to this comment on #22685 pointing out that Bun's bundler can't tree-shake
export * asbarrels.script/treeshake-prepass.ts— Rollup pre-pass with Bun.Transpiler-based pluginscript/build.ts(skippable via--skip-treeshake)rollupas devDependency,.rollup-tmpto.gitignoreThe problem, verified
We tested every bundler against the
export * as X from "./mod"pattern that the namespace migration (#22685) introduced. Results with a test module where onlysmallis used butbigandheavyFunctionare also exported:import { small } from "./mod"(direct)import * as Mod from "./mod"(star)export * as Modbarrelexport namespace(old pattern)Only Rollup can tree-shake
export * asbarrels. Both Bun and esbuild keep all exports (evanw/esbuild#1420, still open). Rollup does AST-level analysis and traces which properties of the namespace object are actually accessed.Real-world simulation
We simulated the Provider module scenario from the namespace-treeshake spec —
cli/error.tsimportsProvider.ModelNotFoundErrorfor lightweight.isInstance()checks, while the Provider module also exportscreateProvider(which pulls in heavy AI SDK deps):The pipeline
Rollup resolves internal source (path aliases, hash imports, .txt/.json assets), eliminates dead code across all 57
export * asbarrels, and writes tree-shaken ESM. Bun then compiles that output into the final binary. Pre-pass takes ~5s.Current savings
With the single-entrypoint architecture, bundle savings are ~0.7% (67KB). This is because most code paths ARE reachable from
src/index.ts— the CLI uses most of its modules. The infrastructure is correct and validated; the savings scale with:export namespace→export * asmigrations — more barrels = more tree-shake surfaceTest plan
bun script/treeshake-prepass.tscompletes successfully (~5s)--skip-treeshakefallbackbun run build --singleproduces working binary (needs CI)https://claude.ai/code/session_01R7zMpXjsq1R6uR7xpyJ14i