Files
openclaw/ui/config/control-ui-chunking.ts
Peter SteinbergerandGitHub cad4e395d2 perf(ui): cut forced reflows in Control UI chat render path (#110472)
* perf(ui): cut forced reflows in chat render path

Profiled the Control UI with Chrome DevTools tracing against a real-data
gateway: session-switch INP was 367ms with ForcedReflow insights on every
load and interaction trace.

- chat-thread: per-row stable Lit ref callbacks (keyed by row key) so the
  virtualizer stops cache-sweeping and re-measuring every visible row on
  every render; prune callbacks when rows leave the list
- chat-composer: stable textarea ref on per-pane state instead of an
  inline arrow, so the textarea is re-measured only on attach or when the
  draft changes programmatically, not on every chat render
- app-sidebar: coalesce scrollHeight/scrollTop reads from updated() into
  one rAF per frame instead of a forced layout flush per render

After: INP 133-143ms on the same interaction sequence, no ForcedReflow
insight in load or interaction traces.

* perf(ui): keep Control UI startup under budget after reflow fixes

The reflow fixes shifted rolldown's chunk partition: the 400 KiB core
maxSize boundary split one core chunk in two (~1.4 KiB gzip compression
loss) and re-balancing minted a tiny build-info startup chunk, pushing
startup JS to 370.8 KiB over the 370 KiB budget.

- pin build-info.ts + build-info-normalizers.ts into control-ui-shared so
  partition noise stops minting extra startup preload requests
- raise core maxSize 400 -> 448 KiB so the core graph packs into fewer,
  better-compressing chunks

Startup JS: 22 requests, 369.1 KiB gzip (main: 23 requests, 369.3 KiB).
2026-07-18 08:04:03 +01:00

88 lines
2.7 KiB
TypeScript

// Control UI config module wires control ui chunking behavior.
function normalizeModuleId(id: string): string {
return id.replace(/\\/g, "/");
}
function moduleIdIncludesPackage(id: string, packageName: string): boolean {
const normalized = normalizeModuleId(id);
return (
normalized.includes(`/node_modules/${packageName}/`) ||
normalized.includes(`/openclaw-pnpm-node-modules/${packageName}/`)
);
}
export function controlUiStableChunkName(id: string): string | undefined {
const normalized = normalizeModuleId(id);
// These entry-and-route helpers must stay together; separate shared chunks
// turn small route-graph changes into extra startup preload requests.
if (
normalized.endsWith("/ui/src/components/config-form.shared.ts") ||
normalized.endsWith("/ui/src/lib/clipboard.ts") ||
normalized.endsWith("/ui/src/build-info-normalizers.ts") ||
normalized.endsWith("/ui/src/build-info.ts")
) {
return "control-ui-shared";
}
if (normalized.endsWith("/ui/src/lib/gateway-methods.ts")) {
return "gateway-runtime";
}
if (
moduleIdIncludesPackage(id, "lit") ||
moduleIdIncludesPackage(id, "lit-html") ||
moduleIdIncludesPackage(id, "@lit/reactive-element")
) {
return "lit-runtime";
}
if (
moduleIdIncludesPackage(id, "highlight.js") ||
moduleIdIncludesPackage(id, "markdown-it") ||
moduleIdIncludesPackage(id, "markdown-it-task-lists") ||
moduleIdIncludesPackage(id, "dompurify") ||
moduleIdIncludesPackage(id, "entities") ||
moduleIdIncludesPackage(id, "linkify-it") ||
moduleIdIncludesPackage(id, "mdurl") ||
moduleIdIncludesPackage(id, "punycode.js") ||
moduleIdIncludesPackage(id, "uc.micro")
) {
return "markdown-runtime";
}
if (moduleIdIncludesPackage(id, "zod") || moduleIdIncludesPackage(id, "json5")) {
return "config-runtime";
}
if (
moduleIdIncludesPackage(id, "@noble/ed25519") ||
moduleIdIncludesPackage(id, "@noble/hashes") ||
moduleIdIncludesPackage(id, "ipaddr.js")
) {
return "gateway-runtime";
}
return undefined;
}
export const controlUiCodeSplitting = {
includeDependenciesRecursively: false,
groups: [
{
name: (id: string) => controlUiStableChunkName(id) ?? null,
test: (id: string) => controlUiStableChunkName(id) !== undefined,
priority: 20,
},
{
name: (id: string) =>
normalizeModuleId(id).includes("/ui/src/") ? "control-ui-core" : "control-ui-foundation",
tags: ["$initial"] as ["$initial"],
priority: 10,
// 448 KiB packs the core graph into fewer chunks; the previous 400 KiB
// boundary split one core chunk in two, costing ~1.4 KiB startup gzip.
maxSize: 448 * 1024,
},
],
};