[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:
- Benutzer klickt auf leeres Eingabefeld — Der Browser feuert ein natives
@input-Ereignis mit dem aktuellen (leeren) Wert - onFormChange wird ausgeführt — Handler empfängt
{name: ""} - Spread-Operator erstellt neues Objekt —
{...e.cronForm, ...{name: ""}}erzeugt eine neue Objektreferenz, obwohl der Wert identisch ist - Lit erkennt Property-Änderung — Der mit
@state()oder@property()dekoriertecronForm-Setter löst ein Re-Rendering aus - Re-Rendering liest Eingabewert — Die
render()-Methode oder der Template-Accessor der Komponente liest den aktuellen Wert des DOM-Eingabefelds ("") - DOM-Ereignis feuert erneut — Das Lesen des Eingabewerts in einigen Implementierungen löst den
@input-Handler erneut aus - 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
- Datei lokalisieren:
src/components/cron-form.jsodersrc/components/cron-jobs/cron-form.ts - Die onFormChange-Methode finden in der CronForm-Klasse
- Den Handler ersetzen mit der oben gezeigten korrigierten Version
- Eine Utility-Funktion hinzufügen zur Wiederverwendung, falls mehrere Formulare dasselbe Muster haben
- 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
- 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}))), // ... } - 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); // ... } - 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,undefinedund leere Strings konsistent behandelt - Numerische Null vs. leer:
0sollte nicht als "kein Wert" behandelt werden - Checkbox/Boolean-Felder:
falsezufalsesollte 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_TIMEOUTWebSocket-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_CPUFirefox-spezifischer Fehler, der auf übermäßigen CPU-Verbrauch in Inhaltsprozessen hinweist, oft symptomatisch für JavaScript-unendliche Schleifen.LIT_UPDATE_CYCLE_EXCEEDEDLit-Framework-Warnung, wenn zu viele Render-Zyklen in einer einzigen Aufgabe auftreten. Kann in der Konsole erscheinen, bevor der Tab einfriert.FORM_VALIDATION_STALEZugehö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 propertiesFramework-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