Keep shiki and the empty state off the markdown page's first load

Merged
TE
18d ago
+70-13

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)

surfacebefore (base 86efbba)afterbudget
markdown page1,407,8901,118,8641,400,000
diff surface1,490,8771,424,9351,500,000
diff page1,481,7341,415,7921,500,000
diff worker253,369253,406400,000
editor page450,735450,790500,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 because ccount and zwitch, which both shiki's hast serializer and the markdown serializer use, had been pulled into shiki-core. They now have their own text-utils chunk (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. pageShared holds appearance, the page client, i18n, ops and cx, and stays eager. pickerModel holds the empty states, the picker model and their ~60 KB string table. The markdown page imports its empty state with React.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 uses continue-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 or Test 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 paintedcode coloredCLS
chromium before120–122 ms179–182 ms0.0022
chromium after106–112 ms167–175 ms0.0022
webkit before156–162 ms901–1013 ms0
webkit after140–144 ms902–918 ms0

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.mjs passes. Numbers are above.
  • Lint and type check (vp lint --type-aware --type-check) are clean, and vp fmt is clean on the touched files. bun run check fails on base for reasons unrelated to this PR: check:type-scale (syncMotion.gallery.css) and the formatting of settings/icons.tsx. The same holds for subtle-elevation and ui-rules in bun test. check-webviews-react-compiler.mjs passes. test_pr_scoped_webviews_checks.py passes.
  • bun test passes: diff-worker-bundle, diff-label-bundle, path-picker-model, caret-selection, code-pages-terminal-font, render-fence. editor-csp passes with CMUX_BROWSER_TESTS=1, including Monaco staying lazy on the markdown page.
  • actionlint on ci-web.yml is clean.

🤖 Generated with Claude Code