Frontmatter
| id | 8811 |
| title | Fix Canvas Remount Race Condition by Retrying getOffscreenCanvas |
| state | Closed |
| labels | bugairegression |
| assignees | tobiu |
| createdAt | Jan 19, 2026, 2:24 PM |
| updatedAt | Jan 19, 2026, 3:02 PM |
| githubUrl | https://github.com/neomjs/neo/issues/8811 |
| author | tobiu |
| commentsCount | 3 |
| parentIssue | null |
| subIssues | [] |
| subIssuesCompleted | 0 |
| subIssuesTotal | 0 |
| blockedBy | [] |
| blocking | [] |
| closedAt | Jan 19, 2026, 2:27 PM |
Fix Canvas Remount Race Condition by Retrying getOffscreenCanvas

Input from Gemini 3 Pro:
✦ Implemented a robust retry mechanism in
Neo.component.Canvas.afterSetMounted.When a component is reused, the App Worker might trigger
afterSetMounted(true)before the new DOM node is fully established in the Main Thread. This causedgetOffscreenCanvasto fail (node not found), leavingoffscreenRegisteredasfalseand preventing the animation loop from starting.The fix adds a loop that attempts to get the offscreen canvas up to 5 times with a 50ms delay if the initial call returns
success: false. This ensures reliable initialization even under heavy load or race conditions.

Input from Gemini 3 Pro:
✦ Refined the solution to address the root cause of "phantom mounts" and further harden the Canvas initialization.
1. Root Cause Analysis: When navigating away from
Newsand back,NewsTabContainermomentarily restored its previousactiveIndex(e.g., Tickets) before the Router updated it to the default (Releases). This caused the Tickets view (andTimelineCanvas) to mount and immediately unmount, triggering a race condition wheregetOffscreenCanvasfailed because the DOM node was removed before it could be registered.2. Architecture Fix (
NewsTabContainer): ImplementedafterSetMountedinapps/portal/view/news/TabContainer.mjsto resetactiveIndextonullwhenever the container unmounts. This ensures that on the next mount, the container starts in a clean state and only renders the tab explicitly requested by the Router, preventing the phantom mount cycle.3. Robustness Fix (
Neo.component.Canvas): Replaced the fixed retry count with a robustwhileloop that polls forgetOffscreenCanvasas long asme.mountedis true. This handles:
- Premature/Phantom Mounts: If the component unmounts during initialization (like in the bug scenario),
me.mountedflips tofalse, and the loop exits gracefully without error.- Slow DOM Updates: If the DOM node takes longer than expected to appear, the loop waits indefinitely (until success or unmount), preventing initialization failures due to timeouts.
This dual approach fixes the specific regression while making the framework more resilient to similar timing issues in the future.

Input from Gemini 3 Pro:
✦ Optimized the polling mechanism in
Neo.component.Canvasto use an exponential back-off strategy.Instead of a fixed 50ms interval, the polling loop now starts at 50ms and doubles the delay after each failed attempt, capping at 1000ms.
Benefits:
- Immediate Responsiveness: Still catches the DOM node appearing quickly (typical case).
- Resource Efficiency: Significantly reduces CPU overhead if the component enters a "zombie mount" state (mounted but DOM missing) or if the DOM update is severely delayed, scaling back to a 1Hz check.
- Self-Healing: Continues to check (at a low frequency) indefinitely as long as the component thinks it is mounted, ensuring that if the DOM eventually recovers (e.g. parent becomes visible), the canvas will initialize.
afterSetMountedtriggersgetOffscreenCanvas. If this runs before the VDOM worker has patched the DOM (creating the node),DomAccessreturnssuccess: false, causingoffscreenRegisteredto remainfalseand the canvas initialization to fail.Neo.component.Canvas.afterSetMountedis insufficient in some race conditions (e.g. heavy main thread load or specific navigation flows).Neo.component.Canvas.afterSetMounted. IfgetOffscreenCanvasfails (returnssuccess: false), wait and try again (e.g., up to 5 times with 50ms delay) before giving up.