Skip to main content




A real-world case study on optimizing React SPA performance.

El rendimiento del sitio Web no se trata solo del tiempo de carga. Es fundamental proporcionar una experiencia rápida y receptiva a los usuarios, especialmente para las aplicaciones de escritorio de productividad que la gente usa todos los días. El equipo de ingenieros de Recruitment technologies went through a refactoring project to improve one of its web applications, AirSHIFT, para un mejor rendimiento de entrada del Username. Así es como lo hicieron.

Slow response, lower productivity

AirSHIFT es una aplicación web de escritorio que ayuda a los propietarios de tiendas, como restaurantes y cafés, a gestionar el trabajo por turnos de los miembros de su personal. Construida con React, la aplicación de una sola página proporciona funciones de client enriquecidas que incluyen varias tablas de cuadrícula de horarios de turnos organizados por día, semana, mes y más.

airshift_visual-3001408

As the Recruit Technologies engineering team added new features to the AirSHIFT app, they started getting more feedback on slow performance. AirSHIFT Engineering Manager Yosuke Furukawa said:

In a user research study, we were surprised when one of the store owners said that he would leave his seat to make coffee after clicking a button, just to kill time waiting for the shift table to load.

After conducting the research, the engineering team realized that many of its users were trying to load massive shift tables onto low-spec computers, such as a 1 GHz Celeron M laptop from 10 years ago.

Endless spinner on low-end devices.

La aplicación AirSHIFT estaba bloqueando el hilo principal con scripts costosos, pero el equipo de ingeniería no se dio cuenta de lo costosos que eran los scripts porque se estaban desarrollando y probando en computadoras de especificaciones ricas con conexiones Wi-Fi rápidas.

main-thread-break-down-7210124

When loading the shift table, about 80% of the loading time was consumed by executing scripts.

Después de perfilar su rendimiento en Chrome DevTools con la limitación de CPU y red habilitada, quedó claro que se necesitaba optimize el rendimiento. AirSHIFT formó un grupo de trabajo para abordar este problema. Aquí hay 5 cosas en las que se centraron para hacer que su aplicación responda mejor a la entrada del usuario.

1. Virtualize large tables

Displaying the shift table required several costly steps: building the virtual DOM and rendering it on the screen in proportion to the number of staff members and time slots. For example, if a restaurant has 50 working members and you want to check their monthly shift schedule, it would be a table of 50 (members) multiplied by 30 (days), resulting in 1,500 cell components to render. This is a very expensive operation, especially for low-spec devices. In reality, things were worse. From the investigation, they discovered that there were stores managing 200 staff members, requiring around 6,000 cell components in a single monthly table.

To reduce the cost of this operation, AirSHIFT virtualized the shift table. The application now only mounts the components inside the viewport and unmounts the components outside the screen.

virtualize_before-9209523

Before: rendering all the cells of the shift table.

virtualize_after-8560709

After: just render the cells within the viewport.

In this case, AirSHIFT used react virtualized as there were requirements around enabling complex two-dimensional grid tables. They are also exploring ways to convert the implementation to use the light reaction window in the future.

Results

Table virtualization alone reduced scripting time by 6 seconds (in a Macbook Pro 4x slower + Fast 3G accelerated environment). This was the most impactful performance improvement in the refactoring project.

virtualize_results_before-4813935

Before: about 10 seconds of script after user input.

virtualize_results_after-6955380

After: 4 seconds of script after user input.

2. Auditoría con API User Timing

The AirSHIFT team then refactored the scripts that run with user input. the flame table
from Chrome DevTools
allows you to analyze what is actually happening in the main thread. But the AirSHIFT team found it easier to analyze application activity based on the React lifecycle.

React 16 provides its performance tracking through the
User time API, which you can view from the
Times section
from Chrome DevTools. AirSHIFT used the Times section to find unnecessary logic running in React lifecycle events.

user_timing-6925370

React user time events.

Results

The AirSHIFT team found that a
Reaction Tree Reconciliation
estaba sucediendo justo antes de cada ruta de navegación. Esto significaba que React estaba actualizando la tabla de turnos innecesariamente antes de las navegaciones. Una actualización innecesaria del estado de Redux estaba causando este problema. Arreglarlo ahorró alrededor de 750 ms de tiempo de scripting. AirSHIFT también realizó otras microoptimizaciones que eventualmente llevaron a una reducción total de 1 segundo en el tiempo de programming.

3. Lazy loading components and costly logic transfer to web workers

AirSHIFT has a built-in chat application. Many store owners communicate with their staff members via chat while looking at the shift table, which means that a user may be typing a message while the table is loading. If the main thread is busy with scripts that are rendering the table, the user input could be annoying.

Para mejorar esta experiencia, AirSHIFT ahora usa React.lazy y Suspense para mostrar marcadores de posición para el contents de la tabla mientras carga lentamente los componentes reales.

The AirSHIFT team also migrated some of the expensive business logic within lazily loaded components to
web workers. This solved the user input jank issue by freeing the main thread so it could focus on responding to user input.

Typically developers are faced with complexity when using workers, but this time Comlink did the heavy lifting for them. Below is the pseudocode of how AirSHIFT worked on one of the most expensive operations they ever had: calculating total labor costs.

In App.js, use React.lazy y Suspense para mostrar contenido de respaldo mientras se carga


import React, { lazy, Suspense } desde 'react'


const Hello = lazy(() => import('./Cost'))

const Loading = () => (
<div>Some fallback content to show while loading</div>
)


export default function App({ userInfo }) {
return (
<div>
<Suspense fallback={<Loading />}>
<Cost />
</Suspense>
</div>
)
}

In the Cost component, use comlink to run the calculation logic


import React desde 'react';
import { proxy } desde 'comlink';


const WorkerlizedCostCalc = proxy(new Worker('./WorkerlizedCostCalc.js'));
export default function Cost({ userInfo }) {
const instance = await new WorkerlizedCostCalc();
const cost = await instance.calc(userInfo);
return <p>{cost}</p>;
}

Implement the calculation logic that runs on the worker and expose it with comlink


import { expose } desde 'comlink'
import { someExpensiveCalculation } desde './CostCalc.js'


expose({
calc(userInfo) {
return someExpensiveCalculation(userInfo);
}
}, self);

Results

A pesar de la cantidad limitada de lógica que trabajaron como prueba, AirSHIFT cambió alrededor de 100 ms de su JavaScript del hilo principal al hilo de trabajo (simulado con aceleración de CPU 4x).

worker-7404546

AirSHIFT is currently exploring whether they can lazy load other components and offload more logic to web workers to further reduce jank.

4. Establish a performance budget

With all of these optimizations in place, it was critical to ensure that the application continued to function over time. AirSHIFT now uses pack size no exceder el tamaño de archivo JavaScript y CSS actual. Además de establecer estos presupuestos básicos, crearon un tablero para mostrar varios percentiles del tiempo de carga de la tabla de turnos para verificar si la aplicación funciona incluso en condiciones no ideales.

  • Ahora se mide el tiempo de finalización del script para cada event de Redux
  • Performance data is collected in Elasticsearch
  • The performance of the 10th, 25th, 50th and 75th percentiles of each event is displayed with Kibana

AirSHIFT is now monitoring the shift table load event to make sure it completes in 3 seconds for 75th percentile users. This is an unapplied budget for now, but they are considering automatic notifications through Elasticsearch when they exceed your budget.

kibana-9058952

The Kibana dashboard showing daily performance data by percentiles.

Results

In the graph above, you can see that AirSHIFT is now mainly hitting the 3 second budget for 75th percentile users and also loads the shift table in one second for 25th percentile users. By capturing RUM performance data from various conditions and devices, AirSHIFT can now check if a new feature version is really affecting the performance of the application or not.

5. Performance hackathons

While all of these performance optimization efforts were important and impactful, it's not always easy to get engineering and business teams to prioritize non-functional development. Part of the challenge is that some of these performance optimizations cannot be planned. They require experimentation and a trial and error mindset.

AirSHIFT is now conducting internal 1-day performance hackathons to allow engineers to focus only on performance-related work. In these hackathons they remove all limitations and respect the creativity of the engineers, which means that any implementation that contributes to speed is worth considering. To speed up the hackathon, AirSHIFT divides the group into small teams and each team competes to see who can get the biggest Lighthouse performance score improvement. Teams become very competitive! 🔥

hackathon-2457180

Results

The hackathon approach is working well for them.

  • Performance bottlenecks can be easily spotted by testing multiple approaches during the hackathon and measuring each with Lighthouse.
  • After the hackathon, it's pretty easy to convince the team which optimization they should prioritize for the production release.
  • It is also an effective way to defend the importance of speed. All participants can understand the correlation between how you code and how it translates to performance.

A nice side effect was that many other engineering teams within Recruit took an interest in this practical approach and the AirSHIFT team is now facilitating multiple speed hackathons within the company.

Summary

It was definitely not the easiest journey for AirSHIFT to work on these optimizations, but it was certainly worth it. Now AirSHIFT is loading the shift table in 1.5 seconds at the median, which is a 6x improvement over its pre-project performance.


After the performance optimizations were released, one user said:

Thank you very much for speeding up the loading of the shift table. Organizing shift work is much more efficient now.