Un caso de éxito real de la optimización del rendimiento de React SPA.
El rendimiento del sitio web no se trata solo del tiempo de carga. Es fundamental proporcionar una experiencia rápida y responsiva a los usuarios, especialmente para las apps de productividad para computadoras de escritorio que las personas usan todos los días. El equipo de ingeniería de Recruit Technologies realizó un proyecto de refactorización para mejorar una de sus apps web, AirSHIFT, y lograr un mejor rendimiento de las entradas del usuario. A continuación, te mostramos cómo lo hicieron.
Respuesta lenta y menos productividad
AirSHIFT es una aplicación web para computadoras de escritorio que ayuda a los propietarios de tiendas, como restaurantes y cafeterías, a administrar el trabajo por turnos de su personal. Creada con React, la aplicación de una sola página proporciona funciones de cliente enriquecidas, incluidas varias tablas de cuadrícula de horarios de turnos organizados por día, semana, mes y mucho más.
A medida que el equipo de ingeniería de Recruit Technologies agregaba nuevas funciones a la app de AirSHIFT, comenzaron a ver más comentarios sobre el rendimiento lento. Yosuke Furukawa, gerente de Ingeniería de AirSHIFT, dijo lo siguiente:
En un estudio de investigación sobre usuarios, nos sorprendió cuando una de las propietarias de la tienda dijo que se levantaba de su asiento para preparar café después de hacer clic en un botón, solo para matar el tiempo mientras esperaba que se cargara la tabla de turnos.
Después de realizar la investigación, el equipo de ingeniería se dio cuenta de que muchos de sus usuarios intentaban cargar enormes tablas de mayúsculas en computadoras de baja especificaciones, como una laptop Celeron M de 1 GHz de hace 10 años.
La app de AirSHIFT bloqueaba el subproceso principal con secuencias de comandos costosas, pero el equipo de ingeniería no se dio cuenta de lo costosas que eran las secuencias de comandos porque desarrollaban y probaban en computadoras con especificaciones avanzadas y conexiones Wi-Fi rápidas.
Después de generar perfiles de su rendimiento en Chrome DevTools con la limitación de la CPU y la red habilitadas, fue evidente que se necesitaba la optimización del rendimiento. AirSHIFT formó un equipo de trabajo para abordar este problema. Aquí hay 5 aspectos en los que se enfocaron para que su app fuera más responsiva a las entradas del usuario.
1. Virtualiza tablas grandes
Para mostrar la tabla de turnos, se requerían varios pasos costosos: construir el DOM virtual y renderizarlo en la pantalla en proporción a la cantidad de miembros del personal y de los horarios disponibles. Por ejemplo, si un restaurante tuviera 50 miembros que trabajan y quisiera consultar su programa de turnos mensual, sería una tabla de 50 (miembros) multiplicados por 30 (días), lo que generaría 1,500 componentes de celda para renderizar. Esta es una operación muy costosa, especialmente para dispositivos con especificaciones bajas. En realidad, la situación era peor. A partir de la investigación, descubrió que había tiendas que administraban 200 miembros del personal, lo que requería alrededor de 6,000 componentes de celdas en una sola tabla mensual.
Para reducir el costo de esta operación, AirSHIFT virtualizó la tabla de turnos. Ahora, la app solo activa los componentes dentro de la ventana de visualización y desactiva los componentes fuera de la pantalla.
En este caso, AirSHIFT usó react-virtualized, ya que había requisitos para habilitar tablas de cuadrículas complejas de dos dimensiones. También están explorando formas de convertir la implementación para usar react-window ligero en el futuro.
Resultados
La virtualización de la tabla por sí sola redujo el tiempo de secuencia de comandos en 6 segundos (4 veces más lento de ralentización de CPU + Rápida 3G limitada para Macbook Pro). Esta fue la mejora de rendimiento más impactante en el proyecto de refactorización.
2. Cómo realizar auditorías con la API de User Timing
A continuación, el equipo de AirSHIFT refactorizó las secuencias de comandos que se ejecutan en la entrada del usuario. El diagrama de llama de Chrome DevTools permite analizar lo que realmente sucede en el subproceso principal. Sin embargo, al equipo de AirSHIFT le resultó más fácil analizar la actividad de la aplicación según el ciclo de vida de React.
React 16 proporciona su seguimiento de rendimiento a través de la API de User Timing, que puedes visualizar en la sección Timings de Chrome DevTools. AirSHIFT usó la sección Timings para encontrar una lógica innecesaria que se ejecutaba en los eventos del ciclo de vida de React.
Resultados
El equipo de AirSHIFT descubrió que se estaba produciendo una Reconciliación de árbol de React innecesaria justo antes de cada navegación de ruta. Esto significaba que React actualizaba la tabla de turnos de forma innecesaria antes de las navegaciones. Una actualización de estado de Redux innecesaria causaba este problema. La corrección permitió ahorrar alrededor de 750 ms de tiempo de secuencias de comandos. AirSHIFT también realizó otras microoptimizaciones que, en última instancia, generaron una reducción total de 1 segundo en el tiempo de escritura de secuencias.
3. Carga diferida de componentes y traslado de lógica costosa a Web Workers
AirSHIFT tiene una aplicación de chat integrada. Muchos propietarios de tiendas se comunican con su personal a través del chat mientras miran la tabla de turnos, lo que significa que un usuario podría estar escribiendo un mensaje mientras se carga la tabla. Si el subproceso principal está ocupado con secuencias de comandos que renderizan la tabla, la entrada del usuario podría ser inestable.
Para mejorar esta experiencia, AirSHIFT ahora usa React.lazy y Suspense para mostrar marcadores de posición para el contenido de la tabla mientras carga de forma diferida los componentes reales.
El equipo de AirSHIFT también migró parte de la lógica empresarial costosa dentro de los componentes cargados de forma diferida a trabajadores web. De esta manera, se resolvió el problema de bloqueo de entrada del usuario, ya que liberó el subproceso principal para que pudiera enfocarse en responder a las entradas del usuario.
Por lo general, los desarrolladores enfrentan complejidades a la hora de usar trabajadores, pero esta vez Comlink se encarga del trabajo pesado. A continuación, se muestra el pseudocódigo de cómo AirSHIFT automatizó una de las operaciones más costosas que tenían: calcular los costos laborales totales.
En App.js, usa React.lazy y Suspense para mostrar contenido alternativo durante la carga
/** App.js */
import React, { lazy, Suspense } from 'react'
// Lazily loading the Cost component with React.lazy
const Hello = lazy(() => import('./Cost'))
const Loading = () => (
<div>Some fallback content to show while loading</div>
)
// Showing the fallback content while loading the Cost component by Suspense
export default function App({ userInfo }) {
return (
<div>
<Suspense fallback={<Loading />}>
<Cost />
</Suspense>
</div>
)
}
En el componente Cost, usa comlink para ejecutar la lógica de cálculo
/** Cost.js */
import React from 'react';
import { proxy } from 'comlink';
// import the workerlized calc function with comlink
const WorkerlizedCostCalc = proxy(new Worker('./WorkerlizedCostCalc.js'));
export default async function Cost({ userInfo }) {
// execute the calculation in the worker
const instance = await new WorkerlizedCostCalc();
const cost = await instance.calc(userInfo);
return <p>{cost}</p>;
}
Implementa la lógica de cálculo que se ejecuta en el trabajador y exponla con un comlink.
// WorkerlizedCostCalc.js
import { expose } from 'comlink'
import { someExpensiveCalculation } from './CostCalc.js'
// Expose the new workerlized calc function with comlink
expose({
calc(userInfo) {
// run existing (expensive) function in the worker
return someExpensiveCalculation(userInfo);
}
}, self);
Resultados
A pesar de la cantidad limitada de lógica que trabajaron como prueba, AirSHIFT cambió alrededor de 100 ms de su JavaScript del subproceso principal al subproceso de trabajo (simulado con una limitación de CPU 4x).
Actualmente, AirSHIFT está explorando si pueden cargar de forma diferida otros componentes y descargar más lógica en los trabajadores web para reducir aún más el bloqueo.
4. Cómo establecer un presupuesto de rendimiento
Después de implementar todas estas optimizaciones, era fundamental asegurarse de que la app mantuviera su rendimiento con el tiempo. AirSHIFT ahora usa bundlesize para no exceder el tamaño actual de los archivos JavaScript y CSS. Además de establecer estos presupuestos básicos, compilaron un panel para mostrar varios percentiles del tiempo de carga de la tabla de turnos y verificar si la aplicación tiene un buen rendimiento incluso en condiciones no ideales.
- Ahora se mide el tiempo de finalización de la secuencia de comandos para cada evento de Redux.
- Los datos de rendimiento se recopilan en Elasticsearch.
- El rendimiento de los percentiles 10, 25, 50 y 75 de cada evento se visualiza con Kibana.
AirSHIFT ahora supervisa el evento de carga de la tabla de turnos para asegurarse de que se complete en 3 segundos para los usuarios del percentil 75. Por el momento, este es un presupuesto no aplicado, pero están considerando notificaciones automáticas a través de Elasticsearch cuando se exceda el presupuesto.
Resultados
En el gráfico anterior, puedes ver que AirSHIFT ahora alcanza principalmente el presupuesto de 3 segundos para los usuarios del percentil 75 y también carga la tabla de cambio en un segundo para los usuarios del percentil 25. A través de la captura de datos de rendimiento de RUM de varias condiciones y dispositivos, AirSHIFT ahora puede verificar si el lanzamiento de una nueva función afecta o no el rendimiento de la aplicación.
5. Hackatones de rendimiento
Si bien todos estos esfuerzos de optimización del rendimiento fueron importantes y eficaces, no siempre es fácil lograr que los equipos de ingeniería y de la empresa prioricen el desarrollo no funcional. Parte del desafío es que algunas de estas optimizaciones de rendimiento no se pueden planificar. Requieren experimentación y una mentalidad de prueba y error.
AirSHIFT ahora realiza hackathons internos de rendimiento de 1 día para permitir que los ingenieros se enfoquen solo en el trabajo relacionado con el rendimiento. En estos hackathons, se quitan todas las restricciones y se respeta la creatividad de los ingenieros, lo que significa que vale la pena considerar cualquier implementación que contribuya a la velocidad. Para acelerar el hackatón, AirSHIFT divide el grupo en equipos pequeños, y cada uno de ellos compite para ver quién logra la mayor mejora en la puntuación de rendimiento de Lighthouse. Los equipos se vuelven muy competitivos. 🔥
Resultados
El enfoque de hackathon les funciona bien.
- Para detectar fácilmente los cuellos de botella de rendimiento, prueba varios enfoques durante el hackathon y mide cada uno con Lighthouse.
- Después del hackathon, es bastante fácil convencer al equipo de qué optimización debe priorizar para el lanzamiento en producción.
- También es una forma eficaz de defender la importancia de la velocidad. Todos los participantes pueden comprender la correlación entre la forma en que codificas y cómo se traduce en rendimiento.
Un buen efecto secundario fue que muchos otros equipos de ingeniería de Recruit se interesaron en este enfoque práctico, y el equipo de AirSHIFT ahora facilita varios hackathons rápidos dentro de la empresa.
Resumen
Definitivamente, no fue el recorrido más sencillo para que AirSHIFT trabajara en estas optimizaciones, pero sin duda valió la pena. Ahora, AirSHIFT carga la tabla de turnos en un promedio de 1.5 segundos, lo que representa una mejora de 6 veces en su rendimiento antes del proyecto.
Después de que se lanzaron las optimizaciones de rendimiento, un usuario dijo lo siguiente:
Muchas gracias por hacer que la tabla de turnos se cargue rápido. Ahora es mucho más eficiente organizar el trabajo por turnos.