Keep shiki and the empty state off the markdown page's first load
Fixes the base red in scripts/check-webviews-diff-budget.mjs: the markdown page's eager JS was over its 1,400,000-byte budget at 1,407,890 bytes, so ci-web skipped every step after the budget check, including the webview and browser tests. The budget stays the same.
Byte counts (node scripts/check-webviews-diff-budget.mjs, local build)
| surface | before (base 86efbba) | after | budget |
|---|---|---|---|
| markdown page | 1,407,890 | 1,118,864 | 1,400,000 |
| diff surface | 1,490,877 | 1,424,935 | 1,500,000 |
| diff page | 1,481,734 | 1,415,792 | 1,500,000 |
| diff worker | 253,369 | 253,406 | 400,000 |
| editor page | 450,735 | 450,790 | 500,000 |
The markdown page drops 289,026 bytes. shiki-core.mjs (222,504) and pickerModel.mjs (93,379) are gone from its eager set. The new chunks pageShared.mjs (27,403) and text-utils.mjs (463) are added. The diff pages also stop loading pickerModel eagerly, since cx was the only thing they needed from it.
What changed
- shiki-core. The markdown page already loaded Shiki with
import(). It was eager only becauseccountandzwitch, which both shiki's hast serializer and the markdown serializer use, had been pulled intoshiki-core. They now have their owntext-utilschunk (463 B). The page starts fetching Shiki's core and its JS regex engine at boot, without waiting on them, so they are ready before a code block asks for colors. - pickerModel. The shared viewer chunk is split in two.
pageSharedholds appearance, the page client, i18n, ops andcx, and stays eager.pickerModelholds the empty states, the picker model and their ~60 KB string table. The markdown page imports its empty state withReact.lazy. The import starts at boot, alongside the host's config round trip that decides between a file and the empty state, so the chunk is in hand before the empty phase renders. Suspense covers the rare case where it is not. - ci-web (
react-apps-check-shard). The budget step now usescontinue-on-error. A new final step,Fail on the eager JS budget(if: !cancelled() && steps.eager-js-budget.outcome == 'failure'), fails the job when the budget fails. A budget miss no longer skips React Compiler,bun run check, the engine install, the PR-scoped rules orTest webviews. A failed bundle build still stops everything after it.
First paint
Measured on the production-built latency harness page (test/latency/markdown.html, the real markdown page on the mock host), headless, median of 15 loads, two interleaved rounds:
| text painted | code colored | CLS | |
|---|---|---|---|
| chromium before | 120–122 ms | 179–182 ms | 0.0022 |
| chromium after | 106–112 ms | 167–175 ms | 0.0022 |
| webkit before | 156–162 ms | 901–1013 ms | 0 |
| webkit after | 140–144 ms | 902–918 ms | 0 |
There is no new plain-to-colored step: highlighting was already asynchronous per grammar, and the prewarmed core keeps it as fast as before. CLS is unchanged.
Verification
node scripts/check-webviews-diff-budget.mjspasses. Numbers are above.- Lint and type check (
vp lint --type-aware --type-check) are clean, andvp fmtis clean on the touched files.bun run checkfails on base for reasons unrelated to this PR:check:type-scale(syncMotion.gallery.css) and the formatting of settings/icons.tsx. The same holds forsubtle-elevationandui-rulesinbun test.check-webviews-react-compiler.mjspasses.test_pr_scoped_webviews_checks.pypasses. bun testpasses: diff-worker-bundle, diff-label-bundle, path-picker-model, caret-selection, code-pages-terminal-font, render-fence.editor-csppasses withCMUX_BROWSER_TESTS=1, including Monaco staying lazy on the markdown page.actionlintonci-web.ymlis clean.
🤖 Generated with Claude Code