From 1719329db6f216a2ea4b945a33085979ecd33fbd Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:37:47 +0800 Subject: [PATCH 1/8] Shrink slightly by replacing callback-based iteration with in delayed event flush paths ( and ). This removes closure overhead and trims emitted code while preserving behavior. Runtime tests pass.\n\nResult: {"status":"keep","bundle_size":95402} --- packages/react/runtime/src/lynx/tt.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/react/runtime/src/lynx/tt.ts b/packages/react/runtime/src/lynx/tt.ts index 03aec47cb7..3a8f5daa34 100644 --- a/packages/react/runtime/src/lynx/tt.ts +++ b/packages/react/runtime/src/lynx/tt.ts @@ -131,8 +131,7 @@ function onLifecycleEventImpl(type: LifecycleConstant, data: unknown): void { // TODO: It seems `delayedEvents` and `delayedLifecycleEvents` should be merged into one array to ensure the proper order of events. flushDelayedLifecycleEvents(); if (delayedEvents) { - delayedEvents.forEach((args) => { - const [handlerName, data] = args; + for (const [handlerName, data] of delayedEvents) { // eslint-disable-next-line prefer-const let [idStr, ...rest] = handlerName.split(':'); while (jsReadyEventIdSwap[idStr!]) idStr = jsReadyEventIdSwap[idStr!]?.toString(); @@ -141,7 +140,7 @@ function onLifecycleEventImpl(type: LifecycleConstant, data: unknown): void { } catch (e) { lynx.reportError(e as Error); } - }); + } delayedEvents.length = 0; } @@ -194,9 +193,9 @@ function flushDelayedLifecycleEvents(): void { if (flushingDelayedLifecycleEvents) return; flushingDelayedLifecycleEvents = true; if (delayedLifecycleEvents) { - delayedLifecycleEvents.forEach((e) => { + for (const e of delayedLifecycleEvents) { onLifecycleEvent(e); - }); + } delayedLifecycleEvents.length = 0; } flushingDelayedLifecycleEvents = false; From d0aa7d4455b2bc735b905d1de8dc841c1b0b2c24 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:39:00 +0800 Subject: [PATCH 2/8] Further trim by inlining delayed public-component event forwarding in and removing the standalone wrapper function. Preserves behavior and cuts bundle to 95,396 bytes. Runtime tests pass.\n\nResult: {"status":"keep","bundle_size":95396} --- packages/react/runtime/src/lynx/tt.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/react/runtime/src/lynx/tt.ts b/packages/react/runtime/src/lynx/tt.ts index 3a8f5daa34..74b5dcbec3 100644 --- a/packages/react/runtime/src/lynx/tt.ts +++ b/packages/react/runtime/src/lynx/tt.ts @@ -36,7 +36,9 @@ function injectTt(): void { const tt = lynxCoreInject.tt; tt.OnLifecycleEvent = onLifecycleEvent; tt.publishEvent = delayedPublishEvent; - tt.publicComponentEvent = delayedPublicComponentEvent; + tt.publicComponentEvent = (_componentId, handlerName, data) => { + delayedPublishEvent(handlerName, data); + }; tt.callDestroyLifetimeFun = () => { removeCtxNotFoundEventListener(); destroyWorklet(); @@ -257,10 +259,6 @@ function publicComponentEvent(_componentId: string, handlerName: string, data: E publishEvent(handlerName, data); } -function delayedPublicComponentEvent(_componentId: string, handlerName: string, data: EventDataType) { - delayedPublishEvent(handlerName, data); -} - function updateGlobalProps(newData: Record): void { Object.assign(lynx.__globalProps, newData); From 0944711f9c1ddd5110394e88f5eabb587cbbd3c6 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:40:28 +0800 Subject: [PATCH 3/8] Continue slimming: inline post-hydration forwarding assignment and remove standalone wrapper function. Bundle improved to 95,385 bytes with tests passing.\n\nResult: {"status":"keep","bundle_size":95385} --- packages/react/runtime/src/lynx/tt.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/react/runtime/src/lynx/tt.ts b/packages/react/runtime/src/lynx/tt.ts index 74b5dcbec3..72fb216c67 100644 --- a/packages/react/runtime/src/lynx/tt.ts +++ b/packages/react/runtime/src/lynx/tt.ts @@ -147,7 +147,9 @@ function onLifecycleEventImpl(type: LifecycleConstant, data: unknown): void { } lynxCoreInject.tt.publishEvent = publishEvent; - lynxCoreInject.tt.publicComponentEvent = publicComponentEvent; + lynxCoreInject.tt.publicComponentEvent = (_componentId, handlerName, d) => { + publishEvent(handlerName, d); + }; // console.debug("********** After hydration:"); // printSnapshotInstance(__root as BackgroundSnapshotInstance); @@ -255,10 +257,6 @@ function publishEvent(handlerName: string, data: EventDataType) { } } -function publicComponentEvent(_componentId: string, handlerName: string, data: EventDataType) { - publishEvent(handlerName, data); -} - function updateGlobalProps(newData: Record): void { Object.assign(lynx.__globalProps, newData); From 06843a5d748c2c82302843388c878c49efbb9b63 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:46:20 +0800 Subject: [PATCH 4/8] Minor micro-optimization: convert two forwarding arrow handlers () to expression-bodied arrows in and post-hydration reassignment. Bundle reduced to 95,381 bytes with tests passing.\n\nResult: {"status":"keep","bundle_size":95381} --- packages/react/runtime/src/lynx/tt.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/react/runtime/src/lynx/tt.ts b/packages/react/runtime/src/lynx/tt.ts index 72fb216c67..4466ce91fa 100644 --- a/packages/react/runtime/src/lynx/tt.ts +++ b/packages/react/runtime/src/lynx/tt.ts @@ -36,9 +36,7 @@ function injectTt(): void { const tt = lynxCoreInject.tt; tt.OnLifecycleEvent = onLifecycleEvent; tt.publishEvent = delayedPublishEvent; - tt.publicComponentEvent = (_componentId, handlerName, data) => { - delayedPublishEvent(handlerName, data); - }; + tt.publicComponentEvent = (_componentId, handlerName, data) => delayedPublishEvent(handlerName, data); tt.callDestroyLifetimeFun = () => { removeCtxNotFoundEventListener(); destroyWorklet(); @@ -147,9 +145,7 @@ function onLifecycleEventImpl(type: LifecycleConstant, data: unknown): void { } lynxCoreInject.tt.publishEvent = publishEvent; - lynxCoreInject.tt.publicComponentEvent = (_componentId, handlerName, d) => { - publishEvent(handlerName, d); - }; + lynxCoreInject.tt.publicComponentEvent = (_componentId, handlerName, d) => publishEvent(handlerName, d); // console.debug("********** After hydration:"); // printSnapshotInstance(__root as BackgroundSnapshotInstance); From efcbd357cd6a0686c53548631d3a720ce759108d Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:48:48 +0800 Subject: [PATCH 5/8] Further micro-trim: inline root-rendered check in (remove temp variable) and simplify delayed lifecycle flush by removing redundant truthy check on always-initialized array. Bundle improved to 95,375 bytes; tests pass.\n\nResult: {"status":"keep","bundle_size":95375} --- packages/react/runtime/src/lynx/tt.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/react/runtime/src/lynx/tt.ts b/packages/react/runtime/src/lynx/tt.ts index 4466ce91fa..44f111f6a5 100644 --- a/packages/react/runtime/src/lynx/tt.ts +++ b/packages/react/runtime/src/lynx/tt.ts @@ -51,10 +51,9 @@ function injectTt(): void { } function onLifecycleEvent([type, data]: [LifecycleConstant, unknown]) { - const hasRootRendered = CHILDREN in __root; // never called `render(, __root)` // happens if user call `root.render()` async - if (!hasRootRendered) { + if (!(CHILDREN in __root)) { delayLifecycleEvent(type, data); return; } @@ -192,12 +191,10 @@ function flushDelayedLifecycleEvents(): void { // avoid stackoverflow if (flushingDelayedLifecycleEvents) return; flushingDelayedLifecycleEvents = true; - if (delayedLifecycleEvents) { - for (const e of delayedLifecycleEvents) { - onLifecycleEvent(e); - } - delayedLifecycleEvents.length = 0; + for (const e of delayedLifecycleEvents) { + onLifecycleEvent(e); } + delayedLifecycleEvents.length = 0; flushingDelayedLifecycleEvents = false; } From 2cedc1bea0d816bd9c790d17aed0e85ab44cc26c Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:58:16 +0800 Subject: [PATCH 6/8] In first-screen delayed-event replay, removed redundant outer try/catch around calls. already handles handler exceptions, so behavior remains covered while trimming code. Bundle improved to 95,320 bytes and tests pass.\n\nResult: {"status":"keep","bundle_size":95320} --- packages/react/runtime/src/lynx/tt.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/react/runtime/src/lynx/tt.ts b/packages/react/runtime/src/lynx/tt.ts index 44f111f6a5..3bb54e6d56 100644 --- a/packages/react/runtime/src/lynx/tt.ts +++ b/packages/react/runtime/src/lynx/tt.ts @@ -129,19 +129,13 @@ function onLifecycleEventImpl(type: LifecycleConstant, data: unknown): void { // TODO: It seems `delayedEvents` and `delayedLifecycleEvents` should be merged into one array to ensure the proper order of events. flushDelayedLifecycleEvents(); - if (delayedEvents) { - for (const [handlerName, data] of delayedEvents) { - // eslint-disable-next-line prefer-const - let [idStr, ...rest] = handlerName.split(':'); - while (jsReadyEventIdSwap[idStr!]) idStr = jsReadyEventIdSwap[idStr!]?.toString(); - try { - publishEvent([idStr, ...rest].join(':'), data); - } catch (e) { - lynx.reportError(e as Error); - } - } - delayedEvents.length = 0; + for (const [handlerName, data] of delayedEvents) { + // eslint-disable-next-line prefer-const + let [idStr, ...rest] = handlerName.split(':'); + while (jsReadyEventIdSwap[idStr!]) idStr = jsReadyEventIdSwap[idStr!]?.toString(); + publishEvent([idStr, ...rest].join(':'), data); } + delayedEvents.length = 0; lynxCoreInject.tt.publishEvent = publishEvent; lynxCoreInject.tt.publicComponentEvent = (_componentId, handlerName, d) => publishEvent(handlerName, d); From 4873050292fdee7dfe5d1715d57893f8862c17c7 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:25:21 +0800 Subject: [PATCH 7/8] Simplify wiring in : make it delegate to once and remove post-hydration reassignment. This preserves pre-hydration buffering and post-hydration direct dispatch while deleting redundant code. Tests pass; bundle reduced to 95,193 bytes.\n\nResult: {"status":"keep","bundle_size":95193} --- packages/react/runtime/src/lynx/tt.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react/runtime/src/lynx/tt.ts b/packages/react/runtime/src/lynx/tt.ts index 3bb54e6d56..ed063a29a7 100644 --- a/packages/react/runtime/src/lynx/tt.ts +++ b/packages/react/runtime/src/lynx/tt.ts @@ -36,7 +36,7 @@ function injectTt(): void { const tt = lynxCoreInject.tt; tt.OnLifecycleEvent = onLifecycleEvent; tt.publishEvent = delayedPublishEvent; - tt.publicComponentEvent = (_componentId, handlerName, data) => delayedPublishEvent(handlerName, data); + tt.publicComponentEvent = (_componentId, handlerName, data) => tt.publishEvent(handlerName, data); tt.callDestroyLifetimeFun = () => { removeCtxNotFoundEventListener(); destroyWorklet(); @@ -138,7 +138,6 @@ function onLifecycleEventImpl(type: LifecycleConstant, data: unknown): void { delayedEvents.length = 0; lynxCoreInject.tt.publishEvent = publishEvent; - lynxCoreInject.tt.publicComponentEvent = (_componentId, handlerName, d) => publishEvent(handlerName, d); // console.debug("********** After hydration:"); // printSnapshotInstance(__root as BackgroundSnapshotInstance); From 964e9a3e4078ea619d6f6113d225a162e8b68a83 Mon Sep 17 00:00:00 2001 From: Qingyu Wang <40660121+colinaaa@users.noreply.github.com> Date: Wed, 18 Mar 2026 21:10:43 +0800 Subject: [PATCH 8/8] fix: guard delayed event queue during hydration --- .github/lynx-stack.instructions.md | 6 ++++++ packages/react/runtime/src/lynx/tt.ts | 15 +++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/lynx-stack.instructions.md b/.github/lynx-stack.instructions.md index 023e0c6ec5..1fe6fe7caa 100644 --- a/.github/lynx-stack.instructions.md +++ b/.github/lynx-stack.instructions.md @@ -6,3 +6,9 @@ When updating web element APIs, add targeted Playwright tests in packages/web-pl Ensure Playwright browsers are installed (pnpm exec playwright install --with-deps ) before running web-elements tests. For x-input type="number" in web-elements, keep inner input type as text, set inputmode="decimal", and filter number input internally without setting input-filter explicitly. Add new web-elements UI fixtures under packages/web-platform/web-elements/tests/fixtures and commit matching snapshots in packages/web-platform/web-elements/tests/web-elements.spec.ts-snapshots. + +--- +applyTo: "packages/react/runtime/src/**/*" +--- + +The delayed event queue exported from packages/react/runtime/src/lifecycle/event/delayEvents.ts is lazily initialized and may be undefined until the first delayed publish; when draining it, read it into a local variable, guard for undefined, and then clear that same array instance. diff --git a/packages/react/runtime/src/lynx/tt.ts b/packages/react/runtime/src/lynx/tt.ts index ed063a29a7..936e4d51fa 100644 --- a/packages/react/runtime/src/lynx/tt.ts +++ b/packages/react/runtime/src/lynx/tt.ts @@ -129,13 +129,16 @@ function onLifecycleEventImpl(type: LifecycleConstant, data: unknown): void { // TODO: It seems `delayedEvents` and `delayedLifecycleEvents` should be merged into one array to ensure the proper order of events. flushDelayedLifecycleEvents(); - for (const [handlerName, data] of delayedEvents) { - // eslint-disable-next-line prefer-const - let [idStr, ...rest] = handlerName.split(':'); - while (jsReadyEventIdSwap[idStr!]) idStr = jsReadyEventIdSwap[idStr!]?.toString(); - publishEvent([idStr, ...rest].join(':'), data); + const queuedDelayedEvents = delayedEvents; + if (queuedDelayedEvents) { + for (const [handlerName, data] of queuedDelayedEvents) { + // eslint-disable-next-line prefer-const + let [idStr, ...rest] = handlerName.split(':'); + while (jsReadyEventIdSwap[idStr!]) idStr = jsReadyEventIdSwap[idStr!]?.toString(); + publishEvent([idStr, ...rest].join(':'), data); + } + queuedDelayedEvents.length = 0; } - delayedEvents.length = 0; lynxCoreInject.tt.publishEvent = publishEvent;