Symptoms
- UI components (such as sidebars and HUDs) fail to display on initial page load because their state is lost, but they pop up again after manual interaction.
- The UI flickers between opening and closing, or cannot be correctly closed through code.
- Event listeners bound to DOM elements fail to fire.
Root cause
Cause 1: Inconsistent DOM lazy-loading timing across different models/modules
- Problem: When handling cross-model (such as Live2D and VRM) or modular applications, some popups only createElement on first click (lazy loading).
- Why it happens: Performance optimization means the DOM is not in the HTML from the start. If you use a fixed setTimeout(..., 100) to grab the DOM and bind events, there is a very high chance you will come up empty (the DOM has not been generated yet).
- Solution: Useself-terminating recursive pollinginstead of waiting blindly.
Cause 2: Race condition conflict between Optimistic UI and underlying state
- Problem: After the user clicks a toggle, the frontend optimistically sets the button to on with an animation and simultaneously sends a request to the backend. But before the backend returns the real \flag, if other components (such as a polling App) trigger a check based on an empty backend or a default erroneous DOM, mutually overwriting BUGs occur.
- Why it happens: Lack of a unified one-way data flow.
- Solution: When reading the source state, determine whether the UI is disabled (loading). If so, preferentially trust the optimistic state cached by \u0007gent_ui_v2 or the backend-cached machineFlags, rather than blindly trusting a newly created and not-yet-synchronized DOM node.
Code solution
1. Binding polling to handle lazy-loaded DOM (safe and reliable)
`javascript const bindEvents = () => { // Compatible with multiple ID resolution const getEl = (ids) => { for (let id of ids) { const el = document.getElementById(id); if (el) return el; } return null; };
const targetEl = getEl(['live2d-agent-keyboard', 'vrm-agent-keyboard']);
// 如果没找到,自己建立一个 500ms 后的新宏任务来重试,然后 return 本次任务。
if (!targetEl) {
setTimeout(bindEvents, 500);
return;
}
// 找到了,顺次执行并结束,不会留下任何定时器垃圾
targetEl.addEventListener('change', myLogic);
myLogic(); // 手动触发第一次检查
};
setTimeout(bindEvents, 100); // Fire the first shot `
2. Multi-source state determination fallback (to prevent overwriting)
`javascript const checkState = () => { const isUiInteractive = domCheckbox && !domCheckbox.disabled; let isFeatureOn = false;
if (!isUiInteractive) {
// UI 在转圈加载,信任乐观状态或后端缓存
isFeatureOn = optimisticState !== undefined ? optimisticState : backendFlags;
} else {
// UI 处于可用交互态,百分百信任当前 DOM
isFeatureOn = optimisticState !== undefined ? optimisticState : domCheckbox.checked;
}
} `
Key lessons
- Never use a hardcoded setTimeout duration to assume that a resource or DOM will definitely finish loading.
- If you do not have a Virtual DOM framework like React, when manually handling cross-component communication, always be mindful of the real DOM snapshot at the current script runtime.