Reaccione las correcciones de rendimiento en las páginas de listado de Airbnb

Puede haber una gran cantidad de fruta baja que afecte el rendimiento en áreas que quizás no rastree muy de cerca ... pero que aún son muy importantes.

Airbnb tiene algunos listados increíbles en Cuba ... y también un rincón de la oficina inspirado en Habana Vieja

Hemos trabajado arduamente para migrar el flujo central de reservas de airbnb.com a una aplicación de servidor de una sola página usando React Router e Hypernova. A principios de año, implementamos esto para la página de destino y los resultados de búsqueda con buen éxito. Nuestro siguiente paso es expandir la aplicación de una sola página para incluir la página de detalles del listado.

Página de detalles de listado de airbnb.com: https://www.airbnb.com/rooms/8357

Esta es la página que visita cuando decide qué listado reservar. A lo largo de su búsqueda, puede visitar esta página muchas veces para ver diferentes listados. Esta es una de las páginas más visitadas e importantes de airbnb.com, por lo que es fundamental que consigamos todos los detalles.

Como parte de esta migración a nuestra aplicación de una sola página, quería investigar cualquier problema de rendimiento persistente que afecte las interacciones en la página de listado (por ejemplo, desplazarse, hacer clic, escribir). Esto encaja con nuestro objetivo de hacer que las páginas comiencen rápido y se mantengan rápidas, y generalmente solo hace que las personas se sientan mejor sobre el uso del sitio.

A través de un proceso de creación de perfiles, corrección y creación de perfiles nuevamente, mejoramos drásticamente el rendimiento de interacción de esta página crítica, lo que hace que la experiencia de reserva sea más fluida y satisfactoria. En esta publicación, aprenderá sobre las técnicas que utilicé para perfilar esta página, las herramientas que utilicé para optimizarla, y verá la escala de este impacto en los gráficos de llamas producidos por mi perfil.

Metodología

Estos perfiles fueron grabados a través de la herramienta de rendimiento de Chrome:

  1. Abrir una ventana de incógnito (para que las extensiones de mi navegador no interfieran con mi perfil)
  2. Visitar la página en el desarrollo local con el que quería hacer un perfil con? React_perf en la cadena de consulta (para habilitar las anotaciones de temporización de usuario de React y para deshabilitar algunas cosas solo para desarrolladores que tenemos que ralentizan la página, como ax-core)
  3. Al hacer clic en el botón de grabación ️
  4. Interactuar con la página (por ejemplo, desplazarse, hacer clic, escribir)
  5. Al hacer clic nuevamente en el botón de grabación e interpretar los resultados

Normalmente, abogo por la creación de perfiles en hardware móvil como un Moto C Plus o con la aceleración de la CPU configurada en una ralentización de 6x, para comprender qué experimentan las personas en dispositivos más lentos. Sin embargo, dado que estos problemas eran lo suficientemente graves, era claramente obvio cuáles eran las oportunidades en mi computadora portátil súper rápida, incluso sin estrangulamiento.

Renderizado inicial

Cuando comencé a trabajar en esta página, noté una advertencia en mi consola:

webpack-internal: /// 36: 36 Advertencia: React intentó reutilizar el marcado en un contenedor pero la suma de comprobación no era válida. Esto generalmente significa que está utilizando la representación del servidor y el marcado generado en el servidor no era lo que el cliente esperaba. Reaccione el nuevo marcado inyectado para compensar qué funciona, pero ha perdido muchos de los beneficios de la representación del servidor. En cambio, descubra por qué el marcado que se genera es diferente en el cliente o servidor:
 (cliente) ut-placeholder-label screen-reader-only "
 (servidor) ut-placeholder-label "data-reactid =" 628 "

Este es el temido desajuste servidor / cliente, que ocurre cuando el servidor representa algo diferente de lo que representa el cliente en el montaje inicial. Esto obliga a su navegador web a hacer un trabajo que no debería tener que hacer cuando se usa la representación del servidor, por lo que React le da esta útil advertencia cada vez que sucede.

Desafortunadamente, el mensaje de error no es muy claro sobre dónde sucede exactamente o cuál podría ser la causa, pero tenemos algunas pistas. Noté un poco de texto que parecía una clase CSS, así que presioné el terminal con:

~ / airbnb ❯❯❯ ag ut-placeholder-label
app / assets / javascripts / components / o2 / PlaceholderLabel.jsx
85: 'input-placeholder-label': verdadero,
app / assets / stylesheets / p1 / search / _SearchForm.scss
77: .input-placeholder-label {
321: .input-placeholder-label,
spec / javascripts / components / o2 / PlaceholderLabel_spec.jsx
25: const placeholderContainer = wrapper.find ('. Input-placeholder-label');

Esto redujo mi búsqueda bastante rápido a algo llamado o2 / PlaceHolderLabel.jsx, que es el componente que se representa en la parte superior de la sección de revisiones para la búsqueda.

Resultó que utilizamos alguna detección de características para asegurarnos de que el marcador de posición fuera visible en los navegadores más antiguos, como Internet Explorer, al representar la entrada de manera diferente si los marcadores de posición no eran compatibles con el navegador actual. La detección de características es la forma correcta de hacer esto (a diferencia de la detección de agentes de usuario), pero dado que no hay un navegador para detectar características cuando se procesa el servidor, el servidor siempre generará un poco de contenido adicional de lo que la mayoría de los navegadores representarán.

Esto no solo perjudicó el rendimiento, sino que también provocó que una etiqueta adicional se visualizara y se eliminara de la página cada vez. Janky! Solucioné esto moviendo la representación de este contenido al estado React y configurándolo en componentDidMount, que no se ejecuta hasta que el cliente lo procesa.

Volví a ejecutar el generador de perfiles y noté que se actualiza poco después del montaje.

101,63 ms dedicados a volver a representar el resumen de contenedores conectados a Redux

Esto termina volviendo a representar una , dos y una cuando se actualiza. Sin embargo, ninguno de estos tiene ninguna diferencia, por lo que podemos hacer que esta operación sea significativamente más barata usando React.PureComponent en estos tres componentes. Esto fue tan sencillo como cambiar esto:

exportar clase predeterminada SummaryIconRow extiende React.Component {
  ...
}

dentro de esto:

exportar clase predeterminada SummaryIconRow extiende React.PureComponent {
  ...
}

A continuación, podemos ver que también pasa por una nueva representación en la carga de página inicial. Según el gráfico flame flame, la mayor parte del tiempo se gasta renderizando y .

103,15 ms dedicados a renderizar BookIt

Lo curioso aquí es que estos componentes ni siquiera son visibles a menos que la entrada del invitado esté enfocada.

La solución para esto es no representar estos componentes cuando no son necesarios. Esto acelera el renderizado inicial, así como cualquier renderizado que pueda terminar sucediendo. Si vamos un poco más allá y agregamos algunos PureComponents más, podemos hacer que esta área sea aún más rápida.

8,52 ms dedicados a volver a representar BookIt

Desplazándose

Mientras hacía un trabajo para modernizar una animación de desplazamiento suave que a veces usamos en la página de listado, noté que la página se sentía muy irregular al desplazarse. La gente suele tener una sensación incómoda e insatisfactoria cuando las animaciones no alcanzan los 60 fps (cuadros por segundo), y tal vez incluso cuando no alcanzan los 120 fps. El desplazamiento es un tipo especial de animación que está directamente conectado a los movimientos de los dedos, por lo que es aún más sensible al mal rendimiento que otras animaciones.

¡Después de un poco de elaboración de perfiles, descubrí que estábamos haciendo muchas repeticiones innecesarias de componentes React dentro de nuestros controladores de eventos de desplazamiento! Así es como se ve el jank realmente malo:

Rendimiento de desplazamiento realmente malo en las páginas de listado de Airbnb antes de cualquier corrección

Pude resolver la mayoría de este problema al convertir tres componentes en estos árboles para usar React.PureComponent: , y . Esto redujo drásticamente el costo de estos renders. Si bien todavía no estamos a 60 fps (cuadros por segundo), estamos mucho más cerca:

Rendimiento de desplazamiento ligeramente mejorado de las páginas de listado de Airbnb después de algunas correcciones

Sin embargo, todavía hay más oportunidades para mejorar. Al acercar un poco al gráfico de llamas, podemos ver que todavía pasamos mucho tiempo volviendo a renderizar . Y, si miramos hacia abajo la pila de componentes, notamos que hay cuatro fragmentos similares de esto:

58,80 ms dedicados a volver a representar StickyNavigationController

El es la parte de la página de listado que se adhiere a la parte superior de la ventana gráfica. A medida que se desplaza entre las secciones, resalta la sección en la que se encuentra actualmente. Cada uno de los fragmentos en la tabla de llamas corresponds corresponde a uno de los cuatro enlaces que representamos en la navegación fija. Y, cuando nos desplazamos entre las secciones, resaltamos un enlace diferente, por lo que parte debe volver a renderizarse. Así es como se ve en el navegador.

Ahora, noté que tenemos cuatro enlaces aquí, pero solo dos cambian de apariencia cuando hacemos la transición entre secciones. Pero aún así, en nuestro gráfico de llamas, vemos que los cuatro enlaces se vuelven a representar cada vez. Esto sucedía porque nuestro componente estaba creando una nueva función en render y la pasaba a como accesorio cada vez, lo que des-optimiza los componentes puros.

const anchors = React.Children.map (children, (child, index) => {
  return React.cloneElement (hijo, {
    seleccionado: activeAnchorIndex === index,
    onPress (evento) {onAnchorPress (índice, evento); },
  });
});

Podemos solucionar esto asegurándonos de que siempre reciba la misma función cada vez que lo represente:

const anchors = React.Children.map (children, (child, index) => {
  return React.cloneElement (hijo, {
    seleccionado: activeAnchorIndex === index,
    índice,
    onPress: this.handlePress,
  });
});

Y luego en :

class NavigationAnchor extiende React.Component {
  constructor (accesorios) {
    super (accesorios);
    this.handlePress = this.handlePress.bind (this);
  }
  handlePress (evento) {
    this.props.onPress (this.props.index, evento);
  }
  render () {
    ...
  }
}

¡Perfilando después de este cambio, vemos que solo se vuelven a representar dos enlaces! ¡Eso es la mitad del trabajo! Y, si usamos más de cuatro enlaces aquí, la cantidad de trabajo que debe hacerse ya no aumentará mucho.

32,85 ms dedicados a volver a representar StickyNavigationController

Dounan Shi de Flexport ha estado trabajando en Reflective Bind, que utiliza un complemento Babel para realizar este tipo de optimización por usted. Todavía es bastante temprano, por lo que puede que todavía no esté listo para la producción, pero estoy bastante entusiasmado con las posibilidades aquí.

Mirando hacia abajo en el panel Principal en la grabación de Performance, noto que tenemos un bloque _handleScroll de aspecto muy sospechoso que consume 19ms en cada evento de desplazamiento. Como solo tenemos 16 ms si queremos alcanzar los 60 fps, esto es demasiado.

18.45 ms gastado en _handleScroll

El culpable parece estar en algún lugar dentro de onLeaveWithTracking. A través de algunas búsquedas de código, rastreé esto hasta el . Y mirando un poco más de cerca estas pilas de llamadas, noto que la mayor parte del tiempo gastado está realmente dentro del setState de React, pero lo extraño es que en realidad no estamos viendo ningún re-renderizado aquí. Hmm ...

Al profundizar un poco más en , noto que estamos usando React state 🗺 para rastrear cierta información sobre la instancia.

this.state = {inViewport: false};

Sin embargo, nunca usamos este estado en la ruta de renderizado y nunca necesitamos estos cambios de estado para causar renders, por lo que terminamos pagando un costo adicional. Convertir todos estos usos del estado React en variables de instancia simples realmente nos ayuda a acelerar estas animaciones de desplazamiento.

this.inViewport = false;
1.16 ms gastados en el controlador de eventos de desplazamiento

También noté que el se estaba volviendo a representar, lo que causó un costoso y una representación innecesaria del componente .

32.24 ms gastados en AboutThisListingContainer re-render

Esto terminó siendo en parte causado por nuestro componente de orden superior withExperiments que usamos para ayudarnos a realizar experimentos. Este HOC se escribió de manera tal que siempre pasa un objeto recién creado como accesorio al componente que envuelve, desoptimizando cualquier cosa a su paso.

render () {
  ...
  const finalExperiments = {
    ... experimentos,
    ... este.experimento.experimentos,
  };
  regreso (
    
  );
}

Lo arreglé trayendo una reselección para este trabajo, que memoriza el resultado anterior para que permanezca referencialmente igual entre sucesivos renders.

const getExperiments = createSelector (
  ({experimentFromProps}) => experimentFromProps,
  ({experimentFromState}) => experimentFromState,
  (experimentFromProps, experimentFromState) => ({
    ... experimentos de Props,
    ... experimentos de Estado,
  }),
);
...
render () {
  ...
  const finalExperiments = getExperiments ({
    experimentFromProps: experimentos,
    experimentFromState: this.state.experiments,
  });
  regreso (
    
  );
}

La segunda parte del problema fue similar. En esta ruta de código estábamos usando una función llamada getFilteredAervicios que tomó una matriz como primer argumento y devolvió una versión filtrada de esa matriz, similar a:

función getFilteredAervicios (amenidades) {
  return amenities.filter (shouldDisplayAmenity);
}

Aunque esto parece lo suficientemente inocente, esto creará una nueva instancia de la matriz cada vez que se ejecute, incluso si produce el mismo resultado, lo que desestimulará cualquier componente puro que reciba esta matriz como accesorio. También arreglé esto al volver a seleccionar para memorizar el filtrado. ¡No tengo un gráfico de llamas para este porque todo el re-renderizado desapareció por completo!

Probablemente todavía haya más oportunidades aquí (por ejemplo, contención CSS), ¡pero el rendimiento de desplazamiento ya se ve mucho mejor!

Rendimiento de desplazamiento mejorado en las páginas de listado de Airbnb después de estas correcciones

Haciendo clic en las cosas

Al interactuar un poco más con la página, sentí un retraso notable al hacer clic en el botón "Útil" en una reseña.

Mi presentimiento fue que al hacer clic en este botón, todas las revisiones de la página se volvían a reproducir. Mirando la tabla de llamas, no estaba muy lejos:

42,38 ms que vuelve a representar ReviewsContent

Después de colocar React.PureComponent en un par de lugares, hacemos que estas actualizaciones sean mucho más eficientes.

12.38 ms re-renderizando ReviewsContent

Escribiendo cosas

Volviendo a nuestro viejo amigo con el desajuste del servidor / cliente, noté que escribir en este cuadro realmente no respondía.

¡En mi perfil descubrí que cada pulsación de tecla causaba que todo el encabezado de la sección de revisión y cada revisión se volviera a representar! Eso no es tan cuervo.

61,32 ms volviendo a procesar Comentarios conectados a Redux

Para solucionar esto, extraje parte del encabezado para que sea su propio componente, de modo que pueda convertirlo en React.PureComponent, y luego rocié algunos React.PureComponents en todo el árbol. Esto hizo que cada pulsación de tecla solo volviera a representar el componente que debía volver a representarse: la entrada.

3.18 ms volviendo a procesar ReviewsHeader

Que aprendimos

  • Queremos que las páginas comiencen rápido y se mantengan rápidas.
  • Esto significa que necesitamos ver más que solo el tiempo para interactuar, también necesitamos analizar las interacciones en la página, como desplazarse, hacer clic y escribir.
  • React.PureComponent y reselect son herramientas muy útiles en nuestro kit de herramientas de optimización de aplicaciones React.
  • Evite buscar herramientas más pesadas, como el estado React, cuando las herramientas más ligeras, como las variables de instancia, se ajustan perfectamente a su caso de uso.
  • React nos da mucho poder, pero puede ser fácil escribir código que desmotive su aplicación.
  • Cultive el hábito de perfilar, hacer un cambio y luego perfilar nuevamente.

Si disfrutaste leyendo esto, siempre estamos buscando personas talentosas y curiosas para que se unan al equipo. Somos conscientes de que todavía hay muchas oportunidades para mejorar el rendimiento de Airbnb, pero si notas algo que podría llamar nuestra atención o simplemente quieres hablar de compras, contáctame en Twitter en cualquier momento @lencioni

Un gran agradecimiento a Thai Nguyen por ayudarnos a revisar la mayoría de estos cambios y por trabajar para llevar la página de listado a la aplicación principal de flujo de reserva de una sola página. ¡Exagera! Muchas gracias al equipo que trabaja en Chrome DevTools: ¡estas visualizaciones de rendimiento son de primera categoría! Además, grandes accesorios para Netflix para Stranger Things 2.