April 21, 2026 • Versión: 2026.2.26

[Bucle de Renderizado Infinito en UI de Control con Campo de Nombre de Cron Job Vacío] - Control UI Infinite Render Loop on Empty Cron Job Name Field

Hacer clic en el campo de entrada Name en la pestaña Cron Jobs activa un bucle reactivo infinito debido a cambios de referencia de objeto en el handler onFormChange, causando 99.6% de uso de CPU y bloqueo del dashboard.

🔍 Síntomas

Manifestaciones técnicas

El problema se manifiesta como un bloqueo completo del lado del cliente al interactuar con un campo de entrada específico:

  • Pico de CPU: El proceso de contenido de Firefox consume 99.6% de los recursos de CPU disponibles de forma indefinida
  • Degradación de WebSocket: Las conexiones de gateway comienzan a agotar el tiempo de espera con errores de handshake timeout de 40s+
  • Falta de respuesta de la pestaña: El panel de control se congela completamente; ninguna interacción responde
  • Integridad del gateway: El gateway de OpenClaw permanece saludable y responde a otros clientes

Secuencia de reproducción

1. Navegar a: http://localhost:3000/__openclaw__/control/
2. Hacer clic en la pestaña "Cron Jobs"
3. Hacer clic en el campo de entrada "Name" (sin escribir)
4. El panel de control se congela inmediatamente

Indicadores de la consola del navegador

[Consola de Firefox DevTools]
⚠ Esta página parece tener un bucle de renderizado que se ha estado ejecutando
   por más tiempo del esperado. Considere optimizar el código del componente.
   Fuente: openclaw-control-ui/cron-form.js

Comportamiento del proceso

# Observado a través del administrador de tareas del navegador:
Pestaña del Panel PID 12345
- CPU: 99.6% (aumentando continuamente)
- Memoria: 847MB → 1.2GB (filtrándose durante el bucle)
- Estado: Ejecutándose (sin respuesta)

🧠 Causa raíz

El mecanismo del bucle reactivo

El bucle de renderizado infinito es causado por una dependencia circular entre el sistema de propiedades reactivas de Lit y los eventos de entrada nativos del DOM. El código problemático existe en el componente de formulario de Cron Jobs:

// Fuente problemática: src/components/cron-form.js (aprox. línea 47)
onFormChange: p => {
  e.cronForm = _o({...e.cronForm, ...p}),
  e.cronFieldErrors = Qn(e.cronForm)
}

Análisis de la secuencia de fallo

El bucle reactivo sigue esta ruta de ejecución precisa:

  1. El usuario hace clic en el campo de entrada vacío — El navegador dispara un evento nativo @input con el valor actual (vacío)
  2. Se ejecuta onFormChange — El manejador recibe {name: ""}
  3. El operador spread crea un nuevo objeto{...e.cronForm, ...{name: ""}} produce un nuevo objeto de referencia aunque el valor sea idéntico
  4. Lit detecta el cambio de propiedad — El setter de cronForm decorado con @state() o @property() dispara una re-renderización
  5. La re-renderización lee el valor del campo de entrada — El método render() del componente o el accesor de plantilla lee el valor actual del campo de entrada del DOM ("")
  6. El evento del DOM se dispara nuevamente — En algunas implementaciones, leer el valor del campo de entrada vuelve a disparar el manejador @input
  7. El bucle se repite — Los pasos 2-6 se ejecutan infinitamente

Diagrama de rotación de referencias de objetos

cronForm (inicial):   {name: "", schedule: "0 * * * *"}
                        ↓ El evento @input se dispara con {name: ""}
cronForm (reasignado):  {name: "", schedule: "0 * * * *"} ← NUEVA REFERENCIA DE OBJETO
                        ↓ Lit detecta el cambio mediante comparación !==
Re-renderizado activado
                        ↓ La plantilla lee input.value
@input se dispara de nuevo: {name: ""}
                        ↓ Misma estructura, NUEVA REFERENCIA
cronForm (reasignado):  {name: "", schedule: "0 * * * *"} ← EL BUCLE CONTINÚA

Por qué falla la verificación de igualdad estándar de Lit

Lit utiliza !== para la detección de cambios de propiedades:

// Setter interno de propiedades de Lit (simplificado)
set value(newVal) {
  if (this._value !== newVal) {  // Object !== Object (diferentes referencias)
    this._value = newVal;
    this.requestUpdate();        // SIEMPRE dispara la actualización
  }
}

El operador spread garantiza una nueva referencia de objeto, evitando cualquier optimización de igualdad de contenido.

🛠️ Solución paso a paso

Solución recomendada: Verificación de igualdad superficial

Modifique el manejador onFormChange para comparar los valores antes de la reasignación:

// ANTES (problemático)
onFormChange: p => {
  e.cronForm = _o({...e.cronForm, ...p}),
  e.cronFieldErrors = Qn(e.cronForm)
}

// DESPUÉS (corregido)
onFormChange: p => {
  const merged = { ...e.cronForm, ...p };
  
  // Verificación de igualdad superficial - solo actualizar si los valores realmente cambiaron
  const hasChanged = Object.keys(p).some(
    key => e.cronForm[key] !== merged[key]
  );
  
  if (hasChanged) {
    e.cronForm = _o(merged);
    e.cronFieldErrors = Qn(e.cronForm);
  }
}

Solución alternativa: Comparación profunda con JSON

Para estructuras de formulario anidadas, una comparación mediante serialización JSON proporciona una verificación exhaustiva:

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);
  }
}

Pasos de implementación

  1. Localice el archivo: src/components/cron-form.js o src/components/cron-jobs/cron-form.ts
  2. Encuentre el método onFormChange en la clase CronForm
  3. Reemplace el manejador con la versión corregida anterior
  4. Agregue una función de utilidad para reutilización si múltiples formularios tienen el mismo patrón
  5. Verifique la corrección usando los pasos de verificación a continuación

Solución preventiva: Utilidad de envoltura

Cree una utilidad reutilizable safeUpdateForm para aplicar en todos los componentes de formulario:

// src/utils/form-helpers.js

/**
 * Actualiza de forma segura un objeto de formulario reactivo, previniendo
 * re-renderizados innecesarios cuando los valores realmente no han cambiado.
 * 
 * @param {Object} currentForm - El estado actual del formulario
 * @param {Object} updates - Las actualizaciones parciales a aplicar
 * @param {Function} validator - Función de validación opcional
 * @returns {Object|null} - Nuevo estado del formulario o null si no cambió
 */
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;
}

// Uso en el componente:
onFormChange: p => {
  const newForm = safeUpdateForm(e.cronForm, p, _o);
  if (newForm) {
    e.cronForm = newForm;
    e.cronFieldErrors = Qn(e.cronForm);
  }
}

🧪 Verificación

Comandos de verificación y resultado esperado

Después de aplicar la corrección, realice los siguientes pasos de verificación:

1. Prueba de interacción funcional

# Haga clic en el campo Name y verifique que no hay pico de CPU
1. Abra DevTools del navegador (F12)
2. Navegue a la pestaña Cron Jobs
3. Haga clic en la pestaña Performance Monitor en DevTools
4. Observe: La CPU debe permanecer por debajo del 5% al interactuar con campos vacíos

2. Caso de prueba automatizado

Cree una prueba para verificar la corrección:

// 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"]');
    
    // Enfocar sin escribir
    nameInput.focus();
    
    // Esperar a que el posible bucle se manifieste
    await new Promise(r => setTimeout(r, 500));
    
    const updatesAfter = el.updateCount || 0;
    
    // No debería tener actualizaciones excesivas
    expect(updatesAfter - updatesBefore).to.be.lessThan(5);
  });
});

3. Lista de verificación de verificación manual

  • Monitoreo de CPU: El administrador de tareas del navegador muestra <5% de CPU durante inactividad e interacción normal
  • Enfoque de entrada: Hacer clic en el campo Name vacío enfoca sin efectos secundarios
  • Envío de formulario: El formulario de trabajo Cron se envía correctamente con datos válidos
  • Validación: Los mensajes de error se muestran correctamente para entradas inválidas
  • Salud de WebSocket: Las conexiones de gateway permanecen estables (sin tiempos de espera de 40s+)

4. Prueba de regresión

# Verificar que los cambios del formulario aún se propagan correctamente
1. Complete Name: "my-cron-job"
2. Complete Schedule: "0 * * * *"
3. Verifique que cronFieldErrors se actualiza apropiadamente
4. Envíe el formulario y confirme la creación del trabajo

⚠️ Errores comunes

Trampas específicas del entorno

  • Compilaciones de desarrollo vs. producción: El Hot Module Replacement (HMR) en desarrollo puede enmascarar el problema al restablecer el estado con más frecuencia. Pruebe en compilaciones de producción para confirmar la corrección.
  • Diferencias entre navegadores: Firefox ESR exhibe este comportamiento más prominentemente debido a su manejo de eventos. Chrome y Safari pueden ocultar el síntoma a través de sus optimizaciones internas, pero el bucle subyacente aún existe.
  • Límites del contenedor Docker: Al ejecutar el panel de control en Docker, los límites de CPU del contenedor pueden causar diferentes modos de fallo (eliminación de procesos vs. bucle infinito).

Errores de implementación a evitar

  1. Usar deepClone en lugar de comparación:
    // INCORRECTO - Costoso y no resuelve la causa raíz
    onFormChange: p => {
      e.cronForm = _o(JSON.parse(JSON.stringify({...e.cronForm, ...p}))),
      // ...
    }
    
  2. Saltarse la recalculación de validación:
    // INCORRECTO - Rompe la validación cuando los valores cambian legítimamente
    onFormChange: p => {
      const merged = { ...e.cronForm, ...p };
      // Siempre actualizar validación, incluso si la referencia del objeto difiere
      e.cronFieldErrors = Qn(merged);
      // ...
    }
    
  3. Comparar cadenas serializadas sin debounce:
    // RIESGOSO - Puede causar problemas con entradas sucesivas rápidas
    onFormChange: p => {
      // Si escribe rápido, esto puede omitir actualizaciones válidas
      if (JSON.stringify(e.cronForm) !== JSON.stringify({...e.cronForm, ...p})) {
        // ...
      }
    }
    

Casos límite

  • Valores null/undefined: Asegúrese de que la verificación de igualdad maneje null, undefined y cadena vacía de manera consistente
  • Cero numérico vs. vacío: 0 no debe tratarse como "sin valor"
  • Campos de casilla de verificación/booleanos: false a false no debería disparar actualizaciones
  • Campos de matriz: Si el formulario contiene matrices, use la comparación apropiada (superficial para la mayoría de los casos)

Patrones de componentes relacionados

Si otros componentes de formulario en la base de código usan el mismo patrón, audite y corríjalos:

# Buscar el patrón problemático en toda la base de código
grep -r "cronForm = _o({...e.cronForm" src/
grep -r "onFormChange.*spread" src/
grep -r "@state()\s*\n.*Form" src/

🔗 Errores relacionados

Problemas contextualmente conectados

  • WEB_SOCKET_HANDSHAKE_TIMEOUT
    Conexiones WebSocket que agotan el tiempo de espera debido a que la pestaña del navegador deja de responder. El gateway permanece saludable pero el cliente no puede procesar mensajes entrantes.
  • CONTENT_PROCESS_HIGH_CPU
    Error específico de Firefox que indica consumo excesivo de CPU en procesos de contenido, a menudo sintomático de bucles infinitos de JavaScript.
  • LIT_UPDATE_CYCLE_EXCEEDED
    Advertencia del framework Lit cuando ocurren demasiados ciclos de renderizado en una sola tarea. Puede aparecer en la consola antes de que la pestaña se congele.
  • FORM_VALIDATION_STALE
    Errores de inconsistencia de validación relacionados cuando el estado del formulario se actualiza más rápido de lo que la validación puede procesar.

Problemas históricos similares

  • Lit Issue #1427: Bucle infinito con spread de objetos en propiedades reactivas
    Documentación a nivel de framework del mismo patrón que afecta a otras aplicaciones basadas en Lit.
  • Comparación de igualdad de useState de React
    El mismo anti-patrón existe en React donde setState({...state, value}) causa re-renderizados infinitos sin la memorización adecuada.

Medidas preventivas para el desarrollo futuro

  • Regla de linting: Agregar regla de ESLint para detectar patrones de spread de objetos en setters de propiedades de Lit
  • Pruebas de componentes: Incluir conteo de ciclos de renderizado en las suites de pruebas de componentes
  • Presupuestos de rendimiento: Establecer alertas para re-renderizados excesivos en el pipeline de CI/CD

Evidencia y fuentes

Esta guía de solución de problemas fue sintetizada automáticamente por la tubería de inteligencia de FixClaw a partir de las discusiones de la comunidad.