Status: Complete - Milestone: M41 (yune-web startup optimization) - Created: 2026-06-26 - Type: browser-harness performance plan
For agentic workers: REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-development(recommended) orsuperpowers:executing-plansto implement this plan task-by-task. Steps use checkbox (- [x]) syntax for tracking.
Goal: Reduce real-browser startup latency for the tracked
apps/yune-web/ harness using fresh post-M40 evidence, without making native
engine-performance claims from browser data or regressing typing behavior.
Architecture: M41 is a browser harness milestone, not an engine milestone.
It starts with a production-build baseline that splits UI shell, asset
delivery/cache, worker/WASM startup, virtual filesystem/persistence, deploy
reuse, schema selection, first key-to-paint, and browser memory. Optimization
then targets the measured top owner only, with luna_pinyin and
jyut6ping3_mobile kept as separate browser scenarios.
Tech Stack: apps/yune-web/ Vite/React app, apps/yune-web/src/worker.ts,
apps/yune-web/src/rime.ts, apps/yune-web/src/yune-integration/*,
apps/yune-web/public-demo/, Playwright, Chromium CDP metrics, browser
Performance APIs, Windows process working-set sampling, and evidence under
apps/yune-web/e2e/results/m41-yune-web-startup-optimization/.
M40 closes the native Track A long-row owner, but it intentionally does not claim web startup or public-demo speed. The root-cause report now records the next work boundary: web harness startup must be measured and optimized as a separate browser/application problem.
Earlier browser evidence suggests startup pain can come from browser shell, asset delivery, worker/WASM startup, cache behavior, virtual filesystem, schema deploy/reuse, and schema selection. The next milestone must not guess. It must first produce a post-M40 browser baseline and then optimize the largest measured owner.
- Work in the tracked
apps/yune-web/app. Do not editapps/yune-web/source/or regenerate the retired patch unless a plan task explicitly changes that rule. - Do not touch
crates/orpackages/unless the browser evidence proves the top owner is inside the runtime/engine boundary and the plan is updated with a narrow justification. - Keep
yune-webvisible naming and public-demo identity intact. - AI remains default-off, local-only, classic-first, and outside startup optimization unless a failing startup test proves the AI path is active.
- Native M40 numbers are an engine baseline only. M41 claims require real-browser evidence.
M41-YWEB-01(fresh baseline): final evidence includes post-M40 production baselines for trackedapps/yune-webbuild and public-demo build, with real-worker and mock-worker modes.M41-YWEB-02(scenario coverage): baseline and final runs includeluna_pinyinandjyut6ping3_mobilestartup scenarios. Track A typing rows includehao,ni,zhongguo,ceshiyixiachangjushuruxingnengzenyang,zhegeyinqingqishiyinggaizhichichaochangjuzishurucainengyong,cszysmsrsd, andzybfshmsru. Track B rows includehai,ngo,caksi,sihaacoenggeoisyujapgecukdou, andtaihaajyugwodaahoucoenggegeoizigosingnangwuidimjoeng.M41-YWEB-03(owner attribution): startup is split into browser shell, resource transfer, worker script load, WASM fetch/instantiate, virtual filesystem/persistence, asset cache, schema deploy/reuse, schema select, and React ready-to-input.M41-YWEB-04(measured optimization): the final implementation reduces the measured top startup owner by at least50%, or records a measured blocker and reduces total ready-to-input by at least30%on the affected scenario.M41-YWEB-05(cache correctness): anyCache.putor browser cache failure is fixed or explicitly proven absent; the public-demo build must not be blocked by a cache exception.M41-YWEB-06(startup budgets): final cold real-worker ready-to-input medians are no worse than1.5sforluna_pinyinand3.0sforjyut6ping3_mobile, or the final report records why the budget is not yet reachable and names the remaining owner. Warm reload and warm new-page medians must improve from baseline.M41-YWEB-07(typing no-regression): first keydown-to-paint after ready does not regress more than10%for required Track A and Track B rows; incomplete pinyin rows are reported as behavior probes if they export no candidates.M41-YWEB-08(memory no-regression): Chromium JS heap, DOM/node counts,performance.measureUserAgentSpecificMemory()when available, and Windows process working set are recorded before/after/peak. Final memory must not regress more than10%unless the owner is attributed and accepted.M41-YWEB-09(public-demo honesty): public-demo evidence covers asset manifest, cache headers, service/cache behavior, startup readiness, and first typing. It does not claim native engine speed.M41-YWEB-10(quality gates): final closeout passesnpm.cmd --prefix apps/yune-web run build,npm.cmd --prefix apps/yune-web run build:public,npm.cmd --prefix apps/yune-web run typecheck, the M41 startup benchmark, focused browser smoke, andgit diff --check. Rust gates are required only if M41 touches Rust.
apps/yune-web/e2e/yune-web-startup-benchmark.spec.ts: create a focused Playwright startup benchmark separate from the broad smoke spec.apps/yune-web/e2e/startup-benchmark/: create if helper modules are needed for scenario definitions, browser memory sampling, resource aggregation, and dashboard generation.apps/yune-web/e2e/results/m41-yune-web-startup-optimization/: raw JSON, CSV, screenshots, dashboard markdown, and SVG visualizations.apps/yune-web/src/worker.ts: optimize only measured worker, asset, cache, deploy, or schema-selection owners.apps/yune-web/src/rime.ts: optimize only measured main-thread/worker queue, ready-state, clear-cache, or first-key-to-paint owners.apps/yune-web/src/yune-integration/adapter.ts: optimize only measured deploy/reuse, persistence, or filesystem owners.apps/yune-web/src/yune-integration/assets.ts: optimize only measured asset loading/validation owners.apps/yune-web/public-demo/*: optimize only measured public-demo packaging, asset, cache-header, or manifest owners.docs/reports/yune-vs-librime-performance.mdanddocs/reports/yune-vs-librime-root-cause-analysis.md: preserve the native engine boundary; add only a pointer to M41 browser evidence if needed.docs/roadmap.md,docs/requirements.md,docs/decisions.md, anddocs/ledgers/milestone-history.md: update on closeout.
Files:
-
Create:
apps/yune-web/e2e/yune-web-startup-benchmark.spec.ts -
Create:
apps/yune-web/e2e/startup-benchmark/scenarios.ts -
Create:
apps/yune-web/e2e/startup-benchmark/metrics.ts -
Create:
apps/yune-web/e2e/results/m41-yune-web-startup-optimization/phase-0-baseline/ -
Step 0.1: Define benchmark scenarios
Create apps/yune-web/e2e/startup-benchmark/scenarios.ts with:
export type StartupSchema = "luna_pinyin" | "jyut6ping3_mobile";
export type StartupMode =
| "real-worker-cold"
| "real-worker-warm-reload"
| "real-worker-warm-new-page"
| "mock-worker-cold"
| "mock-worker-warm";
export interface StartupScenario {
id: string;
schema: StartupSchema;
mode: StartupMode;
publicDemo: boolean;
samples: number;
inputs: string[];
}
export const trackAInputs = [
"hao",
"ni",
"zhongguo",
"ceshiyixiachangjushuruxingnengzenyang",
"zhegeyinqingqishiyinggaizhichichaochangjuzishurucainengyong",
"cszysmsrsd",
"zybfshmsru",
] as const;
export const trackBInputs = [
"hai",
"ngo",
"caksi",
"sihaacoenggeoisyujapgecukdou",
"taihaajyugwodaahoucoenggegeoizigosingnangwuidimjoeng",
] as const;
export const startupScenarios: StartupScenario[] = [
{ id: "tracked-luna-cold", schema: "luna_pinyin", mode: "real-worker-cold", publicDemo: false, samples: 10, inputs: [...trackAInputs] },
{ id: "tracked-luna-warm-reload", schema: "luna_pinyin", mode: "real-worker-warm-reload", publicDemo: false, samples: 20, inputs: [...trackAInputs] },
{ id: "tracked-luna-warm-new-page", schema: "luna_pinyin", mode: "real-worker-warm-new-page", publicDemo: false, samples: 20, inputs: [...trackAInputs] },
{ id: "tracked-jyut-cold", schema: "jyut6ping3_mobile", mode: "real-worker-cold", publicDemo: false, samples: 10, inputs: [...trackBInputs] },
{ id: "tracked-jyut-warm-reload", schema: "jyut6ping3_mobile", mode: "real-worker-warm-reload", publicDemo: false, samples: 20, inputs: [...trackBInputs] },
{ id: "tracked-jyut-warm-new-page", schema: "jyut6ping3_mobile", mode: "real-worker-warm-new-page", publicDemo: false, samples: 20, inputs: [...trackBInputs] },
{ id: "tracked-mock-cold", schema: "luna_pinyin", mode: "mock-worker-cold", publicDemo: false, samples: 10, inputs: ["hao"] },
{ id: "tracked-mock-warm", schema: "luna_pinyin", mode: "mock-worker-warm", publicDemo: false, samples: 20, inputs: ["hao"] },
{ id: "public-luna-cold", schema: "luna_pinyin", mode: "real-worker-cold", publicDemo: true, samples: 10, inputs: [...trackAInputs] },
{ id: "public-jyut-cold", schema: "jyut6ping3_mobile", mode: "real-worker-cold", publicDemo: true, samples: 10, inputs: [...trackBInputs] },
];Expected: scenario definitions make both schema tracks explicit.
- Step 0.2: Capture startup metrics
Create apps/yune-web/e2e/startup-benchmark/metrics.ts with helpers that
collect:
export interface StartupSample {
scenarioId: string;
sampleIndex: number;
url: string;
schema: string;
mode: string;
readyToInputMs: number;
startupCompleteMs?: number;
phases: Array<{ phase: string; ms: number }>;
navigation: Record<string, number>;
resources: Array<{ name: string; initiatorType: string; transferSize: number; encodedBodySize: number; decodedBodySize: number; duration: number }>;
cache: { hits: number; misses: number; errors: string[] };
storageEstimate?: { usage?: number; quota?: number };
browserMemory?: Record<string, number>;
firstKeyToPaint: Array<{ input: string; ms: number; candidateCount?: number }>;
}Expected: the runner can serialize bounded JSON and CSV for every sample.
- Step 0.3: Add mock-worker mode
In the Playwright spec, intercept worker.js for mock-worker-* scenarios and
serve a tiny worker that responds to startup, schema, and process-key calls
without loading WASM:
self.postMessage({
type: "listener",
name: "persistenceDiagnostic",
args: [{
source: "yune-startup",
marker: { phase: "startup:complete", totalMs: 1, markers: [{ phase: "mock-worker:start", ms: 0 }] }
}]
});
self.onmessage = (event) => {
const { name } = event.data;
self.postMessage({
type: "success",
result: name === "processKey" ? { handled: true, candidates: [], context: { composition: "" } } : true,
elapsedMs: 0,
});
};Expected: mock-worker startup isolates React/browser shell from worker/WASM and asset costs.
- Step 0.4: Run phase 0 baseline
Run:
npm.cmd --prefix apps\yune-web run build
npm.cmd --prefix apps\yune-web run build:public
npm.cmd --prefix apps\yune-web\e2e run test:e2e -- --grep "M41 STARTUP" --workers=1Expected: raw baseline JSON/CSV and a baseline dashboard are written under
phase-0-baseline/.
Files:
-
Modify:
apps/yune-web/e2e/startup-benchmark/metrics.ts -
Create:
apps/yune-web/e2e/results/m41-yune-web-startup-optimization/phase-1-owner/owner-summary.md -
Step 1.1: Aggregate owner buckets
Compute median and p95 for:
- browser shell without worker;
- worker script load;
- WASM JS and binary resource transfer;
- WASM instantiate/runtime init;
- schema asset transfer by file group;
- cache hit/miss/error counts;
- virtual filesystem/persistence sync;
- deploy cache hit/miss;
- schema selection;
- ready-to-input;
- first keydown-to-paint;
- memory before/after/peak.
Expected: one top current owner is named per scenario group.
- Step 1.2: Decide optimization order
Write phase-1-owner/owner-summary.md with:
# M41 Phase 1 Owner Summary
| Scenario group | Top owner | Median | p95 | Planned fix |
| --- | ---: | ---: | ---: | --- |
| tracked luna cold | ... | ... | ... | ... |
| tracked jyut cold | ... | ... | ... | ... |
| public luna cold | ... | ... | ... | ... |
| public jyut cold | ... | ... | ... | ... |
Decision: optimize <owner> first because it is the largest repeated blocker.Expected: no implementation begins until this owner table exists.
Files:
-
Modify:
apps/yune-web/src/worker.ts -
Modify:
apps/yune-web/public-demo/build.mjsorbuild.ps1only if the public-demo manifest/header owner is measured. -
Modify:
apps/yune-web/e2e/yune-web-startup-benchmark.spec.ts -
Step 2.1: Make cache writes safe
If phase 1 records Cache.put errors, update cachePublicAsset in
apps/yune-web/src/worker.ts so it only caches successful same-origin basic
responses and records a diagnostic instead of blocking startup:
if (!response.ok || response.type === "opaque") {
dispatch("persistenceDiagnostic", {
source: "yune-startup",
marker: { phase: "asset-cache:skip", path, status: response.status, responseType: response.type },
});
return responseAssetContent(response, path);
}
try {
await cache.put(cacheRequest, response.clone());
} catch (error) {
dispatch("persistenceDiagnostic", {
source: "yune-startup",
marker: { phase: "asset-cache:error", path, error: error instanceof Error ? error.message : String(error) },
});
}Expected: cache failures never block startup or public-demo readiness.
- Step 2.2: Reduce transferred startup assets
If resource attribution shows non-active schema assets dominate cold startup, change startup loading so only the active schema and minimal reachable shared dependencies load before ready-to-input. Defer non-active schema assets until schema switch.
Expected: final resource table shows fewer startup bytes for the affected schema without breaking schema switch smoke.
Files:
-
Modify:
apps/yune-web/src/worker.ts -
Modify:
apps/yune-web/src/yune-integration/adapter.ts -
Modify:
apps/yune-web/src/yune-integration/assets.ts -
Modify:
apps/yune-web/src/rime.ts -
Step 3.1: Preserve deployed reuse
If deploy cache miss dominates warm startup, change the startup path so warm reload/new-page reuse deployed assets when signatures match and skips schema deploy work.
Expected: warm scenarios record deploy:cache-hit and reduced
ready-to-input.
- Step 3.2: Split shell-ready from engine-ready without faking input
If React shell blocks while the worker initializes, expose a separate shell ready marker but keep input disabled until engine ready:
window.dispatchEvent(new CustomEvent("yune:shell-ready", { detail: { at: performance.now() } }));Expected: reports can distinguish visual shell paint from true ready-to-input.
Files:
-
Modify:
apps/yune-web/e2e/yune-web-startup-benchmark.spec.ts -
Modify: app files touched by Tasks 2-3 if first-key-to-paint regresses.
-
Step 4.1: Compare first key-to-paint
For each required input, type after ready and record:
- keydown timestamp;
- worker queue wait;
- worker processing;
- main-thread response mapping;
- React state update;
- paint proxy;
- candidate count.
Expected: no required row regresses more than 10% from phase 0 unless the
owner is named and fixed.
- Step 4.2: Compare browser memory
Record:
- CDP
Performance.getMetrics; - DOM node counts;
- JS heap used/total;
performance.measureUserAgentSpecificMemory()when available;- Windows process working set keyed to the Playwright user-data-dir.
Expected: memory no-regression gate is backed by data.
Files:
-
Create:
apps/yune-web/e2e/results/m41-yune-web-startup-optimization/phase-4-final/ -
Create:
apps/yune-web/e2e/results/m41-yune-web-startup-optimization/final-gates.md -
Modify:
docs/roadmap.md -
Modify:
docs/requirements.md -
Modify:
docs/decisions.md -
Modify:
docs/ledgers/milestone-history.md -
Move on closeout:
docs/plans/active/m41-plan-yune-web-startup-optimization.mdtodocs/plans/completed/m41-plan-yune-web-startup-optimization.md -
Step 5.1: Run final gates
Run:
npm.cmd --prefix apps\yune-web run build
npm.cmd --prefix apps\yune-web run build:public
npm.cmd --prefix apps\yune-web run typecheck
npm.cmd --prefix apps\yune-web\e2e run test:e2e -- --grep "M41 STARTUP" --workers=1
npm.cmd --prefix apps\yune-web\e2e run test:e2e -- --grep "Composition after typing schema-valid keys|Candidate list visible" --workers=1
git diff --checkExpected: all pass. Run Rust gates only if Rust files changed.
- Step 5.2: Write final dashboard
Write final-gates.md with:
- commit SHA and dirty state;
- build mode and URL;
- browser version;
- sample counts;
- cold/warm/mock rows;
- startup owner stacked chart;
- asset transfer/cache table;
- memory before/after/peak table;
- first-key-to-paint table;
- top-owner verdict and final remaining owner.
Expected: a future session can decide the next optimization from the dashboard without reading raw JSON first.
M41 is complete. It closed the tracked apps/yune-web/ startup milestone as a
browser-harness optimization, not a native engine claim.
Final evidence:
- Final gates:
apps/yune-web/e2e/results/m41-yune-web-startup-optimization/final-gates.md - Phase 0 owner baseline:
apps/yune-web/e2e/results/m41-yune-web-startup-optimization/phase-0-one-sample/ - Phase 1 owner summary:
apps/yune-web/e2e/results/m41-yune-web-startup-optimization/phase-1-owner/owner-summary.md - Final full production-browser run:
apps/yune-web/e2e/results/m41-yune-web-startup-optimization/phase-7-final-full/
Measured top owner:
- Phase 0 showed the production runtime packaging was incomplete and the
post-startup React path was paying an unnecessary default customize/deploy
action. A focused trace measured that deploy action around
15,538 msonjyut6ping3_mobile. - Final M41 avoids that path by packaging runtime artifacts, starting the worker with the selected schema, skipping no-op default deploy preferences, and schema-scoping startup assets.
Final production-browser medians:
| Scenario | Phase 0 ready median | Final ready median | Improvement |
|---|---|---|---|
| tracked luna cold | 3,115 ms |
846 ms |
72.8% |
| tracked luna warm reload | 2,399 ms |
266 ms |
88.9% |
| tracked luna warm new page | 2,438 ms |
306 ms |
87.4% |
| tracked jyut cold | 17,041 ms |
1,254 ms |
92.6% |
| tracked jyut warm reload | 15,783 ms |
654 ms |
95.9% |
| tracked jyut warm new page | 16,081 ms |
704 ms |
95.6% |
| public luna cold | 3,119 ms |
867 ms |
72.2% |
| public jyut cold | 16,872 ms |
1,291 ms |
92.3% |
Remaining owners:
luna_pinyincold startup is now mostly browser/React shell residual around600 ms, similar to the mock-worker shell floor.jyut6ping3_mobilecold startup is now mostly worker/schema initialization around600-700 ms.- Jyutping still transfers a large local encoded asset footprint around
34.9 MB, but the payload no longer creates the old15 sstartup delay. - First-key metrics are captured for all required rows. The final cold rows
remain interactive, with tracked cold p95 no worse than
235 msacross the required typed inputs. The phase-0 first-key baseline is a one-sample owner run, so it is retained as diagnostic context rather than a statistically strong first-key regression baseline.
Quality gates:
npm.cmd --prefix apps/yune-web run buildnpm.cmd --prefix apps/yune-web run build:publicnpm.cmd --prefix apps/yune-web run typechecknpm.cmd --prefix apps/yune-web/e2e run test:e2e -- --grep "M41 STARTUP" --workers=1npm.cmd --prefix apps/yune-web/e2e run test:e2e -- --grep "Composition after typing schema-valid keys|Candidate list visible|M41 default startup preserves deploy-time engine defaults" --workers=1git diff --check
Rust gates were not required because no Rust files changed.
Review follow-up: the default startup deploy skip is behavior-preserving only
if the shipped schema defaults match the app's default deploy preferences.
M41 now bakes those defaults into the shipped schema assets and guards them
with M41 default startup preserves deploy-time engine defaults.
Broad historical smoke caveat: the full historical @smoke tag is still not
the M41 closeout claim. M41 closes with the focused current composition,
candidate visibility, deploy-default, and startup benchmark gates. Remaining
legacy browser-parity rows, including the old M16 sentence/combine assertions,
now have a post-M41 targeted follow-up in
m16-followup.md:
the baked defaults are active, but both old M16 rows still fail. They need a
separate cleanup plan if they are promoted back to closeout gates.
| Area | Reason |
|---|---|
| Native engine parity claims | Covered by M40 reports; M41 is browser-only. |
| Product repository work | apps/yune-web is this repo's harness, not the separate TypeDuck-Web product repo. |
| New AI UX | AI remains default-off and outside startup optimization. |
| Broad UI redesign | Only measured startup/typing owners are in scope. |
| Rust engine rewrites | Require a separate engine plan unless M41 evidence proves a narrow runtime boundary blocker. |
- A reader can see exactly why yune-web startup was slow before M41.
- The top measured owner is reduced, not guessed around.
- Public-demo cache/startup errors are fixed or proven absent.
luna_pinyinandjyut6ping3_mobilebrowser startup are both covered.- First typing after ready remains responsive.
- Browser memory does not drift silently.
- The final report clearly separates browser harness wins from native engine wins.