Ecosistema

Haciendo Freighter Más Rápido: Cómo Mejoramos los Tiempos de Carga en un 63%

Autor

Jake Urban

Fecha de publicación

En Freighter, hemos estado obsesionados con hacer que nuestra extensión de navegador se sienta 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, redujimos 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.

Lo Que Estaba Mal

Nuestro primer enfoque fue el tiempo de carga de la página inicial. Las extensiones se comportan 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. Era 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 inicio 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 desaceleraciones 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 agregaba 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.

Lo Que Hicimos

Paso 1: Cargar Solo lo Necesario

Primero, abordamos el problema de los tiempos de carga lentos al inicio de 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 antes posible.

Paso 2: Precargar Datos

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, almacenamos estos datos en caché. 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 almacenados en caché y listos para servir. No necesitarían esperar a que se resolviera una llamada a la API antes de renderizar la vista.

Paso 3: Carga de Iconos de Activos Más Inteligente

También renovamos cómo cargábamos los iconos de activos. Solíamos seguir un proceso de varios pasos:

  1. Cargar la cuenta del emisor de cada activo desde Horizon y luego obtener el dominio principal de ese activo desde la búsqueda en Horizon. 😐
  2. Cargar el archivo toml adjunto a este dominio principal. 🥴
  3. Ver si el emisor del activo listaba un icono, y si lo hacía, cargarlo. 😩

El resultado era 3 viajes de ida y vuelta por 1 pequeño archivo PNG! Y este proceso ocurriría para cada activo que un usuario poseía. Aunque almacenábamos en caché los resultados de esta url de icono para prevenir esta búsqueda múltiple en el futuro, este era un proceso doloroso hasta que podíamos almacenar un resultado en caché.

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, nos sentimos confiados de que esto sería suficiente para un gran número de solicitudes de iconos.

Si eso no daba ningún resultado, 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.

Paso 4: Almacenar Datos del Usuario Agresivamente en Caché

Hablando de almacenar en caché, comenzamos a almacenar los datos del usuario de manera más agresiva. Por cada pieza de información que cargábamos, la almacenábamos en caché hasta que el usuario cerraba la app. Esto hizo que navegar por toda 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áramos 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 el almacenamiento en caché. 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 su lugar, al iterar sobre las transacciones de Historial, deberíamos estar almacenando datos en caché a medida que avanzamos para que estén inmediatamente disponibles para la próxima iteración. Logramos esto usando obtención por lotes.

Lo Que Encontramos

En general, nuestras medidas dieron resultado.

  • LCP mejoró de 3.5s → 1.27s, situándose firmemente en el rango de rendimiento “Bueno” según las directrices de Web Vitals de Sentry.
  • La interfaz se sintió dramáticamente más ágil.
  • La navegación entre vistas se volvió casi instantánea, ya que la mayoría de las solicitudes ahora se resolvían desde la caché o las actualizaciones de datos en segundo plano.

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.

Lo Que Sigue

Aunque es genial estar en la zona verde, todavía hay trabajo por hacer.

1. Pasar a una Fuente de Datos Personalizada

Hoy, nuestro backend consulta la API de 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.

2. Modo Panel Lateral para Acceso Persistente

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.

3. Mejorar los Tiempos de Respuesta Globales

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 globalmente.

Concluyendo

Optimizar Freighter no solo se trataba de reducir milisegundos en 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.