Rendimiento mejorado de carga de páginas de Next.js y Gatsby con fragmentación detallada

Una nueva estrategia de fragmentación de webpack en Next.js y Gatsby minimiza el código duplicado para mejorar el rendimiento de carga de la página.

Chrome está colaborando con herramientas y en el ecosistema de código abierto de JavaScript. Recientemente, se implementaron varias optimizaciones para mejorar el rendimiento de carga de Next.js y Gatsby: En este artículo, se describe una estrategia de fragmentación detallada mejorada que ahora se envía de forma predeterminada en ambos frameworks.

Introducción

Al igual que muchos frameworks web, Next.js y Gatsby usan webpack como su núcleo agrupador. Presentación de webpack v3 CommonsChunkPlugin para que sea posible módulos de salida compartidos entre diferentes puntos de entrada en un solo (o pocos) “comunes” fragmento (o en bloques). El código compartido puede descargarse por separado y almacenarse en la memoria caché del navegador con anticipación, lo que puede un mejor rendimiento de carga.

Este patrón se volvió popular entre muchos frameworks de aplicaciones de una sola página que adoptaban un punto de entrada y del paquete con el siguiente aspecto:

Configuración de paquete y punto de entrada común

Aunque resulta práctico, el concepto de agrupar todo el código del módulo compartido en un solo bloque tiene su las limitaciones de seguridad. Los módulos que no se comparten en todos los puntos de entrada se pueden descargar para las rutas que no lo utilizan. lo que provocará que se descargue más código del necesario. Por ejemplo, cuando se carga page1 el bloque common, carga el código para moduleC aunque page1 no use moduleC. Por esta razón, junto con otros, webpack v4 quitó el complemento para dar lugar a un nuevo uno: SplitChunksPlugin.

Fragmentación mejorada

La configuración predeterminada de SplitChunksPlugin funciona bien para la mayoría de los usuarios. Múltiples bloques divididos son que se crean según una serie de condiciones para evitar recuperar código duplicado en múltiples rutas.

Sin embargo, muchos frameworks web que usan este complemento siguen un único elemento común enfoque para fragmentar la división. Por ejemplo, Next.js generaría un paquete commons con cualquier módulo que se Se usa en más del 50% de las páginas y en todas las dependencias del framework (react, react-dom, etcétera).

const splitChunksConfigs = {
  …
  prod: {
    chunks: 'all',
    cacheGroups: {
      default: false,
      vendors: false,
      commons: {
        name: 'commons',
        chunks: 'all',
        minChunks: totalPages > 2 ? totalPages * 0.5 : 2,
      },
      react: {
        name: 'commons',
        chunks: 'all',
        test: /[\\/]node_modules[\\/](react|react-dom|scheduler|use-subscription)[\\/]/,
      },
    },
  },

Aunque incluir un código que depende del framework en un bloque compartido significa que se puede descargar y en caché para cualquier punto de entrada, la heurística basada en el uso de incluir módulos comunes utilizados en más de la mitad de las páginas no es muy eficaz. Modificar esta proporción solo daría como resultado uno de dos resultados:

  • Si reduces la proporción, se descargará más código innecesario.
  • Si aumentas la proporción, se duplica más código en múltiples rutas.

Para resolver este problema, Next.js adoptó una forma diferente virtual para SplitChunksPlugin que reduce código innecesario para ninguna ruta.

  • Cualquier módulo de terceros lo suficientemente grande (más de 160 KB) se divide en su propio fragmento
  • Se crea un fragmento frameworks separado para las dependencias del framework (react, react-dom y así sucesivamente).
  • Se crean tantos fragmentos compartidos como sea necesario (hasta 25)
  • El tamaño mínimo para generar un fragmento cambió a 20 KB.

Esta estrategia de fragmentación detallada proporciona los siguientes beneficios:

  • Se mejoran los tiempos de carga de la página. Emitir varios fragmentos compartidos, en lugar de uno solo, minimiza la cantidad de código innecesario (o duplicado) para cualquier punto de entrada.
  • Almacenamiento en caché mejorado durante las navegaciones. Divide bibliotecas grandes y dependencias de framework en fragmentos separados reduce la posibilidad de invalidar la caché, ya que es poco probable que hasta que se realice una actualización.

Puedes ver toda la configuración que Next.js adoptó en webpack-config.ts.

Más solicitudes HTTP

SplitChunksPlugin definió la base de la fragmentación detallada y, luego, aplicar este enfoque a un como Next.js no era un concepto completamente nuevo. Sin embargo, muchos frameworks aún siguieron usar una heurística única y “comunes” de paquetes de aplicaciones por varios motivos. Esto incluye la preocupación de que muchas más solicitudes HTTP pueden afectar el rendimiento del sitio de forma negativa.

Los navegadores solo pueden abrir una cantidad limitada de conexiones TCP a un único origen (6 para Chrome), por lo que minimizar la cantidad de fragmentos que genera un agrupador puede garantizar que el número se mantenga por debajo de este umbral. Sin embargo, esto solo es válido para HTTP/1.1. Multiplexación en HTTP/2 permite transmitir múltiples solicitudes en paralelo mediante una única conexión en una sola origen. En otras palabras, no debemos preocuparnos de limitar el número de fragmentos emitido por nuestro agrupador.

Todos los navegadores principales admiten HTTP/2. Los equipos de Chrome y Next.js quería ver si aumentar la cantidad de solicitudes dividiendo los “commons” individuales de Next.js paquete en varios fragmentos compartidos afectaría el rendimiento de carga de cualquier manera. Comenzaron midiendo de un solo sitio y, a la vez, se modifica la cantidad máxima de solicitudes paralelas mediante maxInitialRequests propiedad.

Rendimiento de carga de página con una mayor cantidad de solicitudes

En un promedio de tres ejecuciones de varias pruebas en una sola página web, el load, start-render y los tiempos de First Contentful Paint se mantuvieron casi iguales cuando se varió el valor inicial máximo de solicitudes (de 5 a 15). Curiosamente, notamos una leve sobrecarga de rendimiento solo luego de dividirla de forma agresiva en cientos de solicitudes.

Rendimiento de carga de página con cientos de solicitudes

Esto demostró que mantenerse por debajo de un umbral confiable (entre 20 y 25 solicitudes) lograba encontrar el equilibrio adecuado. entre el rendimiento de carga y la eficiencia del almacenamiento en caché. Después de algunas pruebas de referencia, se seleccionaron 25 el recuento de maxInitialRequest.

Modificar la cantidad máxima de solicitudes que ocurren en paralelo generó más de una y separarlos de forma apropiada para cada punto de entrada redujo cantidad de código innecesario para la misma página.

Reducciones de carga útil de JavaScript con fragmentación aumentada

Este experimento solo consistió en modificar la cantidad de solicitudes para ver si habría alguna un efecto negativo en el rendimiento de carga de la página. Los resultados sugieren que configurar maxInitialRequests como 25 en la página de prueba fue óptimo, ya que redujo el tamaño de la carga útil de JavaScript sin disminuir la velocidad. en toda la página. Se mantuvo la cantidad total de JavaScript que se necesitaba para hidratar la página. prácticamente igual, lo que explica por qué el rendimiento de la carga de la página no mejoró necesariamente cantidad de código.

webpack usa 30 KB como tamaño mínimo predeterminado para que se genere un fragmento. Sin embargo, si se acopla un Un valor maxInitialRequests de 25 con un tamaño mínimo de 20 KB en su lugar, dio como resultado un mejor almacenamiento en caché.

Reducciones de tamaño con fragmentos detallados

Muchos frameworks, incluido Next.js, dependen del enrutamiento del cliente (controlado por JavaScript) para insertar nuevas etiquetas de secuencia de comandos para cada transición de ruta. Pero ¿cómo se predefinen estos fragmentos dinámicos en el tiempo de compilación?

Next.js usa un archivo de manifiesto de compilación del lado del servidor para determinar qué fragmentos de salida usan puntos de entrada diferentes. Para proporcionarle al cliente esta información, un resumen del cliente el archivo de manifiesto de compilación se creó para asignar todas las dependencias de cada punto de entrada.

// Returns a promise for the dependencies for a particular route
getDependencies (route) {
  return this.promisedBuildManifest.then(
    man => (man[route] && man[route].map(url => `/_next/${url}`)) || []
  )
}
Resultado de varios fragmentos compartidos en una app de Next.js.

Esta nueva estrategia de fragmentación detallada se lanzó por primera vez en Next.js detrás de una marca, donde se probó en una la cantidad de usuarios pioneros. Muchos experimentaron reducciones significativas en el total de JavaScript que se usó para sus todo el sitio:

Sitio web Cambio total de JS % de diferencia
https://www.barnebys.com/ -238 KB −23%
https://sumup.com/ -220 KB −30%
https://www.hashicorp.com/ -11 MB −71%
Reducciones de tamaño de JavaScript en todas las rutas (comprimidas)

La versión final se envió de forma predeterminada en la versión 9.2.

Gatsby

Gatsby solía seguir el mismo enfoque de utilizar un sistema de heurística para definir módulos comunes:

config.optimization = {
  …
  splitChunks: {
    name: false,
    chunks: `all`,
    cacheGroups: {
      default: false,
      vendors: false,
      commons: {
        name: `commons`,
        chunks: `all`,
        // if a chunk is used more than half the components count,
        // we can assume it's pretty global
        minChunks: componentsCount > 2 ? componentsCount * 0.5 : 2,
      },
      react: {
        name: `commons`,
        chunks: `all`,
        test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
      },

Al optimizar su configuración de webpack para adoptar una estrategia de fragmentación detallada similar, también notó reducciones considerables de JavaScript en muchos sitios grandes:

Sitio web Cambio total de JS % de diferencia
https://www.gatsbyjs.org/ -680 KB −22%
https://www.thirdandgrove.com/ -390 KB -25%
https://ghost.org/ -1.1 MB −35%
https://reactjs.org/ -80 KB -8%
Reducciones de tamaño de JavaScript en todas las rutas (comprimidas)

Consulta el documento de RR.PP. para comprender cómo implementó esta lógica en su configuración de webpack, que se incluye de forma predeterminada en la versión v2.20.7.

Conclusión

El concepto de envío de fragmentos detallados no es específico de Next.js, Gatsby ni webpack. Todos deberían considerar mejorar la estrategia de fragmentación de su aplicación si sigue un comportamiento común. de paquetes de aplicaciones, sin importar el framework o agrupador de módulos que se use.

  • Si deseas ver las mismas optimizaciones de fragmentación aplicadas a una aplicación de React normal, observa este ejemplo de React de la app. Utiliza un más simple de la estrategia de fragmentación detallada y puede ayudarte a comenzar a aplicar la misma tipo de lógica a tu sitio.
  • En el caso de Rollup, los fragmentos se crean de forma detallada de forma predeterminada. Echa un vistazo a manualChunks si deseas hacerlo manualmente configurar el comportamiento.