From 406f760e2cc3278ba06c5e8bebf8e21b38fb8c18 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 05:02:51 +0000 Subject: [PATCH 1/3] perf(console): vendor-markdown claims only what the first load reaches (objectui#11854) Tag the `vendor-markdown` chunk group `$initial`, as `vendor-objectstack` is, so the family members only `plugin-markdown` reaches (rehype-highlight with lowlight and highlight.js, rehype-slug, rehype-autolink-headings, remark-github-blockquote-alert and their helpers) follow that plugin's lazy chunk instead of riding the eager one. `react-markdown`, needed by the eager `ui-components` co-tenant `MarkdownContent`, gets a group of its own so it does not land on the budgeted `ui-components` line. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude --- apps/console/vite.config.ts | 39 ++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/apps/console/vite.config.ts b/apps/console/vite.config.ts index 1b424e5761..0e32a097d0 100644 --- a/apps/console/vite.config.ts +++ b/apps/console/vite.config.ts @@ -1368,9 +1368,42 @@ export default defineConfig({ { name: 'plugin-calendar', test: /[\\/]packages[\\/]plugin-calendar[\\/]/, priority: 70 }, { name: 'plugin-kanban', test: /[\\/]packages[\\/]plugin-kanban[\\/]/, priority: 70 }, { name: 'plugin-chatbot', test: /[\\/]packages[\\/]plugin-chatbot[\\/]/, priority: 70 }, - // react-markdown / remark / micromark family — heavy markdown - // pipeline pulled in only by markdown/chatbot plugins. - { name: 'vendor-markdown', test: /[\\/]node_modules[\\/](react-markdown|remark-|rehype-|micromark|mdast-|hast-|unified|unist-|vfile|bail|trough|character-entities|decode-named-character-reference|devlop|estree-|comma-separated-tokens|space-separated-tokens|property-information|html-url-attributes|zwitch)/, priority: 85 }, + // + // ## `vendor-markdown` claims only what the first load reaches (objectui#11854) + // + // The remark / rehype / micromark family. The chatbot's message + // renderer (`streamdown`, imported statically by `plugin-chatbot`) + // reaches most of it on every page, so this chunk is eager. Without + // the tag the group also claimed the family members that only + // `plugin-markdown` reaches — `rehype-highlight` with `lowlight` and + // `highlight.js`, `rehype-slug`, `rehype-autolink-headings`, + // `remark-github-blockquote-alert` and their helpers — and one eager + // member made all of them eager: the objectui#11798 capture + // mechanism, one group over. `tags: ['$initial']` is the same option, + // for the same reason, as on `vendor-objectstack` above + // (objectui#11101): the group claims a family member only when a + // static import from the entry reaches it, and the rest follows its + // importer, `plugin-markdown`'s own group, behind that plugin's lazy + // registration and the docs reader's lazy route. The bytes are + // recorded once, on `BASELINE` in + // `scripts/check-eager-closure-budget.mjs`, ⛔ not here. + // + // ⛔ Not `includeDependenciesRecursively: false`, the flag + // objectui#11798 measured into a chunk cycle on `vendor-charts`; the + // tag narrows what the group claims and leaves its capture alone. + // + // `react-markdown` is the one member with no static path from the + // entry that the first load still needs. `MarkdownContent` in + // `packages/fields` imports it statically, and that widget sits in + // the EAGER `ui-components` chunk although it is only reached through + // `React.lazy` — the co-tenancy recorded in + // `scripts/vite-ineffective-dynamic-imports.ts` (objectui#5325). Left + // unclaimed, `ui-components` (priority 80) would take it by its own + // capture, onto a budgeted line; claimed by the tagged group it + // cannot be. So it gets a group of its own here, and its chunk loads + // with whichever importer loads first. + { name: 'vendor-markdown', test: /[\\/]node_modules[\\/](remark-|rehype-|micromark|mdast-|hast-|unified|unist-|vfile|bail|trough|character-entities|decode-named-character-reference|devlop|estree-|comma-separated-tokens|space-separated-tokens|property-information|html-url-attributes|zwitch)/, priority: 85, tags: ['$initial'] }, + { name: 'vendor-react-markdown', test: /[\\/]node_modules[\\/]react-markdown[\\/]/, priority: 84 }, // Sentry — only fetched when the RUNTIME serves a DSN on // /api/v1/runtime/config (objectstack#12681); a deployment that // configured none never requests this chunk at all. From 57a10660198e30d59a43fee0e407c23022afe748 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 05:09:58 +0000 Subject: [PATCH 2/3] perf(console): lower the eager-closure ledger by the 58,258 bytes vendor-markdown shed (objectui#11854) `MAX_EAGER_CLOSURE_GZIP_BYTES` comes down from 3,444,622 to 3,386,364, exactly the bytes that left the console's eager closure when `vendor-markdown` stopped claiming the family members only `plugin-markdown` reaches (`9cb4e29` 3,433,602 -> `406f760` 3,375,344 gzipped, same container, same instrument). `BASELINE` is re-pinned to `406f760` in the same commit, keeping the 11,020 bytes (0.12x) of headroom `main` had. The unit test's rendered-baseline literal moves from 3338.1 to 3296.2. No per-chunk row moved. Patch changeset for `@object-ui/console`. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude --- .changeset/11854-vendor-markdown-initial.md | 9 ++ .../check-eager-closure-budget.test.ts | 7 +- scripts/check-eager-closure-budget.mjs | 126 ++++++++++++++++-- 3 files changed, 125 insertions(+), 17 deletions(-) create mode 100644 .changeset/11854-vendor-markdown-initial.md diff --git a/.changeset/11854-vendor-markdown-initial.md b/.changeset/11854-vendor-markdown-initial.md new file mode 100644 index 0000000000..c1af5411a9 --- /dev/null +++ b/.changeset/11854-vendor-markdown-initial.md @@ -0,0 +1,9 @@ +--- +'@object-ui/console': patch +--- + +The console's first page load no longer downloads the documentation reader's markdown extras (objectui#11854). + +The console's `vendor-markdown` chunk is part of every first page load, because the chat message renderer uses most of the markdown pipeline. The chunk also held the parts only the `markdown` component and the package documentation reader use: code highlighting (`rehype-highlight` with `lowlight` and `highlight.js`), heading anchors (`rehype-slug`, `rehype-autolink-headings`, `github-slugger`), GitHub-style alerts (`remark-github-blockquote-alert`) and their helpers. Every first page load downloaded them, for every user. They now load with `@object-ui/plugin-markdown`, the first time a page renders a `markdown` component or a documentation page. `react-markdown` moves to a small chunk of its own, which the first load still fetches because a lazily rendered markdown field shares a chunk with eagerly loaded components. + +What renders is unchanged: chat replies, `markdown` components, markdown fields and documentation pages render as before. Nothing is added to or removed from any package entry, and no route, registry key or translation key changes. diff --git a/scripts/__tests__/check-eager-closure-budget.test.ts b/scripts/__tests__/check-eager-closure-budget.test.ts index e6f1e29657..39fdf4c6b7 100644 --- a/scripts/__tests__/check-eager-closure-budget.test.ts +++ b/scripts/__tests__/check-eager-closure-budget.test.ts @@ -811,10 +811,11 @@ describe('ceiling sensitivity, judged live (objectui#5924)', () => { // `@objectstack/*` 17.6.0's spec root shed its migration chain, // objectui#11717 UP to 3506.9 on the ruled raise for `@objectstack/*` // 17.7.0, ruling 6027998226, objectui#11798 down to 3338.1 when the Studio - // builder and the chart engine left the first load) — a rendering derived + // builder and the chart engine left the first load, objectui#11854 down to + // 3296.2 when the markdown highlighter left it) — a rendering derived // in the test would agree with the renderer by construction and pin // nothing. - expect(result.message).toContain('3338.1'); + expect(result.message).toContain('3296.2'); }); it('is exactly one regression wide, from either side of the line', () => { @@ -1644,7 +1645,7 @@ describe('main', () => { // about the FIXTURE while the gate under test behaved correctly. The number // this case is actually about is "the report's chunk count, echoed". expect(outputs.closure_chunks).toBe(String(fixture.files.length)); - expect(outputs.closure_gzip_kb).toBe('3338.1'); + expect(outputs.closure_gzip_kb).toBe('3296.2'); }); it('exits 1 — a verdict about the BUNDLE — when over budget', () => { diff --git a/scripts/check-eager-closure-budget.mjs b/scripts/check-eager-closure-budget.mjs index 8f967b95b5..9464d536c3 100644 --- a/scripts/check-eager-closure-budget.mjs +++ b/scripts/check-eager-closure-budget.mjs @@ -417,12 +417,14 @@ import { isEntrypoint } from './invoked-as.mjs'; * header for both builds and the three control rows that show the bytes * LEFT rather than moved. * - * Headroom above {@link BASELINE} is 26,363 bytes — 0.29x - * {@link REGRESSION_THIS_GATE_MUST_CATCH_BYTES}, on the pair objectui#11798 - * re-pinned when the Studio builder and the chart engine left the first load, - * lowering the ceiling by exactly the bytes that left (the header's - * objectui#11798 entry says why that band sits under the 0.50x design point); - * before it, the pair + * Headroom above {@link BASELINE} is 11,020 bytes — 0.12x + * {@link REGRESSION_THIS_GATE_MUST_CATCH_BYTES}, on the pair objectui#11854 + * re-pinned when the markdown highlighter and the docs-only markdown plugins + * left the first load, lowering the ceiling by exactly the bytes that left + * (the header's objectui#11854 entry says why that band sits under the 0.50x + * design point); before it, the pair objectui#11798 re-pinned when the Studio + * builder and the chart engine left the first load carried 26,363 bytes + * (0.29x), the pair * objectui#11717 re-pinned under the ruling recorded in the header's fourth * raise carried 45,972 bytes (0.50x), and the pair objectui#11438 re-pinned * when the `@objectstack/*` 17.6.0 bump paid back the rest of the spec-root @@ -930,8 +932,81 @@ import { isEntrypoint } from './invoked-as.mjs'; * the budgeted chunks moved by at most 2 bytes across the two builds, so * {@link PER_CHUNK_GZIP_CEILINGS} and {@link PER_CHUNK_BASELINE} stand. Not * {@link REGRESSION_THIS_GATE_MUST_CATCH_BYTES}. No exemption was added. + * + * ## ⭐ LOWERED WHEN THE MARKDOWN HIGHLIGHTER LEFT THE FIRST LOAD (objectui#11854) + * + * From 3,444,622 to 3,386,364: down by 58,258 bytes, exactly what + * objectui#11854 took out of the closure. Lowered by the amount recovered, as + * the objectui#11798 entry above was: ⛔ not re-derived as the new reading plus + * half a regression, which would also have absorbed `main`'s drift since the + * `c1d378ec` reading (a raise under another name), and ⛔ not rounded. + * + * WHAT LEFT — two console builds, one container, one instrument, each under + * `scripts/pm/os-verify-lock.sh`: the console's own + * `apps/console/vite.config.ts` run through Vite's `build()` with one read-only + * module-graph dump appended, reading the `eager-closure.json` the build + * writes. `9cb4e29` is `main`; `406f760` is that tree plus the chunk-group + * change to `vendor-markdown` in `apps/console/vite.config.ts`, and nothing + * else. Gzipped bytes: + * + * | chunk | `9cb4e29` | `406f760` | delta | + * |-------------------------------|-----------:|-----------:|--------:| + * | `vendor-markdown` | 163,717 | 104,286 | -59,431 | + * | `vendor-react-markdown`, new | — | 1,144 | +1,144 | + * | `ui-components` | 284,424 | 284,433 | +9 | + * | everything else | 2,985,461 | 2,985,481 | +20 | + * | ⇒ aggregate | 3,433,602 | 3,375,344 | -58,258 | + * + * The builds weigh 333 of 2456 chunks (`9cb4e29`) and 334 of 2457 + * (`406f760`); the one new chunk is `vendor-react-markdown`, and it is eager. + * The +20 is import bookkeeping spread over 19 chunks, none by more than 5, + * ⛔ not attributed further; every `vendor-icon-*` chunk is unmoved. + * + * - THE CAPTURE. On `9cb4e29` `vendor-markdown` held 296 modules, and 53 of + * them no static import from the entry reaches: `highlight.js` (39 of the + * 53, most of the bytes), `lowlight`, `rehype-highlight`, `rehype-slug`, + * `github-slugger`, `rehype-autolink-headings`, + * `remark-github-blockquote-alert`, the five hast and unist helpers they + * import, and `react-markdown`. The chunk was eager for the other 243, + * which the chat message renderer reaches, so the group's capture made all + * 296 eager. With `tags: ['$initial']` the group holds the 243 alone (the + * comment on the group in `apps/console/vite.config.ts` says why), and 52 + * of the 53 follow their importer into `plugin-markdown`'s lazy chunk, + * which went from 4,342 to 62,888 gzipped bytes across the two builds. + * - `react-markdown`, the 53rd, stays eager in the new chunk: `MarkdownContent` + * in `packages/fields` imports it statically, and that widget sits in the + * eager `ui-components` chunk although only `React.lazy` reaches it (the + * objectui#5325 co-tenancy, recorded in + * `scripts/vite-ineffective-dynamic-imports.ts`). A chunk of its own keeps + * its bytes off the budgeted `ui-components` line, which would otherwise + * take it by its own capture. + * + * ⛔ What it did NOT recover. The 243 modules left in `vendor-markdown` are the + * pipeline the chat message renderer runs on every page: `streamdown` imports + * `remark-parse`, `remark-gfm`, `remark-rehype`, `rehype-raw`, + * `rehype-sanitize`, `rehype-harden`, `unified` and `hast-util-to-jsx-runtime` + * statically, and `plugin-chatbot`, which imports `streamdown` statically, is + * eager. Moving them would lazy-load a module the first load reaches, which is + * objectui#11798's question under the objectui#6795 ruling, ⛔ not a chunk + * group's. + * + * Headroom 11,020 bytes = 0.12x {@link REGRESSION_THIS_GATE_MUST_CATCH_BYTES} + * over {@link BASELINE}: exactly the headroom `main` had on `9cb4e29` before + * this change (3,444,622 − 3,433,602), because the ceiling came down by the + * aggregate's own delta. It sits under the 0.29x the objectui#11798 entry left + * because `9cb4e29` weighed 15,343 bytes more than the `c1d378ec` reading that + * entry was sized on — `main`'s drift since, ⛔ not attributed here. No build + * that passed before this edit and measures under 3,386,364 fails after it, + * and the `9cb4e29` build, which is this change reverted, lands over it. + * + * ⛔ What moved with it, and what did not. None of the four per-chunk rows: + * `ui-components` moved by 9 bytes across the two builds and the other three + * budgeted chunks by none, so {@link PER_CHUNK_GZIP_CEILINGS} and + * {@link PER_CHUNK_BASELINE} stand; `vendor-markdown` has no row and gets + * none. Not {@link REGRESSION_THIS_GATE_MUST_CATCH_BYTES}. No exemption was + * added. */ -export const MAX_EAGER_CLOSURE_GZIP_BYTES = 3_444_622; +export const MAX_EAGER_CLOSURE_GZIP_BYTES = 3_386_364; /** * The measurement the ceiling above was derived from. Exported so the two @@ -944,6 +1019,25 @@ export const BASELINE = Object.freeze({ /** * `emitEagerClosureReport`'s `eagerGzipBytes` on this commit. * + * `406f760` is objectui#11854's branch commit that narrows `vendor-markdown` + * to what the first load reaches: `main` at `9cb4e29` plus that one + * chunk-group change in `apps/console/vite.config.ts`, re-pinned with the + * ceiling it was lowered with (the objectui#11854 entry on + * {@link MAX_EAGER_CLOSURE_GZIP_BYTES} carries its table and its `9cb4e29` + * control, ⛔ not restated here). It is a branch commit for the reason the + * paragraphs below give for `c1d378ec`: the pull request's own diff is what + * moved the figure. Measured by the console's own + * `apps/console/vite.config.ts` run through Vite's `build()` with one + * read-only module-graph dump appended, reading the `eager-closure.json` the + * build writes, under `scripts/pm/os-verify-lock.sh`, in the same container + * as the `9cb4e29` control build. The commits after it on the branch (this + * re-pin, its unit test and the changeset) reach no bundler input, so they + * cannot move the figure it pins. + * + * ⚠️ The paragraph below describes the reading this one superseded, the + * previous baseline `c1d378ec` (3,418,259 across 333 of 2456 chunks), kept as + * its provenance. + * * `c1d378ec` is objectui#11798's branch tip: `main` at `455c6466` merged into * the branch that takes the Studio builder and the chart engine out of the * first load, re-pinned with the ceiling it was lowered with (the @@ -1026,10 +1120,10 @@ export const BASELINE = Object.freeze({ * and not from `main` once it is squash-merged — the dead end objectui#9355 * added `squashMerge` below to route around. */ - gzipBytes: 3_418_259, - chunks: 333, - totalChunks: 2456, - commit: 'c1d378ec', + gzipBytes: 3_375_344, + chunks: 334, + totalChunks: 2457, + commit: '406f760', /** * The squash merge that carried the reading above onto `main`, recorded when @@ -1038,7 +1132,7 @@ export const BASELINE = Object.freeze({ * measurement gets a handle rather than a dead end. * * `null` since objectui#10996, and `null` again after objectui#11101, - * objectui#11438, objectui#11717 and objectui#11798, deliberately. This field can only ever be BACK-FILLED: a squash sha does + * objectui#11438, objectui#11717, objectui#11798 and objectui#11854, deliberately. This field can only ever be BACK-FILLED: a squash sha does * not exist until the pull request merges, so the change that re-pins the * field above ⛔ cannot write its own here. ⛔ Do not guess one: a wrong sha in * this position is worse than an absent one, because it RESOLVES, and a @@ -1714,8 +1808,12 @@ export const PER_CHUNK_GZIP_CEILINGS = Object.freeze({ * objectui#11717's `c1e32e96`, the 17.7.0 bump's build, re-pinned both, as * objectui#11438's `2ba091c`, objectui#11101's `4acbea07` and objectui#11073's * `048e7f6` had before it. objectui#11798 re-pinned the aggregate onto - * BASELINE's `c1d378ec` and left this key at `c1e32e96`: that chunk moved by 0 - * bytes across objectui#11798's two builds, and its ceiling did not move. The + * `c1d378ec` and left this key at `c1e32e96`: that chunk moved by 0 + * bytes across objectui#11798's two builds, and its ceiling did not move. + * objectui#11854 re-pinned the aggregate onto BASELINE's `406f760` and left + * every key where it was: `vendor-objectstack`, `framework` and + * `i18n-locale-en` moved by 0 bytes across objectui#11854's two builds and + * `ui-components` by 9, and no ceiling moved. The * aggregate is the later reading for every key: `c1e32e96` for * `vendor-objectstack`, `bbf6b02d9` (2026-09-13, objectui#9251) for `ui-components`, * and `3f775eeb8` for `framework`. ⚠️ `i18n-locale-en`'s commit was `755d34a5f` when it From 8027e1eeeb5d31d8a1f3891dcba71ec074a5b8d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 05:22:04 +0000 Subject: [PATCH 3/3] test(scripts): the eager-closure pins follow vendor-markdown's tag and the lowered band (objectui#11854) Two cases in `check-eager-closure-budget.test.ts` read state this change moves. The objectui#11101 control lists the exact set of console chunk groups carrying `$initial`; it now names `vendor-markdown` beside `vendor-objectstack`. The "one chunk over its ceiling while the aggregate is green" run grew one chunk without shrinking the rest, so its total rode on the aggregate headroom, and the lowered ceiling left less headroom than that chunk's overage; it now offsets the rest of the closure by the same bytes, derived from the constants, so the total stays on the baseline. The `BASELINE` docblock records the ordinary-build calibration. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude --- .../__tests__/check-eager-closure-budget.test.ts | 13 ++++++++++++- scripts/check-eager-closure-budget.mjs | 5 ++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/scripts/__tests__/check-eager-closure-budget.test.ts b/scripts/__tests__/check-eager-closure-budget.test.ts index 39fdf4c6b7..fe8ed8be8a 100644 --- a/scripts/__tests__/check-eager-closure-budget.test.ts +++ b/scripts/__tests__/check-eager-closure-budget.test.ts @@ -635,8 +635,11 @@ describe('chunk attribution (objectui#7399)', () => { expect(group!.options).toContain(`tags: ['$initial']`); // The control: the parse reads the tag off THIS group, not off every // group — a tail-blind or table-wide match would satisfy the line above. + // objectui#11854 tags `vendor-markdown` the same way, for the same + // reason, so the exact set names both, in table order. expect(groups.filter((g) => g.options.includes('$initial')).map((g) => g.name)).toEqual([ 'vendor-objectstack', + 'vendor-markdown', ]); }); @@ -1738,8 +1741,16 @@ describe('main', () => { * past its own line. Before objectui#5490 that run exited 0. */ it('exits 1 when one chunk is over its ceiling while the aggregate is green', () => { + // The chunk grows past its line while the rest of the closure shrinks by + // the same bytes, so the TOTAL stays on the baseline — the shape the + // per-chunk header names: one chunk grows by the whole allowance while the + // others shrink. Without the offset the total rode on the aggregate + // headroom, and when objectui#11854 lowered the aggregate ceiling the + // chunk's overage alone was larger than that headroom, so the aggregate + // went red too and the run stopped testing what its name says. + const over = PER_CHUNK_GZIP_CEILINGS['vendor-objectstack'] + 1; const { code, outputs } = run( - budgeted({ 'vendor-objectstack': PER_CHUNK_GZIP_CEILINGS['vendor-objectstack'] + 1 }), + budgeted({ 'vendor-objectstack': over }, -(over - PER_CHUNK_BASELINE['vendor-objectstack'])), ); expect(code).toBe(1); expect(outputs.closure_status).toBe('pass'); diff --git a/scripts/check-eager-closure-budget.mjs b/scripts/check-eager-closure-budget.mjs index 9464d536c3..769b4baf3b 100644 --- a/scripts/check-eager-closure-budget.mjs +++ b/scripts/check-eager-closure-budget.mjs @@ -1030,7 +1030,10 @@ export const BASELINE = Object.freeze({ * `apps/console/vite.config.ts` run through Vite's `build()` with one * read-only module-graph dump appended, reading the `eager-closure.json` the * build writes, under `scripts/pm/os-verify-lock.sh`, in the same container - * as the `9cb4e29` control build. The commits after it on the branch (this + * as the `9cb4e29` control build. The console's ordinary build of `57a1066` + * (`CI=true pnpm exec turbo run build --filter=@object-ui/console... + * --concurrency=2`, exit 0) wrote the same figure to the byte, which is this + * instrument's calibration. The commits after `406f760` on the branch (this * re-pin, its unit test and the changeset) reach no bundler input, so they * cannot move the figure it pins. *