perf(cli): code-split lowlight to cut startup V8 parse cost

Move the syntax-highlight engine out of the synchronously-parsed cli.js
entry into a separately-emitted chunk and load it via dynamic import on
the first code-block render. Until the chunk arrives, code blocks render
as plain text; the next React commit of the surrounding subtree picks up
the highlighted version, so users never see incorrect highlighting –
just an imperceptibly later transition for the very first code block.

Mechanics:
- esbuild config: switch entry to outdir + splitting:true so that
  `await import('lowlight')` produces an actual on-disk chunk that's
  only parsed by V8 when first needed.
- esbuild-shims: rename injected __dirname/__filename to qwen-prefixed
  symbols + use `define` to redirect free references. Previous inject
  collided with vendored libraries (yargs) that ship their own
  `var __dirname` ESM-compat polyfill once splitting flattens chunks.
- prepare-package: include the new chunks/ directory in the published
  package's files list.
- CodeColorizer: keep the public colorize{Code,Line} signatures and HAST
  rendering identical; on first call when the chunk hasn't loaded it
  returns the plain line and fires the dynamic import via a tiny
  standalone loader module.
- lowlightLoader (new): isolates the lazy-load surface to a module with
  zero transitive imports (no themeManager, settings, or core). This
  lets test-setup prime the cache without dragging the whole UI module
  graph into every test file, which was observed to perturb theme and
  settings test outcomes when CodeColorizer was imported directly.
- test-setup: await loadLowlight() once via the standalone loader so
  synchronous snapshot tests see the highlighted output deterministically.

Measurements (real $HOME, n=15 interleaved A/B vs main HEAD, macOS):

| Metric             | Before (mean±sd ms) | After (mean±sd ms) | Δ        | t      | p        |
| ------------------ | ------------------- | ------------------ | -------- | ------ | -------- |
| firstByte (wall)   | 1633.5 ± 88.7       | 1475.8 ± 73.3      | -157.7   | 5.31   | 1.33e-5  |
| idle (wall)        | 2048.7 ± 93.6       | 1902.3 ± 80.2      | -146.3   | 4.60   | 8.71e-5  |
| cli.js size        | 25 MB               | 6.9 MB             | -18.1 MB | —      | —        |

Both metrics clear the +50ms-or-10% Welch's t-test bar by an order of
magnitude. cli.js drops 72%; total payload (cli.js + chunks/) is
similar but only cli.js is parsed at module-eval time, which is the
phase that dominates the user-visible startup gap.

How to validate:
  npm run bundle
  ls dist/                         # cli.js + chunks/lowlight-*.js
  node dist/cli.js -y              # interactive UI still renders

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
秦奇 2026-05-11 21:07:47 +08:00
parent 51ee87539c
commit c201ba2fcb
7 changed files with 107 additions and 17 deletions

View file

@ -39,6 +39,7 @@ const TARGETS = new Map([
const DIST_REQUIRED_PATHS = ['cli.js', 'vendor', 'bundled/qc-helper/docs'];
const DIST_ALLOWED_ENTRIES = new Set([
'cli.js',
'chunks',
'vendor',
'bundled',
'package.json',

View file

@ -5,25 +5,27 @@
*/
/**
* Shims for esbuild ESM bundles to support require() calls
* This file is injected into the bundle via esbuild's inject option
* Shims for esbuild ESM bundles.
*
* With code-splitting enabled, the inject is applied per-chunk and the
* exported bindings cannot collide with `var __dirname` polyfills that
* vendored libraries (e.g. yargs) emit in their own ESM compat layers.
* To stay collision-free, this file exposes prefixed names; the build
* config uses esbuild `define` to rewrite free `__dirname` / `__filename`
* references in source to these prefixed identifiers, while leaving
* vendor-declared locals untouched.
*/
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
// Create require function for the current module and make it global
const _require = createRequire(import.meta.url);
// Make require available globally for dynamic requires
if (typeof globalThis.require === 'undefined') {
globalThis.require = _require;
}
// Export for esbuild injection
export const require = _require;
// Setup __filename and __dirname for compatibility
export const __filename = fileURLToPath(import.meta.url);
export const __dirname = dirname(__filename);
export const __qwen_filename = fileURLToPath(import.meta.url);
export const __qwen_dirname = dirname(__qwen_filename);

View file

@ -159,6 +159,7 @@ const distPackageJson = {
},
files: [
'cli.js',
'chunks',
'vendor',
'*.sb',
'README.md',