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/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. diff --git a/scripts/__tests__/check-eager-closure-budget.test.ts b/scripts/__tests__/check-eager-closure-budget.test.ts index e6f1e29657..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', ]); }); @@ -811,10 +814,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 +1648,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', () => { @@ -1737,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 8f967b95b5..769b4baf3b 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,28 @@ 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 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. + * + * ⚠️ 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 +1123,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 +1135,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 +1811,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