SuperAPI AI Directory

vanilla js ui race conditions (vrm vs live2d)

Frontend & websites

View repository

Updated 10-16 11:08

About this skill

Handling DOM race conditions and optimistic update pitfalls in vanilla JS

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.