Skip to content

docs(ui): analyze remaining ui and preview bundle size levers - #10632

Open
GiladShoham wants to merge 3 commits into
masterfrom
docs/bundle-size-analysis
Open

docs(ui): analyze remaining ui and preview bundle size levers#10632
GiladShoham wants to merge 3 commits into
masterfrom
docs/bundle-size-analysis

Conversation

@GiladShoham

@GiladShoham GiladShoham commented Aug 18, 2026

Copy link
Copy Markdown
Member

Adds docs/ui-bundle-size-analysis.md — where the remaining bundle size sits after #10628 and #10629, what I measured, and what I tried that did not work. Written to be picked up cold in a later session.

Rebased onto master now that #10628, #10629 and #10631 have merged; targets master directly.

Env preview duplication is deliberately excluded — the core envs are being removed, which takes it along.

Also in here

  • Preview bundle stats. BIT_UI_BUNDLE_STATS=1 now covers the preview pre-bundle too, so one build produces browser, scope-ssr and preview stats together. It is a small copy of the UI helper rather than an import: @teambit/ui's index is imported by browser code, and re-exporting a node-only module through it pulled fs polyfills into the UI bundle (caught by the build failing on Can't resolve 'constants').
  • Stats were being under-reported. rspack's toJson groups assets and modules into summary rows ("assets by status") that carry a size but no name. That showed up as a single unattributable 3.3 MB / 17.9% bucket, and assets: 0. All groupModulesBy* / groupAssetsBy* flags are now off.
  • analyze-bundle.mjs crashed on assets without a name (those same grouped rows).
  • code-view.tsx imported createElement from the react-syntax-highlighter package root, which defeats its own prism-light import two lines later. Changed to the deep path. Behaviour-neutral, and worth stating plainly: it saves nothing today — see below.

Headline findings

The two eagerly-loaded syntax highlighting registries are the biggest single itemhighlight.js (1.34 MB, every language) plus refractor (0.85 MB, every Prism language), in both the browser and ssr bundles. Neither is imported directly anywhere in this repo. They come in through react-syntax-highlighter's package root, which re-exports every build including the full-language ones.

The blocker is that the remaining root imports are in published @teambit components in node_modules (api-reference.renderers.schema-node-member-summary, documenter.ui.code-snippet), whose source is not in this repo. I verified this: fixing the in-repo imports changes the artifact by 0 bytes. Filed separately as #10633.

I tried the bundler-level workaround (alias lowlightlowlight/lib/core, refractorrefractor/core) and rejected it: bit fails the build because both are transitive and would have to be declared dependencies of @teambit/ui, and it silently degrades any consumer relying on auto-registered languages to plain text. That is a product call, not a build one.

lodash is the best effort-to-reward item. It is CJS-only (no module field), so it cannot be tree-shaken, and the repo has 280 from 'lodash' imports and zero cherry-picked ones. It is 0.52 MB of the 1.96 MB preview bundle — 28% — for six functions. Fixing it pays out in the browser, ssr and preview bundles at once.

Also documented: graphql shipping whole into preview (28%), sucrase (0.47 MB) arriving via react-live and never lazy-loaded, date-fns at 302 modules, and the fact that one 6.23 MB chunk is the entire eager payload — which is what makes the cold-cache first paint slower than client-only rendering (measured in #10628).

@shikijs/langs is 1.45 MB but already lazy-loaded per language, and is called out as the pattern the rest of the UI should copy.

Update after the stack merged

Rebased onto master. Two follow-ups folded in:

Qodo reviewed this PR and found no issues.

Re-validated on the rebased branch: full compile, fresh-capsule build of both bundles with BIT_UI_BUNDLE_STATS=1, and analyze-bundle.mjs reproducing the figures the doc quotes — 6.23 MB eager chunk, @shikijs/langs 1.45 MB / 9.5%, highlight.js 1.34 MB / 8.7%, and in preview graphql 0.52 MB / 27.9% next to lodash 0.52 MB / 27.7%.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Document UI bundle levers and add preview bundle statistics

📝 Documentation ✨ Enhancement 🐞 Bug fix 🕐 20-40 Minutes

Grey Divider

AI Description

• Documents measured UI and preview bundle composition, blockers, and prioritized optimization
 levers.
• Adds opt-in preview statistics and disables rspack summary grouping for accurate analysis.
• Hardens bundle analysis and avoids a syntax-highlighter package-root import.
Diagram

graph TD
  FLAG["Stats flag"] --> UI["UI compilation"] --> FILES[("Stats JSON")] --> SCRIPT["Bundle analyzer"] --> FINDINGS["Size findings"]
  FLAG --> PREVIEW["Preview compilation"] --> FILES
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Deep-import the UI statistics helper
  • ➕ Avoids duplicating environment and serialization logic.
  • ➕ Keeps UI and preview statistics options synchronized automatically.
  • ➖ Couples preview bundling to an internal UI module path.
  • ➖ Could expose node-only filesystem dependencies to browser compilation again.
  • ➖ Requires a stable package export that does not currently exist.
2. Extract a node-only statistics utility
  • ➕ Provides one implementation without crossing browser package boundaries.
  • ➕ Allows future bundlers to reuse the same statistics format safely.
  • ➖ Introduces a new module or package for a small helper.
  • ➖ Expands the scope and dependency metadata of this documentation-focused PR.

Recommendation: Keep the small preview-local helper in this PR because it preserves the browser/server boundary and avoids filesystem polyfills. If another compilation needs the same behavior, extract both copies into an explicitly node-only utility with a dedicated export rather than re-exporting it through @teambit/ui.

Files changed (6) +301 / -2

Enhancement (2) +68 / -0
bundle-stats.tsAdd opt-in preview bundle statistics writer +61/-0

Add opt-in preview bundle statistics writer

• Adds a preview-local, node-only helper using BIT_UI_BUNDLE_STATS to write detailed rspack statistics outside shipped artifacts. Grouping options are disabled so every asset and module remains individually attributable.

scopes/preview/preview/bundle-stats.ts

pre-bundle.tsEmit statistics after successful preview pre-bundling +7/-0

Emit statistics after successful preview pre-bundling

• Writes preview.stats.json after a successful rspack compilation when statistics are enabled. Statistics failures are logged at debug level and never fail or replace the build result.

scopes/preview/preview/pre-bundle.ts

Bug fix (2) +22 / -1
bundle-stats.tsPrevent rspack from grouping bundle statistics +19/-0

Prevent rspack from grouping bundle statistics

• Disables asset and module summary grouping while retaining cached, orphaned, and nested entries. This prevents unnamed aggregate rows from under-reporting assets and obscuring package attribution.

scopes/ui-foundation/ui/rspack/bundle-stats.ts

analyze-bundle.mjsHandle unnamed and partially populated asset entries +3/-1

Handle unnamed and partially populated asset entries

• Filters out assets without names before checking source-map suffixes and safely sorts entries with missing sizes. This prevents grouped rspack rows from crashing bundle analysis.

scripts/analyze-bundle.mjs

Documentation (1) +207 / -0
ui-bundle-size-analysis.mdDocument remaining UI and preview bundle-size opportunities +207/-0

Document remaining UI and preview bundle-size opportunities

• Adds reproducible measurements of browser, scope SSR, and preview bundles. It ranks remaining optimization levers, records upstream blockers and rejected aliases, and distinguishes emitted size from parsed module size.

docs/ui-bundle-size-analysis.md

Other (1) +4 / -1
code-view.tsxDeep-import the syntax-highlighter element renderer +4/-1

Deep-import the syntax-highlighter element renderer

• Replaces the react-syntax-highlighter package-root import with its ESM create-element path. This avoids independently introducing the full Highlight.js and Prism registries, although existing transitive root imports currently prevent an artifact-size reduction.

components/ui/code-view/code-view.tsx

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 18, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Analyzer shortcut always fails 🐞 Bug ≡ Correctness
Description
The documented npm run analyze-bundle alternative supplies no stats files, so the analyzer prints
usage and exits with status 1. Users following the reproduction instructions cannot analyze the
generated bundle stats through the advertised shortcut.
Code

docs/ui-bundle-size-analysis.md[36]

+node scripts/analyze-bundle.mjs bundle-stats/*.stats.json     # or: npm run analyze-bundle
Evidence
The package script passes no arguments to the analyzer, while the analyzer explicitly exits with
status 1 when no positional files are present.

package.json[31-33]
scripts/analyze-bundle.mjs[96-100]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Correct the documented npm analyzer command because invoking `npm run analyze-bundle` without arguments exits with an error.

## Issue Context
The npm script only launches `scripts/analyze-bundle.mjs`, which requires at least one positional stats-file path. Document argument forwarding, for example `npm run analyze-bundle -- bundle-stats/*.stats.json`, or change the script to provide suitable defaults.

## Fix Focus Areas
- docs/ui-bundle-size-analysis.md[33-37]
- package.json[31-33]
- scripts/analyze-bundle.mjs[96-100]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 2843c77 ⚖️ Balanced

Results up to commit 962d791


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Base automatically changed from test/ui-start-e2e to master August 19, 2026 08:48
GiladShoham added a commit that referenced this pull request Aug 19, 2026
…10628/#10629 (#10631)

Sanity e2e for `bit start` itself, covering both UI roots. Part 3 of
#10596 follow-up work.

> Rebased onto `master` now that #10628 and #10629 have merged; targets
`master` directly.

Until now nothing exercised `bit start` end to end. That is how the
scope SSR bundle managed to throw on every request for months (#10628),
and the layout change in #10629 has a matching failure mode: name the
fallback document wrong and every *client-side* route 404s while
SSR-rendered ones keep working.

`e2e/harmony/ui-start.e2e.ts` starts a real server per root and asserts,
over http:

- startup writes nothing matching `/error|exception|unhandled/i` to
stderr — a server can listen fine with an aspect that failed to load
- the served document has a react root
- **every script and stylesheet the document references actually
resolves 200** — this is the one that catches assets emitted under a
path the server does not expose
- a deep client-side route returns a document (the history-api fallback)
- `/graphql` answers without errors
- workspace only: the document loads the *workspace* entry and not the
scope one — both roots are entries of one bundle now, so serving the
wrong document would still look like a working page, just booting the
other root's app
- scope only: the markup is server-rendered and contains the exported
component

12 assertions, ~1 min. All `--rebuild`, so they describe this repo's
code rather than whichever bit release is installed.

## Two supporting changes

**`HttpHelper` can start either root.** It was hardcoded to the bare
scope (`scopes.remotePath`, and a ready-message string naming
`teambit.scope/scope`). It now takes `{ extraArgs, uiRootAspectId }`,
derives the cwd from the root, and builds the ready message per root.
Existing callers use the unchanged two-arg form. It also records stderr
so tests can assert on a clean startup.

**`portHolders()` now filters to listening sockets** (`lsof -ti tcp:PORT
-sTCP:LISTEN`). Without it lsof also reports processes holding a
*client* socket to the port — including the mocha process itself, since
node keeps connections alive after a test fetches from the server.
`waitForPortToBeFree` read that as a foreign process squatting the port
and refused to continue, failing the `after` hooks. Only a listener can
actually hold a port. This was a latent bug in the helper; the new tests
hit it because they fetch every referenced asset.



---

## Also: Qodo review findings from #10628 / #10629

Both of those merged before their review findings were addressed, so the
actionable ones land here. Each was verified against the code rather
than taken on trust.

**`bit start` 404s on an existing local UI build (from #10629) — the
important one.** `buildIfNoBundle()` treated *any* existing `public/bit`
directory as a valid build, but the server now falls back to
`<root>.html`, which a build made before #10629 does not contain.
Reproduced end to end: with the pre-fix check the whole UI returns
**404** on `/` and on deep routes; with the fix it detects the missing
document, rebuilds, and serves 200. This would have hit every user
upgrading past #10629 with a previously-built local UI. It now checks
for the root's document rather than the directory.

**Hash written for roots that were never built (from #10629).**
`generateHash()` walked a hardcoded root list and threw when one was not
registered. Beyond failing in a scope-only runtime, it could record a
hash for a root whose document was never emitted — which reads at
startup as "a pre-bundle exists" and then 404s, the same failure as
above. It now walks the same registered roots `build()` turns into
entries, via a new `UiMain.getUiRoots()`.

**Service worker bound to a document that is not emitted (from
#10629).** Confirmed in the built artifact: `service-worker.js`
contained `createHandlerBoundToURL("public/index.html")` while the build
emits only `scope.html` / `workspace.html`. With an entry per root there
is no single app shell, so `navigateFallback` is removed — the express
history-api fallback already serves the right document. Verified the
built service worker no longer contains that binding.

**Entry name collisions (from #10629).** `Object.fromEntries` would
silently keep only the last of two entries sharing a sanitized name,
leaving a root with no chunks and no document while still looking built.
Now throws instead.

**Stats filename could break (from #10628).** `writeBundleStats`
interpolated an unsanitized name into a path, so a root name containing
`/` would fail with ENOENT into a swallowed debug log. Now sanitized.

Not changed: the "ad-hoc chalk in `writeStats`" rule violation. That
line matches the surrounding `[Rspack]` log statements in the same file;
the style guide it cites covers section titles and symbols in command
output, not diagnostic log lines. Happy to switch it if you'd rather be
strict.

The `preview/bundle-stats.ts` copy of the sanitization fix lands with
#10632, which is where that file lives.
@GiladShoham
GiladShoham force-pushed the docs/bundle-size-analysis branch from 53d8234 to 2843c77 Compare August 19, 2026 09:17
```bash
BIT_UI_BUNDLE_STATS=1 bit build "teambit.ui-foundation/ui, teambit.preview/preview" \
--tasks "BundleUI,PreBundlePreview" --reuse-capsules --unmodified
node scripts/analyze-bundle.mjs bundle-stats/*.stats.json # or: npm run analyze-bundle

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Analyzer shortcut always fails 🐞 Bug ≡ Correctness

The documented npm run analyze-bundle alternative supplies no stats files, so the analyzer prints
usage and exits with status 1. Users following the reproduction instructions cannot analyze the
generated bundle stats through the advertised shortcut.
Agent Prompt
## Issue description
Correct the documented npm analyzer command because invoking `npm run analyze-bundle` without arguments exits with an error.

## Issue Context
The npm script only launches `scripts/analyze-bundle.mjs`, which requires at least one positional stats-file path. Document argument forwarding, for example `npm run analyze-bundle -- bundle-stats/*.stats.json`, or change the script to provide suitable defaults.

## Fix Focus Areas
- docs/ui-bundle-size-analysis.md[33-37]
- package.json[31-33]
- scripts/analyze-bundle.mjs[96-100]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 2843c77

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants