Cuando cientos de compradores compiten en una subasta en vivo de frutas y verduras, el reloj en su pantalla es lo único que separa una buena oferta de una oportunidad perdida. El precio baja cada 50 milisegundos. Si dudas, alguien más puja primero. Si ves un precio desactualizado, pujas por algo que no pretendías.
Construimos el reloj de subastas en tiempo real para una plataforma belga que sirve a tres grandes casas de subastas. La primera versión se entregó rápido y funcionaba correctamente: un dial circular pintado a medida con 100 puntos de precio, actualizaciones en vivo por WebSocket y envío preciso de pujas. Hacía lo que tenía que hacer.
Pero “funciona correctamente” y “se siente bien” son dos cosas distintas. A medida que la plataforma escaló y observamos sesiones reales de subastas, vimos margen para llevar la experiencia más lejos. El reloj se actualizaba 20 veces por segundo, lo cual es técnicamente suficiente. Simplemente no se sentía fluido. Y en dispositivos más lentos, detectamos una brecha sutil entre lo que los compradores veían y lo que el sistema calculaba en el momento de la puja. Ambas eran oportunidades para elevar el nivel.
Dónde vimos margen de mejora
El punto se movía a saltos, no con fluidez. El punto activo tenía 100 posiciones discretas y saltaba entre ellas 20 veces por segundo. Funcionalmente correcto, pero visualmente entrecortado. En pantallas más grandes, donde cada salto cubre más píxeles, el efecto era más notable. Los compradores lo describían como “tembloroso”.
Los dispositivos lentos introducían un desfase temporal. En tablets de gama baja corriendo a 5-10fps, el precio mostrado en pantalla podía ir por detrás de la realidad entre 100 y 200 milisegundos. A velocidad de subasta, eso son cuatro pasos de precio. Si un comprador pulsaba “pujar”, una implementación ingenua recalcularía el precio a partir del tiempo real del sistema y enviaría un valor diferente al que se mostraba en pantalla. Lo detectamos a tiempo, pero necesitaba una solución robusta y permanente.
La clave
Ambas oportunidades apuntaban a la misma palanca arquitectónica: el reloj trataba el renderizado como una única operación a una única frecuencia.
Cada 50 milisegundos, el sistema recalculaba el precio, recoloreaba los 100 puntos, reposicionaba 10 etiquetas y movía el punto activo en una sola llamada de pintado. Entre esas actualizaciones, nada se movía. El modelo de renderizado era discreto cuando parte de él necesitaba ser continuo.
Reconocer esta distinción es lo que hizo que la optimización fuera directa. No todo en el reloj cambia a la misma velocidad ni cuesta lo mismo dibujar. Una vez que separamos esas responsabilidades, las soluciones surgieron de forma natural.
Lo que construimos
Separación del pipeline de renderizado
Separamos el reloj en dos painters con frecuencias de actualización independientes. El trabajo costoso (100 puntos, 10 etiquetas, coloreado condicional) se mantiene a la frecuencia natural del reloj. El trabajo ligero (un punto en movimiento) corre a la frecuencia de refresco de la pantalla. Cada painter se repinta de forma independiente. El punto nunca espera al dial.
Interpolación fraccional
En lugar de saltar a 100 posiciones enteras, el punto activo ahora interpola entre ellas usando precisión de sub-paso en cada frame. La posición del punto se calcula como un valor continuo en cada refresco de pantalla, no solo en los límites de cada tick. Esto le da al punto 3 veces más posiciones distintas por cada medio segundo, convirtiendo saltos visibles en movimiento fluido.
Defensa de congelación de puja
Cuando un comprador pulsa pujar, congelamos el reloj de forma síncrona y capturamos exactamente el precio que vio, no un valor recalculado a partir del tiempo del sistema. Flutter procesa los eventos de gestos antes que los callbacks de animación en su pipeline de frames. Aprovechamos esa garantía para detener el reloj en el instante en que se pulsa el botón de puja, antes de que el siguiente frame pueda avanzar el precio. La puja siempre coincide con lo que se mostraba en pantalla.
Inteligencia de salto de frames
Un mecanismo de control omite frames de animación en los que nada podría haber cambiado, reduciendo el cálculo innecesario en un 50% sin perder un solo paso de precio. A 60fps, el ticker se dispara cada 16ms. Pero el precio solo cambia cada 50ms. El mecanismo omite los frames que caen entre los límites de los ticks, reduciendo el cálculo innecesario en todos los relojes visibles, manteniendo el margen lo suficientemente ajustado para no perder nunca un paso.
Los resultados
El punto activo ahora se mueve a 60 frames por segundo. Movimiento fluido y continuo independientemente de la velocidad del reloj. El dial de fondo sigue repintándose a su frecuencia natural, manteniendo el uso de CPU bajo control. El mecanismo de congelación de puja garantiza cero desfase de precio en cualquier dispositivo, verificado mediante pruebas automatizadas que simulan escenarios de frames lentos.
Validamos el sistema completo bajo carga: 500 usuarios concurrentes, conexiones WebSocket reales, streaming de audio en tiempo real y pujas llegando simultáneamente. Los relojes se mantuvieron estables.
Lo que esto reforzó
Tres principios destacaron de este trabajo que se aplican a cualquier aplicación Flutter en tiempo real:
Esto no son trucos. Son el tipo de decisiones que surgen de una experiencia profunda con el motor de renderizado de Flutter y de trabajar en sistemas donde la corrección y la fluidez importan por igual.
¿Estás construyendo una aplicación Flutter en tiempo real y quieres llevar el rendimiento más lejos?