April 21, 2026 • Version: 2026.2.26

[Unendliche Render-Schleife bei leerem Cron-Job-Namensfeld] - Control UI Infinite Render Loop on Empty Cron Job Name Field

Das Anklicken des Namens-Eingabefelds im Cron-Jobs-Tab löst eine unendliche reaktive Schleife aufgrund von Objektreferenz-Churn im onFormChange-Handler aus, was zu einer CPU-Auslastung von 99,6 % und einem Einfrieren des Dashboards führt.

🔍 Symptome

Technische Manifestationen

Das Problem manifestiert sich als vollständiger clientseitiger Einfrierer bei der Interaktion mit einem bestimmten Eingabefeld:

  • CPU-Spitzen: Firefox-Inhaltsprozess verbraucht 99.6% der verfügbaren CPU-Ressourcen unbegrenzt
  • WebSocket-Degradation: Gateway-Verbindungen beginnen mit 40s+ Handshake-Timeout-Fehlern auszulaufen
  • Tab-Reaktionslosigkeit: Das Dashboard friert vollständig ein; keine Interaktionen reagieren
  • Gateway-Integrität: Das OpenClaw-Gateway selbst bleibt gesund und reagiert auf andere Clients

Reproduktionssequenz

1. Navigate to: http://localhost:3000/__openclaw__/control/
2. Click the "Cron Jobs" tab
3. Click the "Name" input field (without typing)
4. Dashboard freezes immediately

Browser-Konsolenindikatoren

[Firefox DevTools Console]
⚠ This page appears to have a render loop that has been running for 
   longer than expected. Consider optimizing the component code.
   Source: openclaw-control-ui/cron-form.js

Prozessverhalten

# Observed via browser task manager:
Dashboard Tab PID 12345
- CPU: 99.6% (continuously climbing)
- Memory: 847MB → 1.2GB (leaking during loop)
- State: Running (unresponsive)

🧠 Ursache

Der reaktive Loop-Mechanismus

Die unendliche Render-Schleife wird durch eine zirkuläre Abhängigkeit zwischen Lits reaktivem Property-System und nativen DOM-Eingabeereignissen verursacht. Der problematische Code befindet sich in der Cron Jobs-Formularkomponente:

// Problematic source: src/components/cron-form.js (approx. line 47)
onFormChange: p => {
  e.cronForm = _o({...e.cronForm, ...p}),
  e.cronFieldErrors = Qn(e.cronForm)
}

Analyse der Fehlersequenz

Die reaktive Schleife folgt diesem präzisen Ausführungspfad:

  1. Benutzer klickt auf leeres Eingabefeld — Der Browser feuert ein natives @input-Ereignis mit dem aktuellen (leeren) Wert
  2. onFormChange wird ausgeführt — Handler empfängt {name: ""}
  3. Spread-Operator erstellt neues Objekt{...e.cronForm, ...{name: ""}} erzeugt eine neue Objektreferenz, obwohl der Wert identisch ist
  4. Lit erkennt Property-Änderung — Der mit @state() oder @property() dekorierte cronForm-Setter löst ein Re-Rendering aus
  5. Re-Rendering liest Eingabewert — Die render()-Methode oder der Template-Accessor der Komponente liest den aktuellen Wert des DOM-Eingabefelds ("")
  6. DOM-Ereignis feuert erneut — Das Lesen des Eingabewerts in einigen Implementierungen löst den @input-Handler erneut aus
  7. Schleife wiederholt sich — Schritte 2-6 werden unendlich ausgeführt

Diagramm der Objektreferenz-Fluktuation

cronForm (initial):   {name: "", schedule: "0 * * * *"}
                        ↓ @input event fires with {name: ""}
cronForm (reassigned):  {name: "", schedule: "0 * * * *"} ← NEW OBJECT REFERENCE
                        ↓ Lit detects change via !== comparison
Re-render triggered
                        ↓ Template reads input.value
@input fires again:     {name: ""}
                        ↓ Same structure, NEW REFERENCE
cronForm (reassigned):  {name: "", schedule: "0 * * * *"} ← LOOP CONTINUES

Warum Lits Standard-Gleichheitsprüfung fehlschlägt

Lit verwendet !== für die Property-Änderungserkennung:

// Lit's internal property setter (simplified)
set value(newVal) {
  if (this._value !== newVal) {  // Object !== Object (different references)
    this._value = newVal;
    this.requestUpdate();        // ALWAYS triggers update
  }
}

Der Spread-Operator garantiert eine neue Objektreferenz und umgeht damit jede Inhalts-Gleichheitsoptimierung.

🛠️ Schritt-für-Schritt-Lösung

Empfohlene Lösung: Flache Gleichheitsprüfung

Ändern Sie den onFormChange-Handler, um Werte vor der Neuzuweisung zu vergleichen:

// BEFORE (problematic)
onFormChange: p => {
  e.cronForm = _o({...e.cronForm, ...p}),
  e.cronFieldErrors = Qn(e.cronForm)
}

// AFTER (fixed)
onFormChange: p => {
  const merged = { ...e.cronForm, ...p };
  
  // Shallow equality check - only update if values actually changed
  const hasChanged = Object.keys(p).some(
    key => e.cronForm[key] !== merged[key]
  );
  
  if (hasChanged) {
    e.cronForm = _o(merged);
    e.cronFieldErrors = Qn(e.cronForm);
  }
}

Alternative Lösung: Tiefenvergleich mit JSON

Für verschachtelte Formularstrukturen bietet ein JSON-Serialisierungsvergleich eine gründliche Prüfung:

onFormChange: p => {
  const merged = { ...e.cronForm, ...p };
  const serialized = JSON.stringify(merged);
  const currentSerialized = JSON.stringify(e.cronForm);
  
  if (serialized !== currentSerialized) {
    e.cronForm = _o(merged);
    e.cronFieldErrors = Qn(e.cronForm);
  }
}

Implementierungsschritte

  1. Datei lokalisieren: src/components/cron-form.js oder src/components/cron-jobs/cron-form.ts
  2. Die onFormChange-Methode finden in der CronForm-Klasse
  3. Den Handler ersetzen mit der oben gezeigten korrigierten Version
  4. Eine Utility-Funktion hinzufügen zur Wiederverwendung, falls mehrere Formulare dasselbe Muster haben
  5. Die Korrektur verifizieren mit den untenstehenden Verifizierungsschritten

Vorbeugende Korrektur: Wrapper-Utility

Erstellen Sie eine wiederverwendbare safeUpdateForm-Utility, um sie auf alle Formularkomponenten anzuwenden:

// src/utils/form-helpers.js

/**
 * Safely updates a reactive form object, preventing unnecessary
 * re-renders when values haven't actually changed.
 * 
 * @param {Object} currentForm - The current form state
 * @param {Object} updates - The partial updates to apply
 * @param {Function} validator - Optional validation function
 * @returns {Object|null} - New form state or null if unchanged
 */
export function safeUpdateForm(currentForm, updates, validator) {
  const merged = { ...currentForm, ...updates };
  
  const hasChanged = Object.keys(updates).some(
    key => currentForm[key] !== merged[key]
  );
  
  if (!hasChanged) {
    return null;
  }
  
  return validator ? validator(merged) : merged;
}

// Usage in component:
onFormChange: p => {
  const newForm = safeUpdateForm(e.cronForm, p, _o);
  if (newForm) {
    e.cronForm = newForm;
    e.cronFieldErrors = Qn(e.cronForm);
  }
}

🧪 Verifizierung

Verifizierungsbefehle und erwartete Ausgabe

Führen Sie nach der Anwendung der Korrektur die folgenden Verifizierungsschritte durch:

1. Funktioneller Interaktionstest

# Click the Name field and verify no CPU spike
1. Open browser DevTools (F12)
2. Navigate to the Cron Jobs tab
3. Click the Performance Monitor tab in DevTools
4. Observe: CPU should remain below 5% when interacting with empty fields

2. Automatisierter Testfall

Erstellen Sie einen Test zur Verifizierung der Korrektur:

// tests/cron-form.test.js

import { fixture, expect } from '@open-wc/testing';
import { sendKeys } from '@web/test-runner-commands';

describe('cron-form', () => {
  it('should not trigger infinite render on empty input focus', async () => {
    const el = await fixture('');
    
    const updatesBefore = el.updateCount || 0;
    const nameInput = el.shadowRoot.querySelector('input[name="name"]');
    
    // Focus without typing
    nameInput.focus();
    
    // Wait for potential loop to manifest
    await new Promise(r => setTimeout(r, 500));
    
    const updatesAfter = el.updateCount || 0;
    
    // Should not have excessive updates
    expect(updatesAfter - updatesBefore).to.be.lessThan(5);
  });
});

3. Manuelle Verifizierungs-Checkliste

  • CPU-Überwachung: Der Browser-Task-Manager zeigt weniger als 5% CPU im Leerlauf und bei normaler Interaktion
  • Eingabefokus: Klicken auf das leere Namensfeld fokussiert ohne Nebeneffekte
  • Formularübermittlung: Das Cron-Job-Formular übermittelt korrekt mit gültigen Daten
  • Validierung: Fehlermeldungen werden korrekt für ungültige Eingaben angezeigt
  • WebSocket-Gesundheit: Gateway-Verbindungen bleiben stabil (keine 40s+ Timeouts)

4. Regressionstest

# Verify form changes still propagate correctly
1. Fill in Name: "my-cron-job"
2. Fill in Schedule: "0 * * * *"
3. Verify cronFieldErrors updates appropriately
4. Submit form and confirm job creation

⚠️ Häufige Fehler

Umgebungsspezifische Fallen

  • Entwicklungs- vs. Produktions-Builds: Hot Module Replacement (HMR) in der Entwicklung kann das Problem maskieren, indem der Status häufiger zurückgesetzt wird. Testen Sie auf Produktions-Builds, um die Korrektur zu bestätigen.
  • Browser-Unterschiede: Firefox ESR zeigt dieses Verhalten aufgrund seiner Ereignisbehandlung deutlicher. Chrome und Safari verbergen das Symptom möglicherweise durch ihre internen Optimierungen, aber die zugrunde liegende Schleife existiert immer noch.
  • Docker-Container-Limits: Bei Ausführung des Dashboards in Docker können Container-CPU-Limits unterschiedliche Fehlerarten verursachen (Prozessbeendigung vs. unendliche Schleife).

Zu vermeidende Implementierungsfehler

  1. deepClone anstelle von Vergleich verwenden:
    // WRONG - Expensive and doesn't solve the root cause
    onFormChange: p => {
      e.cronForm = _o(JSON.parse(JSON.stringify({...e.cronForm, ...p}))),
      // ...
    }
    
  2. Validierungs-Neuberechnung überspringen:
    // WRONG - Breaks validation when values change legitimately
    onFormChange: p => {
      const merged = { ...e.cronForm, ...p };
      // Always update validation, even if object reference differs
      e.cronFieldErrors = Qn(merged);
      // ...
    }
    
  3. Serialisierte Strings ohne Debouncing vergleichen:
    // RISKY - May cause issues with rapid successive inputs
    onFormChange: p => {
      // If typing fast, this may skip valid updates
      if (JSON.stringify(e.cronForm) !== JSON.stringify({...e.cronForm, ...p})) {
        // ...
      }
    }
    

Grenzfälle

  • Null/undefinierte Werte: Stellen Sie sicher, dass die Gleichheitsprüfung null, undefined und leere Strings konsistent behandelt
  • Numerische Null vs. leer: 0 sollte nicht als "kein Wert" behandelt werden
  • Checkbox/Boolean-Felder: false zu false sollte keine Updates auslösen
  • Array-Felder: Wenn das Formular Arrays enthält, verwenden Sie einen geeigneten Vergleich (flach für die meisten Fälle)

Zugehörige Komponentenmuster

Wenn andere Formularkomponenten im Codebase dasselbe Muster verwenden, prüfen und korrigieren Sie diese:

# Grep for the problematic pattern across the codebase
grep -r "cronForm = _o({...e.cronForm" src/
grep -r "onFormChange.*spread" src/
grep -r "@state()\s*\n.*Form" src/

🔗 Zugehörige Fehler

Kontextbezogen verbundene Probleme

  • WEB_SOCKET_HANDSHAKE_TIMEOUT
    WebSocket-Verbindungen, die aufgrund eines nicht reagierenden Browser-Tabs ein Timeout haben. Das Gateway bleibt gesund, aber der Client kann eingehende Nachrichten nicht verarbeiten.
  • CONTENT_PROCESS_HIGH_CPU
    Firefox-spezifischer Fehler, der auf übermäßigen CPU-Verbrauch in Inhaltsprozessen hinweist, oft symptomatisch für JavaScript-unendliche Schleifen.
  • LIT_UPDATE_CYCLE_EXCEEDED
    Lit-Framework-Warnung, wenn zu viele Render-Zyklen in einer einzigen Aufgabe auftreten. Kann in der Konsole erscheinen, bevor der Tab einfriert.
  • FORM_VALIDATION_STALE
    Zugehörige Validierungsinkonsistenzfehler, wenn Formularstatus-Updates schneller erfolgen als die Validierung verarbeiten kann.

Ähnliche historische Probleme

  • Lit Issue #1427: Infinite loop with object spread in reactive properties
    Framework-Level-Dokumentation desselben Musters, das andere Lit-basierte Anwendungen betrifft.
  • React useState equality comparison
    Ähnliches Anti-Pattern existiert in React, wo setState({...state, value}) ohne ordnungsgemäße Memoisation unendliche Re-Renders verursacht.

Vorbeugende Maßnahmen für die zukünftige Entwicklung

  • Linting-Regel: ESLint-Regel hinzufügen, um Object-Spread-Muster in Lit-Property-Setters zu erkennen
  • Komponententests: Render-Zyklen-Zählung in Komponenten-Test-Suiten einbeziehen
  • Performance-Budgets: Alarme für übermäßige Re-Renders in der CI/CD-Pipeline festlegen

Belege & Quellen

Diese Troubleshooting-Anleitung wurde automatisch von der FixClaw Intelligence Pipeline aus Community-Diskussionen synthetisiert.