El código llegaba en 100 ms, pero los widgets tardaban 380 ms
En Tab Widgets, una extensión de nueva pestaña para Chrome y Edge, el equipo asumió que el dashboard tardaba en llenarse porque el bundle era demasiado grande. Al perfilar una build de producción en Chromium headless descubrieron otra cosa: todos los scripts se cargaban y evaluaban en unos 100 ms, pero los widgets no aparecían hasta los 380 ms. Durante la mayor parte de esa brecha, el navegador no hacía absolutamente nada.
La causa no era el peso del código. Era React esperando a propósito. El arreglo más grande que aplicaron no eliminó ni un solo byte, y el que sí redujo el archivo principal no cambió nada medible. Esa distinción —entre tamaño y momento de aparición— es la lección central del caso.
Cómo estaba montada la app
Tab Widgets es una aplicación de React 19 construida con Rspack. Cada widget (reloj, notas, clima, Gmail, Spotify y decenas más) vive en su propio chunk de código y se carga con React.lazy, de modo que una pestaña nueva solo descarga el código de los widgets que realmente usas. Cada widget está dentro de su propio Suspense boundary, que muestra un placeholder de carga hasta que el widget está listo.
¿Y esto cómo se aplica en tu negocio?
Dueños de negocio de Latinoamérica y España usan un agente de IA para ganar más y recuperar su tiempo. El Kit «tu mano derecha con IA» te muestra cómo en un video de 11 minutos, con una ficha para armar el tuyo. CAR está cerrada por ahora; al anotarte, te avisamos cuando abra.
👥 Quiero el Kit →Al abrir una pestaña nueva, la app carga el bundle principal, lee las páginas y widgets guardados, carga el shell y la lista de widgets disponibles, renderiza la página y luego carga el código de cada widget a medida que se renderiza. Los archivos de una extensión se leen del disco local, no de la red: unos cientos de kilobytes extra cuestan muy poco. La versión web carga el mismo código por red, donde el tamaño pesa más, pero el problema de fondo es idéntico.
¿Por qué React retenía los widgets?
Dos comportamientos de React se combinan mal en el arranque.
Primero: un componente lazy siempre suspende la primera vez que se renderiza, incluso si su código ya está cargado. React.lazy solo llama a import() cuando se renderiza por primera vez, y recibe la respuesta a través de una promesa que siempre se resuelve después de ese render. Llamar a import() por tu cuenta antes no ayuda: el componente lazy inicia su propia importación al renderizarse, obtiene una promesa sin resolver y suspende igual.
Segundo: React limita la velocidad con la que revela el contenido suspendido. Cuando el contenido suspendido está listo, React solo lo muestra una vez transcurridos 300 ms desde la última vez que apareció o se reemplazó un fallback en cualquier parte de la página. Es el valor FALLBACK_THROTTLE_MS en el código fuente de React. Todo lo que esté listo dentro de esa ventana se revela junto. La documentación de Suspense lo describe como "como máximo una vez cada 300 ms, medido desde la última revelación".
Ese throttle es intencional: evita que la página parpadee cuando muchas piezas pequeñas terminan de cargar en momentos ligeramente distintos. Pero castiga justo el caso contrario, el de una app que arranca desde cero y cuyo contenido está listo casi de inmediato. Hay una discusión larga en GitHub sobre hacerlo configurable y, por ahora, el equipo de React lo mantiene como una heurística fija.
El resultado: cada widget de una pestaña nueva mostraba su placeholder y luego esperaba al menos 300 ms, sin importar cuán rápido llegara su código. Y como ese temporizador es compartido por toda la página, los widgets aparecían en tandas.
El primer intento: precargar antes del primer render
React expone internamente cómo se inicia un componente lazy: llama a component._init(component._payload). Si haces esa llamada tú mismo antes de que React renderice, la importación arranca antes y, cuando termina, el componente lazy ya no suspende. El equipo encapsuló esos campos privados en un archivo pequeño con tests unitarios, de modo que una actualización de React que los rompa falle en CI en lugar de perder la mejora en silencio.
En el arranque calculaban a qué página abre la pestaña, precargaban sus widgets y esperaban hasta 100 ms antes de renderizar. En Chromium headless, los widgets pasaron de unos 380 ms a unos 95 ms.
Pero una espera con tope tiene un punto débil: cualquier widget cuyo código tarde más que el tope suspende de todos modos y, como su fallback aparece más tarde, la ventana de 300 ms empieza y termina más tarde también. En un disco lento o una máquina ocupada, ese widget aparecería hasta 100 ms después que sin esperar. Además, sacar los modales del bundle de arranque introdujo una regresión: el spinner de carga se quedaba con el foco del teclado y cerrar el modal ya no devolvía el foco al botón que lo abrió. Lo detectaron los tests end-to-end y ahora precargan esos modales cuando la página está inactiva.
La solución que aguantó: no suspender en absoluto
En lugar de competir contra un temporizador, dejaron de hacer que los widgets suspendieran por su propio código. Cada widget ahora comprueba si su código ya cargó. Si no, renderiza el placeholder habitual como contenido normal y arranca la carga. Cuando el código llega, una actualización de estado normal sustituye el placeholder por el widget real.
El mecanismo es un hook pequeño construido sobre la precarga: si el componente está listo, se renderiza; si no, se muestra el placeholder y se dispara la carga. El Suspense y el error boundary siguen alrededor, porque siguen capturando una importación fallida y cualquier cosa que suspenda si los campos privados de React cambian algún día.
El retraso de 300 ms de React solo aplica cuando revela contenido que suspendió. Aquí no suspende nada, así que cada widget aparece en cuanto su código está dentro. El arranque sigue empezando a cargar los widgets de la página inicial antes del primer render, pero ya no espera por ellos.
El costo: se renuncia a lo que el throttle buscaba. Los widgets aparecen uno a uno según llega su código, en lugar de en una tanda. Para ellos está bien, porque cada marco de widget tiene un tamaño fijo en la grilla y el placeholder lo llena: nada se mueve cuando el widget entra. Aplicaron la misma idea al chequeo de permisos que ejecutan los widgets de temporizador, cuenta regresiva, Pomodoro, historial y marcadores antes de renderizar, que antes era una consulta con suspensión y ahora es una consulta normal.
¿Qué resultados concretos obtuvo?
Medición en Chrome sobre Windows 11, con cuatro pestañas nuevas en frío y un dashboard de 20 widgets (16 visibles, sesión cerrada):
| Media de 4 ejecuciones | Antes | Después | Cambio |
|---|---|---|---|
| Barra de navegación visible | 274 ms | 326 ms | +52 ms |
| Primer contenido de widget visible | 540 ms (todo a la vez) | 298 ms | −242 ms |
| 85% del contenido de widgets visible | 540 ms | 336 ms | −204 ms |
| Todo el contenido visible | 540 ms | 544 ms | +4 ms |
La barra de navegación se detecta unos 50 ms más tarde, pero no se renderiza más tarde: forma parte del mismo primer render que los placeholders y se desvanece en 150 ms, así que la captura la pilla a mitad del fade. En las mediciones basadas en DOM, llegaba unos 11 ms más tarde con el arreglo, dentro del ruido entre ejecuciones. Los últimos widgets tampoco se mueven: Weather y Word of the Day cargan su código y vuelven a suspender mientras piden sus datos, así que el throttle de 300 ms todavía los retiene. El arreglo sacó la carga de código de Suspense, no la carga de datos.
¿Qué significa esto para tu startup?
Si tienes un dashboard en React con widgets o tarjetas que se cargan por separado, este caso te afecta más de lo que crees. Tres cosas que puedes hacer esta semana:
- Mide cuándo aparece el contenido, no cuánto pesa el archivo. El equipo movió la librería de fechas fuera del archivo principal de 283 KB y no cambió nada medible: la librería solo se movió a otro archivo que el arranque seguía necesitando. Revertir ese cambio fue la decisión correcta. Antes de optimizar bytes, perfila una build de producción y anota el instante en que el usuario ve el primer dato útil.
- Busca el tiempo muerto. La ganancia más grande vino de una brecha en la que el hilo principal no hacía nada, no de hacer más rápido ningún código. Si en tu traza hay un hueco entre "todo cargado" y "algo visible", tienes un problema de orquestación, no de rendimiento bruto.
- Revisa si tu Suspense está en el camino crítico del primer render. Si cada tarjeta tiene su propio boundary y todas suspenden al montarse, el throttle de 300 ms se convierte en tu mayor latencia. Una salida sin tocar campos privados de React es mantener un mapa propio de módulos cargados: defines el loader a nivel de módulo, precargas, y renderizas el componente con una actualización de estado normal en lugar de dejar que suspenda.
Y una advertencia de producto: si tu equipo de diseño depende de que las tarjetas aparezcan en bloque para evitar saltos visuales, el cambio a "una por una" solo es seguro si cada contenedor tiene altura reservada. En Tab Widgets funcionó porque la grilla fija el tamaño de cada marco; en un layout fluido, verifica primero el layout shift.
Conclusión
El caso de Tab Widgets invierte el instinto habitual: el problema no era el tamaño del bundle sino una espera deliberada del framework. Reducir el archivo principal no movió la aguja; quitar la suspensión del camino crítico adelantó el primer widget unos 240 ms y el 85% del dashboard unos 200 ms en escritorio. Para un founder, la traducción es simple: antes de contratar más ancho de banda o reescribir el build, entiende qué está esperando tu interfaz y por qué. A veces el cuello de botella no está en tu código, está en una decisión de diseño del framework que tú puedes rodear.
Fuentes
- React.lazy, Suspense and the Hidden 300 ms Delay (fuente original)
¿Y esto cómo se aplica en tu negocio?
Dueños de negocio de Latinoamérica y España usan un agente de IA para ganar más y recuperar su tiempo. El Kit «tu mano derecha con IA» te muestra cómo en un video de 11 minutos, con una ficha para armar el tuyo. CAR está cerrada por ahora; al anotarte, te avisamos cuando abra.
👥 Quiero el Kit →














