Layer0 es uno de los principales contribuyentes al framework de eCommerce PWA de código abierto, React StoreFront. A principios de este año, contribuimos con muchas nuevas características y optimizaciones como React StoreFront v7, Dos de los más significativos fueron el cambio a next.js y la eliminación de varias dependencias clave (por ejemplo, MobX) a favor de las nuevas capacidades incorporadas de React para administrar el estado, como el hook useState y la API contextual. Esto dio lugar a que el tamaño del paquete del navegador se cortara aproximadamente a la mitad.
Esta fue una buena ganancia en ese momento y ayudó a mejorar las puntuaciones de rendimiento de Lighthouse (v5.7) para las aplicaciones típicas de React StoreFront de los años 80 a los 90, según lo medido por PageSpeed Insights (PSI). Para el contexto, una puntuación de más de 83 superó el 99% de los 500 sitios web de comercio electrónico más importantes en Lighthouse v5.7. No nos dimos cuenta de lo esencial que sería la reducción de paquetes en los próximos meses, cuando Lighthouse v6.0 caería como una bomba y destruiría las puntuaciones de rendimiento de todos.
Vea cómo cambió la distribución de las puntuaciones de Lighthouse medidas en PSI para los principales sitios web de comercio electrónico cuando se cayó la versión 6.0:
Las nuevas métricas de Lighthouse 6,0: TBT, LCP y CLS
Lighthouse v6.0 presenta varias nuevas métricas de velocidad perceptual y reformula cómo las métricas generales afectan la puntuación de rendimiento de Lighthouse de una página.Faro 5,7 | Peso | Faro 6,0 | Peso |
---|---|---|---|
Primera Pintura Contenida (FCP) | 20% | Primera Pintura Contenida (FCP) | 15% |
Índice de velocidad (SI) | 27% | Índice de velocidad (SI) | 15% |
Primera pintura significativa (FMP) | 7% | Pintura Contenful más grande (LCP) | 25% |
Primer CPU inactivo (FCI) | 13% | Tiempo total de bloqueo (OTC) | 15% |
Tiempo de Interacción (TTI) | 33% | Tiempo de Interacción (TTI) | 15% |
- | - | Cambio de diseño acumulativo (CLS) | 5% |
Tiempo total de bloqueo (OTC)
El tiempo de bloqueo total es una nueva métrica incluida en Lighthouse v6.0 que mide el tiempo que el análisis y la ejecución de JavaScript bloquean el subproceso principal durante la carga de la página. Esta métrica trata los spas/PWA modernos y pesados de JavaScript muy duramente. Incluso con el recorte del 50% de React StoreFront 7 en el tamaño del paquete, vimos una caída de 20 a 30 puntos en la puntuación de rendimiento de Lighthouse v6.0 para las páginas de productos, en gran parte debido a la inclusión de TBT como una métrica que influye en el 25% de la puntuación de rendimiento general.
Si está utilizando un marco isomórfico, como next.js, que admite renderización del lado del servidor, TBT se determina principalmente por el tamaño del paquete y el tiempo de hidratación. En pocas palabras, la única manera de mejorar TBT es eliminar dependencias, optimizar sus componentes o simplificar su sitio utilizando menos componentes.
La pintura más grande con contenido (LCP)
LCP es una nueva métrica que tiene un peso del 25% sobre la puntuación general de Lighthouse v6.0. LCP tiene como objetivo cuantificar cómo el usuario percibe el rendimiento de la carga inicial de la página observando cuánto tiempo tarda el elemento contentful más grande en terminar de pintar. Para la mayoría de los sitios, especialmente en sitios web de comercio electrónico, el elemento contentful más grande es la imagen de héroe. En el caso de las páginas de productos en las aplicaciones de React StoreFront, el elemento contentful más grande es la imagen principal del producto. Si no estás seguro de qué elemento está en tu sitio, PSI te dirá:
Cambio de diseño acumulativo (CLS)
El cambio de diseño acumulativo mide cuánto cambia el diseño de página durante la carga inicial de la página. El cambio de diseño es causado más comúnmente por las imágenes, que tienden a empujar los elementos a su alrededor a medida que cambian de tamaño para acomodar la imagen una vez que los datos se descargan de la red. El cambio de diseño a menudo se puede eliminar por completo reservando espacio para cada imagen antes de que se cargue. Afortunadamente, el componente Image de React StoreFront ya lo hace, por lo que la aplicación de inicio de React StoreFront cuenta con una puntuación CLS perfecta de 0 fuera de la caja. Cabe señalar que otros culpables comunes de CLS pobres son banners y ventanas emergentes que aparecen después de que la página es pintada inicialmente. Los usuarios los odian y ahora Lighthouse también lo hace.Cómo optimizamos React StoreFront para Lighthouse v6.0
Cuando probamos por primera vez la página de producto de la aplicación de inicio React StoreFront en Lighthouse v6.0, utilizando PageSpeed Insights, obtuvo una puntuación en los 60 bajos:Para mejorar la puntuación, primero fijamos nuestra mirada en LCP. A los 2,5 segundos, FCP era preocupantemente alto (llegaremos a eso más tarde), pero la brecha de casi 3 segundos entre FCP y LCP se destacó como algo que necesitaba mejorar.
Optimización de LCP
La imagen principal del producto de React StoreFront (simplemente un marcador de posición – el cuadro verde con “Producto 1” en el centro) fue representada como una etiqueta HTML img con una URL src que apunta a https://via.placeholder.com.Ese sitio tiene un TTFB decente (alrededor de 150 ms), que podría mejorarse moviendo a un CDN más rápido como Layer0 CDN-as-JavaScript. Aún así, dudamos que 150 ms representaran la brecha de casi 3 segundos entre FCP y LCP. Para eliminar la brecha, incluimos la imagen usando una URL de datos base64 como esta:
Lo hicimos descargando la imagen principal, convirtiéndola a base64, y rellenándola en el atributo src durante el renderizado del lado del servidor en la nube Serverless JavaScript de Layer0. Aquí está la línea en el punto final de la API del producto:
const mainProductImage = result.product.media.full[0]
mainProductImage.src = await getBase64ForImage(mainProductImage.src)
Y aquí está el código que obtiene y convierte la imagen a base 64:
import fetch from 'node-fetch'
export default async function getBase64ForImage(src) {
const res = await fetch(src)
const contentType = res.headers.get('content-type')
const buffer = await res.buffer()
return `data:${contentType};base64,${buffer.toString('base64')}`
return `data:${contentType};base64,${buffer.toString('base64')}` return `data:${contentType};base64,${buffer.toString('base64')}` return `data:${contentType};base64,${buffer.toString('base64')}`
}
Bastante simple y de la vieja escuela. ¿Y el impacto en la puntuación?
Al bajar el LCP de 5.3s a 2.8s, ¡ganamos 21 puntos en la puntuación Lighthouse v6.0 de la página! Es un poco inquietante cómo un cambio tan pequeño puede afectar dramáticamente la puntuación de Lighthouse v6.0, pero lo tomaremos. Debe tenerse en cuenta que todas las métricas varían un poco entre carreras, pero la puntuación general fue consistentemente en los bajos 80s. Para el contexto, el sitio web líder de comercio electrónico de más alto rendimiento en v6.0 obtiene una puntuación de 87 según la PSI y parece que está directamente salido de la década de 90 – eche un vistazo www.rockauto.com
La brecha entre FCP y LCP mostrada anteriormente era casi tan grande como la vimos en varias carreras. La mayoría de las veces la brecha estaba en el rango de 100ms a 300ms. Ocasionalmente FCP y LCP eran iguales.
Optimización de los OTC
A continuación, tratamos de mejorar el TBT. Esto fue bastante desafiante. Como se mencionó anteriormente, para mejorar el LCP, es necesario reducir el tamaño de tu paquete de JavaScript o mejorar el tiempo de hidratación. Con la mayoría de las aplicaciones, simplemente no es factible comenzar a extraer dependencias para hacer que el paquete sea más pequeño. Las aplicaciones creadas con React StoreFront 7 ya se benefician de muchas optimizaciones en tiempo de compilación que minimizan el tamaño del paquete proporcionado por la configuración del webpack de next.js, así como optimizaciones específicas de babel para Material UI. Entonces, ¿dónde podríamos mejorar? Tiempo de hidratación.
Hidratación perezosa para la victoria!
Afortunadamente, la comunidad de React StoreFront ya había comenzado a trabajar para apoyar la hidratación perezosa antes de que Lighthouse v6.0 cayera. Sin duda, esto nos hizo acelerar nuestros esfuerzos.
En caso de que no lo sepa, la hidratación se refiere a React tomando el control de los elementos HTML renderizados en el servidor para que puedan ser interactivos. Los botones se vuelven clickable; los carruseles se vuelven swipeable, etc. Cuantos más componentes tenga una página, más tiempo tardará la hidratación. Los componentes complejos, como el menú principal y el carrusel de imagen del producto, tardan aún más.
La hidratación perezosa implica retrasar la hidratación de ciertos componentes hasta que sea absolutamente necesaria y, lo más importante, después de la carga inicial de la página (y después de calcular el TBE). La hidratación perezosa puede ser arriesgada. Debe asegurarse de que los elementos de la página estén listos para responder a la entrada del usuario antes de que el usuario intente interactuar con ellos.
Implementar hidratación perezosa en React StoreFront resultó bastante difícil debido a la dependencia de Material UI en CSS-in-JS, que agrega estilos dinámicamente al documento solo después de que los componentes se hidratan. Guardaré los detalles para otro momento. Al final, construimos un componente LazyHydrate que los desarrolladores pueden insertar en cualquier lugar del árbol de componentes para retrasar la hidratación hasta que ocurra un evento específico, como el elemento que se desplaza hacia la vista o el usuario que toca la pantalla.
Aquí hay un ejemplo donde hidratamos perezosamente el MediaCarousel que muestra las imágenes principales del producto:
import LazyHydrate from 'react-storefront/LazyHydrate'
Aplicamos hidratación perezosa en varias áreas de la aplicación, especialmente:
- El menú slide-in: Hidratamos esto cuando el usuario toca el botón de hamburguesa.
- Todos los controles debajo del pliegue: Incluyen los selectores de tamaño, color y cantidad, así como las pestañas de información del producto.
- El carrusel de imagen principal: Este y el menú principal son probablemente los componentes con más funcionalidad y, por lo tanto, los más caros de hidratar.
Aquí está la puntuación Lighthouse v6.0 con hidratación perezosa aplicada:
La hidratación perezosa redujo el TBT en casi un 40% y recortó el TTI (que tiene un peso del 15% sobre las puntuaciones en v6.0) en 700 ms. Esto anotó una ganancia de 6 puntos en la puntuación general de Lighthouse v6.0.
Notarás que FCP subió un poco, pero LCP bajó. Estos pequeños cambios están esencialmente “dentro del ruido” que obtienes al ejecutar PageSpeed Insights. Todas las puntuaciones fluctúan ligeramente entre carreras.
Optimización de FCP
Basándonos en la puntuación anterior, sentimos que FCP y/o LCP podrían mejorarse aún más. Sabemos que los scripts pueden bloquear el renderizado, así que analizamos cómo next.js importa scripts al documento:
Usar async aquí podría no ser la mejor opción. Si el script se descarga durante el renderizado, puede pausar el renderizado mientras se evalúa el script, lo que aumenta tanto los tiempos FCP como LCP. El uso de diferir en lugar de async aseguraría que los scripts solo se evalúen después de pintar el documento.
Desafortunadamente, next.js no permite cambiar la forma en que se importan los scripts, por lo que necesitábamos extender el componente NextScript de la siguiente manera:
import React from 'react'
import { NextScript as OriginalNextScript } from 'next/document'
/**
* A replacement for NextScript from `next/document` that gives you greater control over how script elements are rendered.
* This should be used in the body of `pages/_document.js` in place of `NextScript`.
*/
export default class NextScript extends OriginalNextScript {
static propTypes = {
/**
* Set to `defer` to use `defer` instead of `async` when rendering script elements.
*/
mode: PropTypes.oneOf(['async', 'defer']),
}
static defaultProps = {
mode: 'async',
}
getScripts() {
return super.getScripts().map(script => {
return React.cloneElement(script, {
key: script.props.src,
defer: this.props.mode === 'defer' ? true : undefined,
async: this.props.mode === 'async' ? true : undefined,
})
})
}
}
Luego añadimos lo siguiente a pages/_document.js:
Para nuestro deleite, esto mejoró el LCP y las puntuaciones generales:
También golpeó ligeramente el FCP en muchas carreras, pero esto puede estar dentro del “ruido”. Sin embargo, el puntaje general fue consistentemente 2-3 puntos más alto cuando se utiliza el diferir vs async.
Resumiendo
Cuando Lighthouse v6.0 fue lanzado a finales de mayo de 2020, las puntuaciones de rendimiento para muchos sitios, incluidas las aplicaciones de React StoreFront, cayeron en picado. Antes de las optimizaciones, el rendimiento PDP de la aplicación de inicio React StoreFront estaba empantanado en los bajos 60s.. Con estas optimizaciones, lo conseguimos en el aire ahora rarificado de los bajos 90s.. En este punto, creemos que la única manera de mejorar aún más la puntuación sería eliminar las dependencias, lo que puede significar el intercambio de la productividad del desarrollador por el rendimiento de la aplicación.
Esa es una discusión para otro momento. Déjame dejarte con algunas cosas que probamos que no funcionaron:
Preactúa
Preact hace que el tamaño del paquete sea 20-30% más pequeño, pero las puntuaciones de Lighthouse v6.0 fueron constantemente peores en todas las métricas, incluso TTI. No tenemos idea de por qué, pero sabemos que esto no es nuevo ni exclusivo de Lighthouse v6.0. Era más lento con Lighthouse v5.7 también. Seguimos revisando periódicamente y esperamos que algún día esto se solucione.
Chunking
Next.js introdujo recientemente un fragmento más fino de los activos del navegador. Cuando esto se introdujo por primera vez en Next.js 9,1, notamos que los trozos adicionales y más pequeños en realidad empeoraron el TTI. Probablemente hace que la aplicación sea más rápida para los usuarios que regresan después de que se lanza una nueva versión porque puede aprovechar mejor la caché del navegador, pero Lighthouse no se preocupa por nada de eso. Así que React StoreFront ha limitado el número de fragmentos del navegador a uno por un tiempo:
const webpack = require('webpack')
const withReactStorefront = require('react-storefront/plugins/withReactStorefront')
module.exports = withReactStorefront({
target: 'serverless',
webpack: config => {
config.plugins.push(
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
})
)
return config
},
})
Fuentes web
La mayoría de los sitios usan una fuente web personalizada. De forma predeterminada, React StoreFront utiliza Roboto (aunque esto puede cambiarse o eliminarse). Las fuentes web personalizadas matan el rendimiento, simple y simple. Elimina la fuente web y obtendrás aproximadamente 1 segundo de FCP.
Al igual que con la analítica, las partes interesadas parecen dispuestas a cambiar el rendimiento para tener una fuente específica. React StoreFront sirve la fuente web desde el mismo origen que el sitio para eliminar el tiempo de negociación TLS en el que incurriría si cargara la fuente desde una CDN de terceros, como Google Fonts. Específicamente, utilizamos el paquete typeface-roboto de NPM. Cuando se combina con el cargador css de Webpack , el uso de typeface-roboto da como resultado la importación de la fuente a través de un archivo CSS separado que el navegador necesita descargar y analizar. Pensamos que incluir ese CSS en el documento podría ayudar con el rendimiento, pero no lo fue.
Cuando se trata de rendimiento, siempre hay que medir. Las optimizaciones que deberían funcionar en teoría pueden no estar en la práctica.
Aumenta tu puntuación de Lighthouse
Nuestros clientes se ubican en el percentil 95 de los 500 mejores sitios web de comercio electrónico en Lighthouse v6.0.