A toda marcha con un enfoque sistemático al rendimiento web

Foto por toineG en Unsplash

Este artículo fue publicado originalmente el 28 de abril de 2021 en eng.lyft.com

Escrito por Mihir Mathur. Traducido por Fernando A. López P., con la ayuda de los editores del Engineering Blog en Español.

Desde reproducir viajes compartidos en un mapa hasta resolver problemas de seguridad física en tiempo real, administrar una flotilla de miles de bicicletas y monopatines, o ver las trayectorias de automóviles autónomos, los servicios de frontend en Lyft atienden una amplia cantidad y variedad de casos de uso.

Así como ha crecido Lyft durante la última década, también ha crecido la complejidad de nuestras necesidades de negocio. Sin embargo, hay un requisito central para satisfacer dichas múltiples necesidades, y es tener aplicaciones web de alto rendimiento.

Platicamos con nuestros ingenieros senior y staff que están familiarizados con la historia de la ingeniería de frontend en Lyft para entender cómo hemos navegado a través de los desafíos de rendimiento y construido un ecosistema que da soporte a una gran cantidad de casos de uso que necesitan aplicaciones de alto rendimiento. Mirando atrás en nuestro camino de haber creado más de 100 microservicios de alto rendimiento en Lyft, hemos destilado nuestro aprendizaje en lo que llamamos la jerarquía de necesidades de rendimiento web, un sistema que puede identificar estratégicamente cuáles de las necesidades de rendimiento en una organización que crea aplicaciones web tendrán el mayor impacto.

La jerarquía de necesidades de rendimiento web

En este post describiremos este esquema y echaremos un vistazo a nuestro siempre cambiante stack de rendimiento de frontend. Este esquema puede ayudar a quienes conozcan las mejores prácticas de rendimiento web pero han tenido preguntas como:

  • ¿Cómo cuantificar y medir el impacto en el negocio de las mejoras en rendimiento para darles prioridad sobre el desarrollo de nueva funcionalidad?
  • ¿Qué tipo de herramientas y técnicas de rendimiento se deben hacer o construir primero?
  • ¿Cómo influir en la cultura de toda la organización para darle valor al alto rendimiento?

La necesidad más básica: medir y monitorear

La frase popular “si no lo puedes medir, no lo puedes mejorar” sigue siendo cierta para el rendimiento web.

Hace seis años, una de las primeras inversiones en rendimiento de Lyft fue capturar, ver y analizar datos de rendimiento de usuarios reales (RUM, por sus siglas en inglés: Real User Monitoring). Funcionaba corriendo un script en cada aplicación web que usaba el (ya deprecado) API PerformanceTiming para registrar cuándo sucedían eventos como requestStart, domLoaded y domInteractive. Estos datos se enviaban asíncronamente a un punto final de analítica usando Navigator.sendBeacon() al momento de cargar la página y se guardaban en nuestros almacenes de datos para su análisis.

El año pasado comenzamos a utilizar el nuevo estándar de métricas de rendimiento web: los Core Web Vitals de Google. Para registrar fácilmente estas métricas en todas nuestras aplicaciones, creamos un envoltorio alrededor de la biblioteca web-vitals, el cual envía métricas a nuestro punto final de analítica y puede ser usado así:

import WebVitalsTracker from '@lyft/web-vitals-tracker';

export class App extends React.Component<AppProps> {
render() {
return (
<div>
<WebVitalsTracker sendingService='miservicio' />
</div>
);
}
);

Habiendo recolectado estos datos, necesitamos herramientas para examinarlos y entenderlos mejor. Usamos dos herramientas de terceros para este propósito:

Métricas Core Web Vitals en el reporte de Mode para un servicio
  • Grafana: Para observabilidad de métricas en tiempo real e integración con alertas.
  • Mode: Para análisis avanzados de tendencias de datos (guardados en nuestros clústers de Hive) usando queries a la medida de Presto.

Formando una hipótesis de rendimiento

Equipado con herramientas para capturar y analizar datos de rendimiento, uno podría saber algunos pasos accionables para mejorar métricas. Pero simplemente saber qué hacer para mejorar métricas de rendimiento en una organización en la que ya hay planeado tanto trabajo de creación de nueva funcionalidad es, con frecuencia, insuficiente. ¿Cómo uno puede convencer a su equipo y a la directiva de dedicar tiempo y recursos ingenieriles para mejorar el desempeño?

Un método que se toma en Lyft es aunar los datos de desempeño a métricas claves del negocio. Articular y comunicar la hipótesis de rendimiento en el formato: “incrementar <métrica_de_rendimiento> en X% aumentaría / disminuiría <métrica_de_negocio> en aproximadamente Y%” a los interesados puede ayudar a darle prioridad al rendimiento.

Sin embargo, en lo que se refiere a formular tales hipótesis, del dicho al hecho hay un gran trecho. En primer lugar, los miembros de ingeniería deben estar equipados con datos, tanto de rendimiento como del negocio. En segundo lugar, la relación causal entre una métrica de rendimiento y una de negocio puede no siempre estar clara. El primer problema se resuelve con una cultura de transparencia de datos. Una herramienta que puede ayudar a democratizar el acceso a datos para todos los interesados en una organización es Amundsen, el motor de descubrimiento de datos de código abierto de Lyft.

Para el segundo problema puede ser de ayuda pensar cómo les afectaría a los usuarios la mejora significativa de una métrica de rendimiento. ¿Pueden hacer más en menos tiempo? ¿Será más probable que regresen al sitio? ¿Recomendarán la app a más personas?

Optimizaciones en tiempo de carga

Una de las mejoras de rendimiento con mayor impacto que pueden hacerse, especialmente en productos web para el consumidor, es hacer que las páginas carguen lo más pronto posible. Un estudio reciente encontró que las páginas web de carga lenta causan un aumento significativo en la presión arterial de los usuarios y provocan estrés. Lo que es más, crear una hipótesis de rendimiento que ata una métrica de carga de página (por ejemplo, Largest Contentful Paint, Speed Index, Time To Interactive) a valor de negocio (como $ en ventas, suscripciones, ahorros en costo) puede ser relativamente sencillo, dado que hay muchos ejemplos en los que pueden basarse estas hipótesis.

Por ejemplo, en 2015, cuando usábamos Angular 1.3 en el frontend, se hizo un proyecto para hacer que la página de inscripción para conductores de Lyft cargara más rápido. La hipótesis era que la reducción en el tiempo de carga de la página llevaría a un aumento de inscripciones. Se introdujeron una serie de mejoras, como usar webpack para partir bundles (bundle splitting), migrar a React para hacer más rápida la renderización (entre otras razones), habilitar renderización del lado del servidor y quitar CSS excesivo. Estas mejoras llevaron a una reducción en el tiempo de carga de la página de más de 2 segundos a unos cuantos cientos de milisegundos, lo que se tradujo en un 9% de incremento en inscripciones de conductores al final del embudo.

ride.lyft.com: una de las aplicaciones para clientes de Lyft.

En Lyft hacemos varias optimizaciones en tiempo de carga, como la renderización del lado del servidor, la división del código (code-splitting) de las apps, la compresión Brotli de archivos estáticos, la pre-carga (pre-fetching) de contenido, la carga dinámica de Javascript pesado, el uso de fuentes web de respaldo, la configuración de bibliotecas para hacerles tree-shaking, entre muchas otras. La mayoría de estas optimizaciones suceden durante la compilación.

A pesar de todas las optimizaciones que hereda cada uno de nuestros servicios de frontend, a veces erramos. Por ejemplo, nos ha pasado que una misma biblioteca se empaqueta en más de un bundle de JS para la misma app. Gracias a nuestras herramientas podemos echarle un ojo a las regresiones. Por ejemplo, registramos el tamaño de los bundles en cada compilación y usamos webpack-bundle-analyzer para auditar periódicamente los bundles de nuestros servicios y quitarles tantos kilobytes de Javascript como sea posible.

Si bien las optimizaciones en tiempo de carga muchas veces se pueden correlacionar fácilmente con métricas de negocio, el impacto anticipado no siempre se hace presente. Por ejemplo, tratando de mejorar la tasa de conversión de una de nuestras páginas para usuarios, hicimos una prueba A/B de una mejora del lado del servidor que decrementó el Time to First Byte (TTFB, en español: Tiempo hasta el primer byte) en 100ms para p50 y en más de 3 segundos para p95. La tasa de conversión para esa página casi no cambió. Aunque lanzamos a producción el cambio, aprendimos que las optimizaciones de tiempo de carga no siempre mueven la aguja de las métricas de negocio.

Optimizaciones de rendimiento en tiempo de ejecución

Con algunas de las optimizaciones mencionadas arriba, se puede hacer que un sitio web cargue rápidamente. Pero para realmente deleitar a los usuarios, cada interacción posible en una aplicación debe sentirse instantánea (es decir, debe haber respuestas visibles a cada interacción dentro de 100ms). Nos esforzamos por crear tales experiencias.

Sin embargo, tener interacciones rápidas y agradables es difícil cuando la aplicación es intensiva en computaciones o en cantidad de datos. Un ejemplo como tal es la aplicación usada por nuestros agentes de soporte para resolver problemas de conductores y pasajeros. Esta aplicación pesada proporciona una interfaz para hablar por teléfono y chatear en tiempo real, la cual está integrada a un CRM (sistema de administración de relaciones con los clientes, por sus siglas en inglés) para poder, al mismo tiempo, revisar rápidamente el historial de viajes, pagos o historial de soporte de varios usuarios. Otro ejemplo de un servicio de frontend computacionalmente intenso del lado del cliente es nuestra aplicación interna en la que los equipos de operaciones administran las flotillas de bicicletas y monopatines. Sus usuarios necesitan rápidamente acercar o alejar el zoom del mapa al tiempo que ven una gran cantidad de información encima de él.

Nuestra app interna para administrar flotillas de bicicletas y monopatines.

Estos servicios, como muchos otros, son usados por cada usuario interno por muchas horas al día. Por lo tanto, habilitar interacciones rápidas es vital para su productividad. Algunas cosas que hacemos para mejorar el rendimiento en tiempo de ejecución son:

  • Reducir la “cola larga” de latencias de llamadas a APIs: Examinando la latencia de cada llamada a APIs podemos dar prioridad a la mejora de las peticiones más lentas.
  • Procesar peticiones en lotes: Podemos reducir significativamente el número de peticiones de salida al procesarlas en lotes (batching). Por ejemplo, las métricas y logs pueden enviarse a un punto final de analítica en un solo lote en vez de individualmente.
  • Obtención inteligente de datos: Usamos GraphQL o APIs paginadas para obtener datos y damos prioridad a obtener datos para componentes que aparecen sobre el doblez (above-the-fold).
  • Memorización (memoizing): Guardamos en memoria componentes de frecuente renderización usando React.memo() y el hook useMemo.
  • Perfilado de componentes e interacciones: Analizamos el tiempo de renderización, scripting y pintado de componentes o interacciones complejas para escoger sus mejores optimizaciones.

React profiler y Chrome DevTools son muy buenas herramientas para el perfilado de rendimiento en tiempo de ejecución. Estos perfiladores, junto con un sólido conjunto de herramientas de medición y monitoreo que les permitan a los usuarios registrar cualquier métrica en tiempo de ejecución y visualizarla a través del tiempo, pueden ayudar a encontrar las partes más lentas de una aplicación y hacer más fácil la priorización.

Infraestructura de alto rendimiento: Herramientas y velocidad de lxs desarrolladorxs

Una vez que las páginas web cargan bien rápido y cada interacción se completa en un chasquido — ¿en qué debemos enfocarnos? El siguiente paso es asegurarnos de que cada nueva aplicación creada en la empresa herede instrumentación para el monitoreo del rendimiento, tenga rápidas velocidades de carga y tenga excelente rendimiento en tiempo de ejecución — todo con una inversión mínima de parte de lxs desarrolladorxs.

Esto está en la punta de la pirámide porque uno puede condensar los aprendizajes de todos los problemas de rendimiento ya resueltos y extraerlos en el sistema de compilación o en primitivas reutilizables. Un nuevo miembro de ingeniería con muy poca experiencia podría entonces, automágicamente, escribir código frontend de alto rendimiento usando las primitivas.

Aún más: en una infraestructura web de alto rendimiento ideal, debería ser muy difícil añadir código de bajo rendimiento. Esto se puede lograr mediante una combinación de abstracciones, procesos y educación. Algunos de los bloques de construcción de nuestra arquitectura de alto rendimiento son:

  • lyft-node-service: Nuestra infraestructura basada en NextJS hace que sea fácil crear un nuevo servicio de frontend con la mayoría de las optimizaciones de rendimiento necesarias ya instaladas.
  • Sistema de diseño Lyft Product Language: Nuestro sistema de diseño y su correspondiente biblioteca de componentes de UI accesibles y de alto rendimiento proporcionan bloques de construcción para aplicaciones de alto rendimiento.
  • Plugins y migraciones: Hemos construido un sistema interno de plugins que le permite a desarrolladorxs compartir funcionalidades fácilmente con otros servicios en Lyft, de modo que la optimización implementada por un equipo (por ejemplo, compresión de imágenes) pueda ser fácilmente distribuida a cualquier otro servicio. Las migraciones son scripts de jscodeshift que aplican cambios o mejoras a todos nuestros servicios de frontend. Por ejemplo, una mejora de rendimiento que necesita que una biblioteca se actualice o que se edite algún código puede ser aplicada de forma automática a todos los servicios para que, desde la perspectiva de lxs desarrolladorxs, el cambio no rompa funcionalidad. Esta plática de Andrew Hao explica los plugins y las migraciones en más detalle.
  • Frontend Performance Force: Un grupo de trabajo de ingenieros/as de diferentes equipos que buscan mejorar el rendimiento web en Lyft. Este grupo identifica nuevas áreas de rendimiento en las cuales enfocarse, comparte aprendizajes, crea recursos educacionales y busca construir una cultura que prioriza el rendimiento en Lyft.

¿Por qué no invertir la pirámide?

La jerarquía de necesidades de rendimiento web presentada aquí es una heurística para priorización basada en nuestros aprendizajes en Lyft. Una pregunta posible es: ¿Podría invertirse la pirámide? Es decir, dar prioridad a la construcción de infraestructura y herramientas compartidas para mejoras de desempeño, después hacer optimizaciones en tiempo de carga y en tiempo de ejecución, seguidos por mediciones y monitoreo.

Uno de los problemas de no invertir primero en herramientas de medición al inicio es que la priorización no se basaría en datos, por lo que sería difícil justificar su valor de negocios. Lo que es más, embeber mejoras de desempeño en la infraestructura compartida (si es que la hay) sería complicado para organizaciones con pocos recursos y mucho trabajo pendiente en la creación de nueva funcionalidad.

Además, uno de los beneficios de dar más prioridad a optimizaciones individuales en tiempo de carga y en tiempo de ejecución que a la preparación de infraestructura de alto desempeño es que la organización debe aprender qué optimizaciones son lo suficientemente importantes como para ser extraídas a la capa de infraestructura. Sin embargo, una salvedad en la jerarquía que hemos presentado es que si una aplicación web tiene una audiencia cautiva (por ejemplo, herramientas internas, software empresarial), entonces las optimizaciones en tiempo de ejecución pueden tener mayor prioridad que las de tiempo de carga.

Cuando no se presta atención al rendimiento, la lentitud puede matar negocios silenciosamente. Hemos aprendido que debemos pensar en el rendimiento desde el principio, y una jerarquía de necesidades puede ayudar en dar prioridad a las mejoras de rendimiento que deben hacerse.

Si te interesa trabajar con nosotros en hacer software de alto rendimiento o resolver otros complejos desafíos en el área del transporte para crear el mejor servicio de transporte del mundo, ¡nos encantaría saber de ti! Visita www.lyft.com/careers para ver nuestras vacantes.

Reconocimientos

¡Queremos agradecer a Andrew Hao y a Eric Bidelman por proporcionar ideas y revisar este artículo, así como a Michael Rebello por editarlo! Un reconocimiento a Joshua Callender, Ryan Jadhav y Xiaotian Hao por proporcionar algunos de los ejemplos mencionados.

--

--