Una estrategia de fragmentació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 marcos de trabajo en el ecosistema de código abierto de JavaScript. Recientemente, se agregaron varias optimizaciones más nuevas para mejorar el rendimiento de carga de Next.js y Gatsby. En este artículo, se describe una estrategia mejorada de fragmentación detallada 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 agrupador principal. webpack v3 introdujo CommonsChunkPlugin
para permitir la salida de módulos compartidos entre diferentes puntos de entrada en un solo (o varios) fragmentos "comunes". El código compartido se puede descargar por separado y almacenar en la caché del navegador desde el principio, lo que puede generar un mejor rendimiento de carga.
Este patrón se hizo popular con muchos frameworks de aplicaciones de una sola página que adoptaron una configuración de punto de entrada y paquete que se veía de la siguiente manera:
Aunque 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 todos los puntos 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 algunos otros, webpack v4 quitó el complemento en favor de uno nuevo: SplitChunksPlugin
.
División en fragmentos mejorada
La configuración predeterminada de SplitChunksPlugin
funciona bien para la mayoría de los usuarios. Se crean varios fragmentos divididos según una serie 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 "comunes únicos" para la división de fragmentos. Next.js, por ejemplo, generaría un paquete commons
que contendría 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. Si modificas esta proporción, solo se producirá 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 (superior a 160 KB) se divide en su propio fragmento individual.
- Se crea un fragmento
frameworks
independiente para las dependencias del framework (react
,react-dom
, etc.) - Se crean tantos fragmentos compartidos como sea necesario (hasta 25)
- El tamaño mínimo para que se genere un fragmento se cambió a 20 KB.
Esta estrategia de división detallada ofrece 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.
- Se mejoró el almacenamiento en caché durante las navegaciones. Dividir bibliotecas grandes y dependencias de 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ó las bases para el fragmentación detallada, y aplicar este enfoque a un framework como Next.js no era un concepto completamente nuevo. Sin embargo, muchos frameworks seguían usando una sola heurística y una estrategia de paquetes "comunes" por algunos motivos. Esto incluye la preocupación de que muchas más solicitudes HTTP puedan afectar negativamente el rendimiento del sitio.
Los navegadores solo pueden abrir una cantidad limitada de conexiones TCP a un solo origen (6 para Chrome), por lo que minimizar la cantidad de fragmentos que genera un empaquetador puede garantizar que la cantidad total de solicitudes permanezca por debajo de este umbral. Sin embargo, esto solo es válido para 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 tenemos que preocuparnos por limitar la cantidad de fragmentos que emite nuestro empaquetador.
Todos los navegadores principales admiten HTTP/2. Los equipos de Chrome y Next.js querían ver si aumentar la cantidad de solicitudes dividiendo el único paquete “común” de Next.js en varios fragmentos compartidos afectaría el rendimiento de la carga de alguna manera. Comenzó midiendo el rendimiento de un solo sitio mientras modificaba la cantidad máxima de solicitudes simultáneas con la propiedad maxInitialRequests
.
En un promedio de tres ejecuciones de varias pruebas en una sola página web, los tiempos de load
, inicio de renderización y Primer procesamiento de imagen con contenido permanecieron aproximadamente iguales cuando se varió el recuento máximo de solicitudes iniciales (de 5 a 15). Curiosamente, notamos una ligera sobrecarga de rendimiento solo después de dividirlo de forma agresiva en cientos de solicitudes.
Esto demostró que mantenerse por debajo de un umbral confiable (entre 20 y 25 solicitudes) lograba el equilibrio correcto 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 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.
El objetivo de este experimento era solo 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 lo más óptimo, ya que redujo el tamaño de la carga útil de JavaScript sin ralentizar la página. La cantidad total de JavaScript que se necesitaba para hidratar la página seguía siendo aproximadamente la misma, lo que explica por qué el rendimiento de carga de la página no mejoró necesariamente con la cantidad reducida de código.
Webpack usa 30 KB como tamaño mínimo predeterminado para que se genere 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é.
Reducción 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 para cada transición de ruta. Pero, ¿cómo se predeterminan estos fragmentos dinámicos en el tiempo de compilación?
Next.js usa un archivo de manifiesto de compilación del servidor para determinar qué fragmentos generados usan los diferentes puntos de entrada. Para proporcionar esta información al cliente, se creó un archivo de manifiesto de compilación resumido 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 detallada se lanzó por primera vez en Next.js detrás de una marca, donde se probó en una serie de usuarios pioneros. Muchos observaron reducciones significativas en el JavaScript total que se usaba en todo su 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 envió 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)[\\/]/,
},
Cuando optimizaron su configuración de Webpack para adoptar una estrategia de fragmentación detallada similar, también observaron reducciones significativas 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 PR para comprender cómo implementaron esta lógica en su configuración de Webpack, que se envía de forma predeterminada en la versión 2.20.7.
Conclusión
El concepto de envío de fragmentos detallados no es específico de Next.js, Gatsby ni siquiera de webpack. Todos deberían considerar mejorar la estrategia de fragmentación de su aplicación si sigue un enfoque de paquete "común" grande, independientemente del framework o el empaquetador de módulos que se use.
- Si deseas ver las mismas optimizaciones de fragmentación aplicadas a una aplicación React sin modificaciones, consulta esta app de React de ejemplo. Usa una versión simplificada de la estrategia de fragmentación detallada y puede ayudarte a comenzar a aplicar el mismo tipo de lógica a tu sitio.
- En el caso de Rollup, los fragmentos se crean de forma granular de forma predeterminada. Consulta
manualChunks
si deseas configurar el comportamiento de forma manual.