Una estrategia de división de webpack más reciente en Next.js y Gatsby minimiza el código duplicado para mejorar el rendimiento de carga de la página.
Chrome colabora con herramientas y frameworks en el ecosistema de código abierto de JavaScript. Recientemente, se agregaron varias optimizaciones más recientes para mejorar el rendimiento de carga de Next.js y Gatsby. En este artículo, se describe una estrategia de fragmentación granular mejorada que ahora se incluye de forma predeterminada en ambos frameworks.
Introducción
Al igual que muchos frameworks web, Next.js y Gatsby usan webpack como su agrupador principal. webpack v3 introdujo CommonsChunkPlugin
para permitir la salida de módulos compartidos entre diferentes puntos de entrada en un solo fragmento "común" (o algunos fragmentos). El código compartido se puede descargar por separado y almacenar en la caché del navegador con anticipación, lo que puede generar un mejor rendimiento de carga.
Este patrón se popularizó con muchos frameworks de aplicaciones de una sola página que adoptaron una configuración de punto de entrada y paquete similar a la siguiente:
Si bien es práctico, el concepto de agrupar todo el código del módulo compartido en un solo fragmento tiene sus limitaciones. Los módulos que no se comparten en cada punto de entrada se pueden descargar para las rutas que no los usan, lo que genera que se descargue más código del necesario. Por ejemplo, cuando page1
carga el fragmento common
, carga el código de moduleC
, aunque page1
no use moduleC
.
Por este motivo, junto con otros, webpack v4 quitó el complemento y lo reemplazó por uno nuevo: SplitChunksPlugin
.
Fragmentación mejorada
La configuración predeterminada de SplitChunksPlugin
funciona bien para la mayoría de los usuarios. Se crean varios fragmentos divididos según una cantidad de condiciones para evitar la recuperación de código duplicado en varias rutas.
Sin embargo, muchos frameworks web que usan este complemento aún siguen un enfoque de "único común" para la división de fragmentos. Next.js, por ejemplo, generaría un paquete commons
que contenga cualquier módulo que se use en más del 50% de las páginas y 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)[\\/]/,
},
},
},
Si bien incluir código dependiente del framework en un fragmento compartido significa que se puede descargar y almacenar en caché para cualquier punto de entrada, la heurística basada en el uso de incluir módulos comunes que se usan en más de la mitad de las páginas no es muy eficaz. Modificar esta proporción solo generaría uno de los siguientes dos resultados:
- Si reduces la proporción, se descargará más código innecesario.
- Si aumentas la proporción, se duplicará más código en varias rutas.
Para resolver este problema, Next.js adoptó una configuración diferente paraSplitChunksPlugin
que reduce el código innecesario para cualquier ruta.
- Cualquier módulo de terceros lo suficientemente grande (más de 160 KB) se divide en su propio fragmento individual.
- Se crea un fragmento
frameworks
independiente para las dependencias del framework (react
,react-dom
, etcétera). - Se crean tantos fragmentos compartidos como sean necesarios (hasta 25).
- Se cambió el tamaño mínimo para generar un fragmento a 20 KB.
Esta estrategia de fragmentación detallada proporciona los siguientes beneficios:
- Se mejoraron los tiempos de carga de las páginas. Emitir varios fragmentos compartidos, en lugar de uno solo, minimiza la cantidad de código innecesario (o duplicado) para cualquier punto de entrada.
- Se mejoró el almacenamiento en caché durante la navegación. Dividir las bibliotecas grandes y las dependencias del framework en fragmentos separados reduce la posibilidad de invalidación de la caché, ya que es poco probable que ambos cambien hasta que se realice una actualización.
Puedes ver toda la configuración que adoptó Next.js en webpack-config.ts
.
Más solicitudes HTTP
SplitChunksPlugin
definió la base para la división granular, y aplicar este enfoque a un framework como Next.js no era un concepto completamente nuevo. Sin embargo, muchos frameworks siguieron usando una sola estrategia heurística y de paquete de "commons" por varios motivos. Esto incluye la preocupación de que muchas más solicitudes HTTP pueden afectar negativamente el rendimiento del sitio.
Los navegadores solo pueden abrir una cantidad limitada de conexiones TCP a un solo origen (6 en Chrome), por lo que minimizar la cantidad de fragmentos que genera un bundler puede garantizar que la cantidad total de solicitudes se mantenga por debajo de este umbral. Sin embargo, esto solo se aplica a HTTP/1.1. La multiplexación en HTTP/2 permite que se transmitan varias solicitudes en paralelo con una sola conexión a través de un solo origen. En otras palabras, por lo general, no necesitamos preocuparnos por limitar la cantidad de fragmentos que emite nuestro bundler.
Todos los navegadores principales admiten HTTP/2. Los equipos de Chrome y Next.js querían saber si aumentar la cantidad de solicitudes dividiendo el único paquete "commons" de Next.js en varios fragmentos compartidos afectaría el rendimiento de carga de alguna manera. Comenzaron midiendo el rendimiento de un solo sitio mientras modificaban la cantidad máxima de solicitudes paralelas con la propiedad maxInitialRequests
.
En un promedio de tres ejecuciones de varias pruebas en una sola página web, los tiempos de load
, start-render y First Contentful Paint se mantuvieron casi iguales cuando se varió el recuento máximo de solicitudes iniciales (de 5 a 15). Curiosamente, observamos una ligera sobrecarga de rendimiento solo después de dividir agresivamente en cientos de solicitudes.
Esto demostró que mantenerse por debajo de un umbral confiable (entre 20 y 25 solicitudes) logró el equilibrio adecuado entre el rendimiento de carga y la eficiencia del almacenamiento en caché. Después de algunas pruebas de referencia, se seleccionó 25 como el recuento de maxInitialRequest
.
Modificar la cantidad máxima de solicitudes que se realizan en paralelo generó más de un solo paquete compartido, y separarlos de forma adecuada para cada punto de entrada redujo significativamente la cantidad de código innecesario para la misma página.
Este experimento solo consistió en modificar la cantidad de solicitudes para ver si habría algún efecto negativo en el rendimiento de la carga de la página. Los resultados sugieren que establecer maxInitialRequests
en 25
en la página de prueba fue óptimo porque redujo el tamaño de la carga útil de JavaScript sin ralentizar la página. La cantidad total de JavaScript que se necesitó para hidratar la página siguió siendo casi la misma, lo que explica por qué el rendimiento de carga de la página no mejoró necesariamente con la menor cantidad de código.
webpack usa 30 KB como tamaño mínimo predeterminado para generar un fragmento. Sin embargo, combinar un valor de maxInitialRequests
de 25 con un tamaño mínimo de 20 KB generó 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 etiquetas de secuencia de comandos más nuevas en cada transición de ruta. Pero, ¿cómo predeterminan estos fragmentos dinámicos en el momento de la compilación?
Next.js usa un archivo de manifiesto de compilación del servidor para determinar qué fragmentos generados se usan en diferentes puntos de entrada. Para proporcionar esta información al cliente también, se creó un archivo de manifiesto de compilación abreviado del cliente 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}`)) || []
)
}

Esta nueva estrategia de fragmentación granular se lanzó por primera vez en Next.js detrás de una marca, donde se probó en varios usuarios pioneros. Muchos sitios observaron reducciones significativas en el total de JavaScript que se usaba en 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% |
La versión final se lanzó de forma predeterminada en la versión 9.2.
Gatsby
Gatsby solía seguir el mismo enfoque de usar una heurística basada en el uso 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 división granular similar, también observaron 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% |
Consulta la solicitud de extracción para comprender cómo implementaron esta lógica en su configuración de webpack, que se incluye de forma predeterminada en la versión 2.20.7.
Conclusión
El concepto de enviar fragmentos detallados no es específico de Next.js, Gatsby ni siquiera webpack. Todos deberían considerar mejorar la estrategia de división de su aplicación si sigue un enfoque de paquete "común" grande, independientemente del framework o del bundler de módulos que se use.
- Si deseas ver las mismas optimizaciones de fragmentación aplicadas a una aplicación de React estándar, consulta esta aplicación de ejemplo de React. Utiliza una versión simplificada de la estrategia de fragmentación granular y puede ayudarte a comenzar a aplicar el mismo tipo de lógica a tu sitio.
- En Rollup, los fragmentos se crean de forma granular de forma predeterminada. Consulta
manualChunks
si deseas configurar manualmente el comportamiento.