Issue #98: RAF — Vereinfachung + React Scheduler Integration stabilisieren #93

Closed
opened 2026-06-19 08:52:22 +00:00 by Artur · 0 comments
Owner

Problembeschreibung

Die aktuelle VirtualFrameClock (VFC, src/fakes/virtual-frame-clock.ts) ist ueberengineered:

  • 169 Zeilen fuer eine einfache RAF-Queue
  • Frame-Queue + Idle-Queue + Timer-Management
  • setTimeout-basierter Frame-Takt (16.67ms, 60fps)
  • Idle-Callback-Integration (wird aktuell nirgendwo genutzt)

Konsequenz: Der G3.2 React-Test (tests/interaction/react-18.test.ts) failt weil React 18 + CDN-Script-Load + RAF >500ms braucht. Das Problem ist jedoch NICHT die RAF-Komplexitaet, sondern dass:

  1. Die VFC unnoetig kompliziert ist fuer das, was wir brauchen
  2. Der Test-Timeout zu niedrig ist
  3. Reacts Concurrent Mode mit setTimeout-basierten RAFs nicht ideal harmoniert

Aktuelle Architecture

VFC.install(window)
  -> window.requestAnimationFrame = VFC._requestAnimationFrame
  -> VFC._scheduleNextFrame() via setTimeout(16.67ms)
      -> _processFrame()
          -> splice RAF-Queue
          -> fire callbacks mit performance.now()
          -> fire idle callbacks wenn Queue leer

Was wir eigentlich brauchen

  • RAF-Callbacks feuern ~60fps (16.67ms Intervall)
  • Reacts Scheduler kann setState-Updates zwischen Frames planen
  • Keine asynchronen Race-Conditions (RAF-Callback reiht neuen RAF -> naechstes Frame)

Analyse

Option A — Vereinfachter RAF-Loop (EMPFEHLUNG)

class SimpleRAF {
  private _timer: any = null;
  private _callbacks = new Map<number, FrameRequestCallback>();
  private _nextId = 0;

  install(win: any): void {
    win.requestAnimationFrame = (cb: FrameRequestCallback) => {
      const id = ++this._nextId;
      this._callbacks.set(id, cb);
      if (!this._timer) this._startLoop();
      return id;
    };
    win.cancelAnimationFrame = (id: number) => {
      this._callbacks.delete(id);
    };
  }

  private _startLoop(): void {
    const loop = () => {
      if (this._callbacks.size === 0) { this._timer = null; return; }
      const now = performance.now();
      const cbs = [...this._callbacks.values()];
      this._callbacks.clear();
      for (const cb of cbs) {
        try { cb(now); } catch (e) { console.error('[RAF] callback error:', e); }
      }
      this._timer = setTimeout(loop, 16);
    };
    this._timer = setTimeout(loop, 16);
  }

  stop(): void { if (this._timer !== null) { clearTimeout(this._timer); this._timer = null; } }
  tick(): void { /* manual tick for testing */ }
}

~50 Zeilen. Gleiches Verhalten. Keine idle-Queue.

Option B — Promisified RAF + Scheduler.await

React 18 nutzt Scheduler.unstable_next() und Scheduler.yield(). Wenn wir zusaetzlich ein await new Promise(r => requestAnimationFrame(r)) unterstuetzen, koennen wir mit setTimeout + Promise-Microtasks besser harmonieren.

Option C — Microtask-basierte Frame-Boundaries

Statt setTimeout: queueMicrotask(() => { ... setTimeout(loop, 16); }). Fuehrt RAF-Callbacks im Microtask-Cycle aus -> Reacts Scheduler sieht sofort neue State-Updates. Aber: Microtasks sind kein Frame-Boundary.

Entscheidung

Option A (Vereinfachung). Option B (Promisified RAF) als optionaler Zusatz. Option C ist spec-widrig (RAF muss in einem Macrotask feuern, nicht Microtask).

Akzeptanzkriterien

  • VirtualFrameClock durch SimpleRAF ersetzt (oder VFC radikal vereinfacht)
  • RAF-Callbacks feuern ~16.67ms Intervall (60fps)
  • Neue RAFs waehrend eines Callbacks landen im NAECHSTEN Frame (nicht im aktuellen)
  • cancelAnimationFrame(id) entfernt korrekt nur den spezifischen Callback
  • Test G3.2 (React 18 + RAF) laeuft gruen mit >2s Timeout
  • Alle existierenden RAF-Tests bleiben gruen
  • requestIdleCallback bleibt verfuegbar (kann separate Implementierung sein)
  • Performance: Kein Memory-Leak durch nicht-geraeumte Callbacks (Frame mit 0 RAFs stoppt Timer)

Betroffene Dateien

Datei Aenderung
src/fakes/virtual-frame-clock.ts Ersetzen durch simple-raf.ts oder radikal vereinfachen
src/runtime-isolation.ts VFC durch SimpleRAF ersetzen
tests/interaction/react-18.test.ts Timeout auf 2000ms erhoehen
tests/unit/virtual-frame-clock.test.ts An neue Implementation anpassen

Technische Risiken

  • setTimeout-Clamping: Browser clammen setTimeout auf 4ms ab 5. Aufruf. Bun clampt nicht. Unser Test-Verhalten kann sich also von Browser unterscheiden -- aber das ist OK da wir kein reales Display brauchen.
  • React 19: Neuer Scheduler koennte andere Anforderungen haben.

Testplan

  1. Unit: RAF feuert korrekt mit 16ms Intervall
  2. Unit: cancelAnimationFrame funktioniert
  3. Unit: Timer stoppt bei 0 RAFs, startet neu bei naechstem RAF
  4. Integration: React 18 Concurrent Mode mit RAF-basierten setState-Updates
  5. Regression: Alle 1545 Tests gruen
## Problembeschreibung Die aktuelle `VirtualFrameClock` (VFC, `src/fakes/virtual-frame-clock.ts`) ist ueberengineered: - **169 Zeilen** fuer eine einfache RAF-Queue - Frame-Queue + Idle-Queue + Timer-Management - `setTimeout`-basierter Frame-Takt (16.67ms, 60fps) - Idle-Callback-Integration (wird aktuell nirgendwo genutzt) **Konsequenz:** Der G3.2 React-Test (`tests/interaction/react-18.test.ts`) failt weil React 18 + CDN-Script-Load + RAF >500ms braucht. Das Problem ist jedoch NICHT die RAF-Komplexitaet, sondern dass: 1. Die VFC unnoetig kompliziert ist fuer das, was wir brauchen 2. Der Test-Timeout zu niedrig ist 3. Reacts Concurrent Mode mit setTimeout-basierten RAFs nicht ideal harmoniert ### Aktuelle Architecture ``` VFC.install(window) -> window.requestAnimationFrame = VFC._requestAnimationFrame -> VFC._scheduleNextFrame() via setTimeout(16.67ms) -> _processFrame() -> splice RAF-Queue -> fire callbacks mit performance.now() -> fire idle callbacks wenn Queue leer ``` ### Was wir eigentlich brauchen - RAF-Callbacks feuern ~60fps (16.67ms Intervall) - Reacts Scheduler kann setState-Updates zwischen Frames planen - Keine asynchronen Race-Conditions (RAF-Callback reiht neuen RAF -> naechstes Frame) ## Analyse **Option A — Vereinfachter RAF-Loop (EMPFEHLUNG)** ```typescript class SimpleRAF { private _timer: any = null; private _callbacks = new Map<number, FrameRequestCallback>(); private _nextId = 0; install(win: any): void { win.requestAnimationFrame = (cb: FrameRequestCallback) => { const id = ++this._nextId; this._callbacks.set(id, cb); if (!this._timer) this._startLoop(); return id; }; win.cancelAnimationFrame = (id: number) => { this._callbacks.delete(id); }; } private _startLoop(): void { const loop = () => { if (this._callbacks.size === 0) { this._timer = null; return; } const now = performance.now(); const cbs = [...this._callbacks.values()]; this._callbacks.clear(); for (const cb of cbs) { try { cb(now); } catch (e) { console.error('[RAF] callback error:', e); } } this._timer = setTimeout(loop, 16); }; this._timer = setTimeout(loop, 16); } stop(): void { if (this._timer !== null) { clearTimeout(this._timer); this._timer = null; } } tick(): void { /* manual tick for testing */ } } ``` ~50 Zeilen. Gleiches Verhalten. Keine idle-Queue. **Option B — Promisified RAF + Scheduler.await** React 18 nutzt `Scheduler.unstable_next()` und `Scheduler.yield()`. Wenn wir zusaetzlich ein `await new Promise(r => requestAnimationFrame(r))` unterstuetzen, koennen wir mit setTimeout + Promise-Microtasks besser harmonieren. **Option C — Microtask-basierte Frame-Boundaries** Statt setTimeout: `queueMicrotask(() => { ... setTimeout(loop, 16); })`. Fuehrt RAF-Callbacks im Microtask-Cycle aus -> Reacts Scheduler sieht sofort neue State-Updates. Aber: Microtasks sind kein Frame-Boundary. ## Entscheidung **Option A** (Vereinfachung). **Option B** (Promisified RAF) als optionaler Zusatz. Option C ist spec-widrig (RAF muss in einem Macrotask feuern, nicht Microtask). ## Akzeptanzkriterien - [ ] `VirtualFrameClock` durch `SimpleRAF` ersetzt (oder VFC radikal vereinfacht) - [ ] RAF-Callbacks feuern ~16.67ms Intervall (60fps) - [ ] Neue RAFs waehrend eines Callbacks landen im NAECHSTEN Frame (nicht im aktuellen) - [ ] `cancelAnimationFrame(id)` entfernt korrekt nur den spezifischen Callback - [ ] Test G3.2 (React 18 + RAF) laeuft gruen mit >2s Timeout - [ ] Alle existierenden RAF-Tests bleiben gruen - [ ] requestIdleCallback bleibt verfuegbar (kann separate Implementierung sein) - [ ] Performance: Kein Memory-Leak durch nicht-geraeumte Callbacks (Frame mit 0 RAFs stoppt Timer) ## Betroffene Dateien | Datei | Aenderung | |---|---| | `src/fakes/virtual-frame-clock.ts` | Ersetzen durch `simple-raf.ts` oder radikal vereinfachen | | `src/runtime-isolation.ts` | VFC durch SimpleRAF ersetzen | | `tests/interaction/react-18.test.ts` | Timeout auf 2000ms erhoehen | | `tests/unit/virtual-frame-clock.test.ts` | An neue Implementation anpassen | ## Technische Risiken - **setTimeout-Clamping**: Browser clammen setTimeout auf 4ms ab 5. Aufruf. Bun clampt nicht. Unser Test-Verhalten kann sich also von Browser unterscheiden -- aber das ist OK da wir kein reales Display brauchen. - **React 19**: Neuer Scheduler koennte andere Anforderungen haben. ## Testplan 1. **Unit**: RAF feuert korrekt mit 16ms Intervall 2. **Unit**: cancelAnimationFrame funktioniert 3. **Unit**: Timer stoppt bei 0 RAFs, startet neu bei naechstem RAF 4. **Integration**: React 18 Concurrent Mode mit RAF-basierten setState-Updates 5. **Regression**: Alle 1545 Tests gruen
Artur closed this issue 2026-06-19 10:12:27 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
glow-all/true-headless-browser#93
No description provided.