Ecosistema
Author
Piyal Basu
Publishing date
En Freighter, hemos estado obsesionados con hacer que nuestra extensión de navegador sea instantánea. Pero tras analizar métricas del mundo real, descubrimos que los tiempos de carga iniciales eran mucho más lentos de lo que queríamos. Los usuarios esperaban más de 3.5 segundos en promedio para que su billetera apareciera.
Para una extensión de navegador que los usuarios abren varias veces al día, esos segundos realmente se acumulan y crean mucha fricción para los usuarios frecuentes. Después de varias semanas de auditoría, perfilación y optimización, reducimos el tiempo de carga a 1.27 segundos, una mejora del 63% que hizo que la app se sintiera dramáticamente más rápida y receptiva.
Así es como encontramos nuestros cuellos de botella, los cambios que hicimos, y lo que planeamos a continuación.
Nuestro primer enfoque fue el tiempo de carga de la página inicial. Las extensiones se comportan de manera diferente a las aplicaciones web; no viven en una pestaña que permanece abierta. Los usuarios las abren y cierran frecuentemente a lo largo del día. Si cada apertura tarda varios segundos en cargar, la experiencia rápidamente se vuelve frustrante.
Medimos el rendimiento usando Largest Contentful Paint (LCP), recopilando datos del mundo real a través de nuestra herramienta de monitoreo web Sentry’s performance monitoring. En promedio, los usuarios enfrentaban un LCP de 3.5 segundos al abrir la vista de Cuenta. Fue importante usar Sentry para obtener una visión holística de lo que el usuario promedio estaba experimentando porque en nuestras pruebas locales, estábamos viendo tiempos de carga más cortos. Claramente nuestra experiencia no coincidía con la del usuario promedio.
Esos 3.5 segundos representaban el tiempo total desde la carga de la página, pasando por el spinner, hasta un estado completamente interactivo. Comenzamos investigando qué estaba contribuyendo a este tiempo de arranque de 3.5 segundos. Usando las herramientas de desarrollador de Chrome, simulamos diferentes velocidades de internet para identificar qué procesos estaban tomando más tiempo. En un esfuerzo por dar a los usuarios acceso a todos sus datos de una vez, estábamos esperando a que se resolvieran numerosas API’s antes de mostrar la UI. Algunos de estos procesos, como la obtención de iconos, tomaban mucho más tiempo que otros. Esto nos estaba ralentizando.
Y las ralentizaciones no se detuvieron ahí. Cada vez que los usuarios navegaban por la app, digamos, de Cuenta a Historial y de vuelta, estábamos re-obteniendo datos innecesariamente. Los saldos de cuenta que no habían cambiado se solicitaban una y otra vez. Esto añadía más demora y llamadas de red redundantes.
Nuestros objetivos se hicieron claros: bajar de 1.5 segundos para el LCP inicial y dejar de re-obtener datos innecesariamente.
Primero, abordamos el problema de los tiempos de carga lentos al iniciar la app. Estábamos esperando a que se cargara mucha información de API’s externas: saldos de cuenta, historial de cuenta, iconos de activos y más. Redujimos esto al mínimo necesario para mostrar al usuario su billetera, saldos de cuenta, para que los usuarios pudieran comenzar lo más pronto posible.
Todo lo demás, lo movimos a un proceso en segundo plano. Comenzamos a cargar el historial de cuenta del usuario, listas de activos e iconos de activos de manera asíncrona después de que los saldos de cuenta del usuario ya estaban cargados y visibles. Luego, cachamos estos datos. Esto hizo que la navegación fuera mucho más rápida. Ahora, cuando un usuario navegaba a Historial, por ejemplo, ya tendrían los datos necesarios cachados y listos para servir. No necesitarían esperar a que se resolviera una llamada a la API antes de renderizar la vista.
También renovamos cómo cargábamos los iconos de activos. Solíamos seguir un proceso de varios pasos:
El resultado eran 3 viajes de ida y vuelta para 1 pequeño archivo PNG! Y este proceso ocurría para cada activo que un usuario poseía. Aunque cachábamos los resultados de esta url de icono para prevenir esta búsqueda multi paso en el futuro, este fue un proceso doloroso hasta que pudimos cachar un resultado.
Para mejorar esto, comenzamos consultando las listas de activos del usuario para el icono. Dado que estas listas de activos representan algunos de los tokens más populares en el ecosistema de Stellar, sentimos confianza de que esto sería suficiente para un gran número de solicitudes de iconos.
Si eso no daba resultados, recurriríamos al método antiguo con una distinción importante: comenzamos a usar entradas de ledger de Stellar RPC para buscar múltiples cuentas a la vez. Ya no tendríamos que iterar sobre todos los activos del usuario, obteniendo la cuenta Horizon de cada uno en busca de un dominio principal. Ahora, podríamos hacer una sola solicitud y obtener todos los emisores de activos (y sus dominios principales) en una llamada.
Hablando de cachear, comenzamos a cachear los datos del usuario de manera más agresiva. Por cada pieza de información que cargábamos, la cachábamos hasta que el usuario cerraba la app. Esto hizo que navegar por la app fuera mucho más rápido ya que ya teníamos la mayoría de los datos necesarios. Para ayudar a asegurar que no mostrábamos al usuario datos obsoletos, si la caché duraba más de 3 minutos, forzaríamos una re-obtención. Además, si estábamos tomando una acción que sabíamos haría obsoleta la caché (como enviar un pago o añadir una línea de confianza), re-obtendríamos datos de manera oportunista.
El Historial de Cuenta proporcionó otra avenida para cachear. Al procesar transacciones pasadas en las que un usuario ha estado involucrado, a menudo estamos tratando con la misma información múltiples veces. Por ejemplo, un usuario puede estar interactuando con el mismo contrato de Soroban numerosas veces. No deberíamos estar re-obteniendo esos datos para ese contrato cada vez que aparece en Historial. En cambio, al iterar sobre transacciones de Historial, deberíamos estar cachando datos a medida que avanzamos para que estén inmediatamente disponibles para la próxima iteración. Logramos esto usando obtención por lotes.
En gran medida, nuestras medidas dieron resultado.
No solo lo vimos en los números. A los pocos días de la implementación, comenzaron a llegar comentarios de usuarios de todo el ecosistema: la gente notó y apreció cuán rápido se sentía ahora Freighter.
Aunque es genial estar en la zona verde, todavía hay trabajo por hacer.
Hoy, nuestras consultas de backend usan la API Horizon de Stellar para datos del ledger. Pronto, migraremos a nuestro propio backend de billetera, optimizado específicamente para billeteras. Al servir solo los datos mínimos que una billetera necesita, nuestra llamada a la API de saldo de cuenta debería resolverse aún más rápido.
También estamos construyendo un modo de panel lateral que permite a los usuarios fijar Freighter al lado de su navegador. Dado que las extensiones se cierran automáticamente cada vez que haces clic fuera, esta única característica tiene el potencial de eliminar por completo las esperas de inicio para los usuarios que mantienen el panel abierto mientras navegan.
Desglosar los datos de Sentry por geografía reveló variaciones notables en los tiempos de LCP a través de las regiones. Con ayuda del monitoreo sintético a través de New Relic, identificamos una mayor latencia de red para nuestros puntos finales de API desde ciertas áreas del mundo. Como siguiente paso, planeamos desplegar caché regional y servidores para mejorar el rendimiento a nivel global.
Optimizar Freighter no solo se trataba de reducir milisegundos de los tiempos de carga, sino de hacer que la aplicación se sintiera viva de nuevo. Al replantear nuestro flujo de datos, estrategia de caché y diseño de API, convertimos una extensión que se sentía lenta en una que se siente instantánea.
Y no nos detendremos aquí. Con un backend más rápido y un modo de panel lateral en camino, nuestro próximo objetivo es aún más ambicioso: llevar el tiempo de carga de Freighter a menos de un segundo en cualquier parte del mundo.



