Cinco formas en que AirSHIFT mejoró el rendimiento del tiempo de ejecución de su app de React

Un caso de éxito real sobre la optimización del rendimiento de la SPA de React.

Kento Tsuji
Kento Tsuji
Satoshi Arai
Satoshi Arai
Yusuke Utsunomiya
Yusuke Utsunomiya
Yosuke Furukawa
Yosuke Furukawa

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 de escritorio que las personas usan a diario. El equipo de ingeniería de Recruit Technologies realizó un proyecto de refactorización para mejorar una de sus aplicaciones web, AirSHIFT, a fin de mejorar el rendimiento de las entradas de los usuarios. Así es como lo hicieron.

Respuesta lenta, menos productividad

AirSHIFT es una aplicación web para computadoras que ayuda a los propietarios de tiendas, como restaurantes y cafés, a administrar el trabajo por turnos de los miembros del personal. Creada con React, la aplicación de una sola página proporciona funciones de cliente enriquecidas, incluidas varias tablas de cuadrícula con cronogramas de turnos organizados por día, semana, mes y mucho más.

Captura de pantalla de la app web AirSHIFT.

A medida que el equipo de ingeniería de Recruit Technologies agregó nuevas funciones a la app AirSHIFT, comenzó a recibir más comentarios sobre el rendimiento lento. El gerente de ingeniería de AirSHIFT, Yosuke Furukawa, dijo:

En un estudio de investigación sobre usuarios, nos sorprendió cuando uno de los dueños de las tiendas dijo que dejaría su asiento para preparar café después de hacer clic en un botón, solo para matar el tiempo esperando que se cargara la tabla de cambios.

Después de realizar la investigación, el equipo de ingeniería notó que muchos de sus usuarios intentaban cargar enormes tablas de turnos en computadoras de gama baja, como una laptop Celeron M de 1 GHz de hace 10 años.

Ícono giratorio infinito en dispositivos de gama baja.

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 estas, porque estaban desarrollando y realizando pruebas en computadoras con grandes especificaciones y conexiones Wi-Fi rápidas.

Un gráfico que muestra la actividad del tiempo de ejecución de la app.
Cuando se cargaba la tabla de cambios, alrededor del 80% del tiempo de carga se consumió mediante la ejecución de secuencias de comandos.

Después de perfilar su rendimiento en las Herramientas para desarrolladores de Chrome con la limitación de la CPU y la red habilitadas, quedó claro que se necesitaba optimización del rendimiento. AirSHIFT formó un grupo de trabajo para abordar este problema. Aquí hay 5 aspectos en los que se enfocaron para que su app sea más responsiva a las entradas del usuario.

1. Virtualizar 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 horarios. Por ejemplo, si un restaurante tiene 50 miembros que trabajan y quiere consultar su horario de turnos mensual, sería una tabla de 50 (miembros) multiplicada por 30 (días), lo que generaría 1,500 componentes de celda para renderizar. Esta es una operación muy costosa, en especial para dispositivos de gama baja. En realidad, las cosas eran peores. A partir de la investigación, descubrieron que había tiendas que administraban a 200 miembros del personal, requiriendo alrededor de 6,000 componentes de celda 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 del viewport y desactiva los que están fuera de la pantalla.

Captura de pantalla con anotaciones que demuestra que AirSHIFT se usó para renderizar contenido fuera del viewport.
Antes: Procesa todas las celdas de la tabla de cambios.
Captura de pantalla con anotaciones que demuestra que AirSHIFT ahora solo procesa el contenido que está visible en el viewport
Después: Solo se renderizan las celdas dentro del viewport.

En este caso, AirSHIFT utilizó la opción react-virtualizada, ya que existían requisitos relacionados con la habilitación de tablas de cuadrícula bidimensionales complejas. También están explorando formas de convertir la implementación para usar react-window liviana en el futuro.

Resultados

La virtualización de la tabla por sí sola redujo el tiempo de escritura de secuencias de comandos en 6 segundos (en una ralentización de CPU 4 veces más lenta + el entorno 3G rápido de Macbook Pro limitado). Esta fue la mejora de rendimiento de mayor impacto en el proyecto de refactorización.

Captura de pantalla anotada de una grabación del panel Performance de Chrome Herramientas para desarrolladores.
Antes: alrededor de 10 segundos de secuencia de comandos después de la entrada del usuario.
Otra captura de pantalla anotada de una grabación del panel Performance de Chrome Herramientas para desarrolladores.
Después: 4 segundos de secuencia de comandos después de la entrada del usuario.

2. Realiza auditorías con la API de User Timing

Luego, el equipo de AirSHIFT refactorizó los scripts que se ejecutan a partir de las entradas del usuario. El gráfico tipo llama de las Herramientas para desarrolladores de Chrome permite analizar lo que está sucediendo en el subproceso principal. Sin embargo, al equipo de AirSHIFT les resultó más fácil analizar la actividad de la app según el ciclo de vida de React.

React 16 proporciona su registro de rendimiento a través de la API de User Timing, que puedes visualizar en la sección Tiempos de las Herramientas para desarrolladores de Chrome. AirSHIFT usó la sección Tiempos para encontrar la lógica innecesaria que se ejecutaba en eventos de ciclo de vida de React.

Sección Tiempos del panel Rendimiento de Herramientas para desarrolladores de Chrome.
Eventos de User Timing de React.

Resultados

El equipo de AirSHIFT descubrió que justo antes de cada navegación de ruta se producía una conciliación innecesaria del árbol de reacción. Eso significa que React actualizaba la tabla de cambios innecesariamente antes de las navegaciones. Este problema se debe a una actualización innecesaria del estado de Redux. Cuando se corrigió, se ahorró alrededor de 750 ms de tiempo de escritura de secuencias de comandos. AirSHIFT también realizó otras microoptimizaciones, que finalmente llevaron a una reducción total de 1 segundo en el tiempo de escritura de secuencias de comandos.

3. Carga los componentes de forma diferida y traslada las lógicas costosas a los trabajadores web

AirSHIFT tiene una aplicación de chat integrada. Muchos propietarios de tiendas se comunican con los miembros de 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 generar bloqueos.

Para mejorar esta experiencia, AirSHIFT ahora usa React.lazy y Suspense a fin de mostrar marcadores de posición para el contenido de las tablas mientras se cargan de forma diferida los componentes reales.

El equipo de AirSHIFT también migró parte de la costosa lógica empresarial de los componentes de carga diferida a los trabajadores web. Esto resolvió el problema de bloqueo de entrada del usuario porque liberó el subproceso principal para que pudiera enfocarse en responder a las entradas del usuario.

Por lo general, los desarrolladores tienen dificultades en el uso de trabajadores, pero esta vez Comlink se encargó del trabajo pesado. A continuación, se muestra el seudocódigo de cómo AirSHIFT trabajadorizó una de las operaciones más costosas que tuvo: calcular los costos laborales totales.

En App.js, usa React.lazy y Suspense para mostrar contenido de resguardo mientras se carga el contenido

/** 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 Costo, usa el vínculo 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 expónla con 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 JavaScript del subproceso principal al subproceso de trabajo (simulado con una limitación de CPU 4 veces superior).

Captura de pantalla de una grabación del panel Performance de Chrome Herramientas para desarrolladores que muestra que la secuencia de comandos ahora se produce en un trabajador web y no en el subproceso principal.

AirSHIFT está explorando si pueden cargar de forma diferida otros componentes y descargar más lógica a los trabajadores web para reducir aún más los bloqueos.

4. Cómo establecer un presupuesto de rendimiento

Después de implementar todas estas optimizaciones, era fundamental asegurarse de que la app mantuviera un buen rendimiento a lo largo del tiempo. AirSHIFT ahora usa bundlesize para no exceder el tamaño actual del archivo JavaScript y CSS. Además de configurar estos presupuestos básicos, crearon un panel para mostrar varios percentiles del tiempo de carga de la tabla de cambio a fin de verificar si la aplicación tiene un buen rendimiento incluso en condiciones no ideales.

  • Ahora se mide el tiempo de finalización del guion para cada evento Redux
  • Los datos de rendimiento se recopilan en Elasticsearch.
  • El rendimiento en 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 cambio a fin de 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 las notificaciones automáticas a través de Elasticsearch cuando se exceda su presupuesto.

Un gráfico en el que se muestra que el percentil 75 se completa en alrededor de 2,500 ms, el percentil 50 en alrededor de 1,250 ms, el percentil 25 en alrededor de 750 ms y el décimo percentil, aproximadamente 500 ms.
El panel de Kibana, en el que se muestran los datos de rendimiento diario por percentiles.

Resultados

En el gráfico anterior, puedes ver que ahora AirSHIFT alcanza el presupuesto de 3 segundos para los usuarios del percentil 75 y, además, carga la tabla de cambio en un segundo para los usuarios del percentil 25. Al capturar los datos de rendimiento de RUM de varias condiciones y dispositivos, AirSHIFT ahora puede verificar si el lanzamiento de una nueva función está afectando realmente el rendimiento de la aplicación o no.

5. Hackatones de rendimiento

Aunque todos estos esfuerzos de optimización del rendimiento fueron importantes y significativos, no siempre es fácil que los equipos de ingeniería y comerciales 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 está llevando a cabo hackatones internos de rendimiento de 1 día para permitir que los ingenieros se enfoquen solo en el trabajo relacionado con el rendimiento. En estos hackatones, quitan todas las restricciones y respetan 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 al grupo en equipos pequeños, y cada uno compite para ver quién puede obtener la mayor mejora en la puntuación de rendimiento de Lighthouse. Los equipos se ponen muy competitivos. 🔥

Fotos del hackatón

Resultados

El enfoque del hackatón funciona bien para ellos.

  • Los cuellos de botella de rendimiento se pueden detectar fácilmente si se prueban varios enfoques durante el hackatón y se miden cada uno con Lighthouse.
  • Después del hackatón, es bastante fácil convencer al equipo de qué optimización debería priorizar para el lanzamiento de producción.
  • También es una manera eficaz de defender la importancia de la velocidad. Cada participante puede comprender la correlación entre cómo se programa y cómo da como resultado el rendimiento.

Un buen efecto secundario fue que muchos otros equipos de ingeniería dentro de Recruit se interesaron en este enfoque práctico, y el equipo de AirSHIFT ahora facilita hackatones de varias velocidades dentro de la empresa.

Resumen

Definitivamente, no fue el camino más fácil para que AirSHIFT trabajara en estas optimizaciones, pero dio resultado. Ahora AirSHIFT está cargando la tabla de cambios en un promedio de 1.5 s, lo que representa una mejora de 6 veces con respecto a su rendimiento antes del proyecto.

Después de que se lanzaron las optimizaciones de rendimiento, un usuario dijo:

Muchas gracias por acelerar la carga de la tabla de cambios. Ahora, organizar el trabajo por turnos es mucho más eficiente.