Encontrar la mejor manera de organizar los materiales del proyecto web puede ser una tarea desalentadora. Hay muchos escenarios diferentes para que los usuarios trabajen con proyectos, muchas tecnologías y otros factores que deben tenerse en cuenta.
El autor del material, cuya traducción publicamos hoy, dice que quiere contar aquí todo lo que necesita saber para la preparación competente de los materiales del proyecto web para el trabajo. En primer lugar, se tratará de cómo elegir una estrategia para separar los archivos del sitio que mejor se adapte a un proyecto en particular y a sus usuarios. En segundo lugar, se considerarán los medios para implementar la estrategia elegida.

Información general
Según el
Glosario de Webpack , hay dos estrategias para compartir archivos. Esto es división de paquetes y división de código. Puede parecer que estos términos se usan indistintamente, pero no lo son.
- Dividir un paquete es una técnica para dividir paquetes grandes en varias partes, que son archivos más pequeños. Dichos archivos, en cualquier caso, como cuando se trabaja con un solo paquete, serán descargados por todos los usuarios del sitio. La fuerza de esta técnica es mejorar el uso de mecanismos de almacenamiento en caché basados en el navegador.
- La separación de código es un enfoque que implica cargar dinámicamente el código a medida que surja la necesidad. Esto lleva al hecho de que el usuario solo descarga el código que necesita para trabajar con una determinada parte del sitio en un momento determinado.
La división de código parece ser mucho más interesante que la división de código. Y, de hecho, existe la sensación de que en muchos artículos sobre nuestro tema, el enfoque principal está en la separación del código, esta técnica se considera como la única forma útil de optimizar los materiales del sitio.
Sin embargo, me gustaría decir que para muchos sitios es la primera estrategia que es mucho más valiosa: la separación de paquetes. Y, tal vez, literalmente todos los proyectos web pueden ganar con su implementación.
Hablemos de esto con más detalle.
Separación de paquetes
La técnica para dividir paquetes se basa en una idea muy simple. Si tiene un archivo enorme y cambia una sola línea de código, el usuario normal tendrá que descargar el archivo completo la próxima vez que visite el sitio. Sin embargo, si divide este archivo en dos archivos, el mismo usuario tendrá que descargar solo el que se ha cambiado, y el segundo archivo se tomará de la memoria caché del navegador.
Vale la pena señalar que, dado que la optimización de los materiales del sitio al dividir los paquetes está vinculada al almacenamiento en caché, los usuarios que visitan el sitio por primera vez tendrán que descargar todos los materiales de todos modos, por lo que no les importa si estos materiales se presentarán como un solo archivo o como varios .
Me parece que hablar demasiado sobre el rendimiento de los proyectos web está dedicado a los usuarios que visitan el sitio por primera vez. Quizás esto se deba en parte a la importancia de la primera impresión que el proyecto causará en el usuario, así como al hecho de que la cantidad de datos transmitidos a los usuarios cuando visitan el sitio por primera vez es simple y conveniente de medir.
Cuando se trata de visitantes regulares, puede ser difícil medir el impacto de las técnicas de optimización de materiales que se les aplican. Pero simplemente debemos conocer las consecuencias de tales optimizaciones.
Para analizar estas cosas, necesita algo como una hoja de cálculo. También deberá crear una lista estricta de condiciones bajo las cuales podamos probar cada una de las estrategias de almacenamiento en caché estudiadas.
Aquí hay un script que se ajusta a la descripción general dada en el párrafo anterior:
- Alice visita nuestro sitio una vez por semana durante 10 semanas.
- Actualizamos el sitio una vez por semana.
- Cada semana actualizamos la página de la lista de productos.
- Además, tenemos una página con detalles del producto, pero aún no estamos trabajando en ello.
- En la quinta semana, agregamos un nuevo paquete npm a los materiales del proyecto.
- En la octava semana, actualizamos uno de los paquetes npm ya utilizados en el proyecto.
Hay personas (como yo) que intentarán hacer que este escenario sea lo más realista posible. Pero no necesitas hacer eso. El escenario real aquí realmente no importa. Por qué es así, lo descubriremos pronto.
▍ Condiciones iniciales
Supongamos que el tamaño total de nuestro paquete de JavaScript es de 400 Kb considerables y, en las condiciones actuales, transferimos todo esto al usuario como un solo archivo
main.js
Tenemos una configuración de Webpack, que, en términos generales, es similar a la siguiente (eliminé las cosas que no son relevantes para nuestra conversación):
const path = require('path'); module.exports = { entry: path.resolve(__dirname, 'src/index.js'), output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash].js', }, };
Webpack nombra el archivo
main.js
resultante cuando hay una sola entrada en la configuración.
Si no tiene una muy buena idea de trabajar con el caché, tenga en cuenta que cada vez que escribo
main.js
aquí, en realidad me refiero a algo como
main.xMePWxHo.js
. Una secuencia loca de caracteres es un hash del contenido de un archivo, lo que se llama
contenthash
en la configuración. El uso de este enfoque lleva al hecho de que, al cambiar el código, los nombres de los archivos también cambian, lo que obliga al navegador a descargar nuevos archivos.
De acuerdo con el escenario anterior, cuando realizamos algunos cambios en el código del sitio cada semana,
contenthash
línea
contenthash
del paquete. Como resultado, al visitar semanalmente nuestro sitio, Alice se ve obligada a cargar un nuevo archivo de 400 Kb.
Si hacemos una buena tableta (con una línea de resultados inútil hasta ahora) que contiene datos sobre el volumen semanal de carga de datos por este archivo, obtenemos lo siguiente.
La cantidad de datos cargados por el usuarioComo resultado, resulta que el usuario, en 10 semanas, descargó 4,12 MB de código. Este indicador se puede mejorar.
▍ Separación de paquetes de terceros del código principal
Divida el paquete grande en dos partes. Nuestro propio código estará en el archivo
main.js
y el código de terceros en el archivo
vendor.js
. Es fácil hacer esto, la siguiente configuración de Webpack nos ayudará con esto:
const path = require('path'); module.exports = { entry: path.resolve(__dirname, 'src/index.js'), output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash].js', }, optimization: { splitChunks: { chunks: 'all', }, }, };
Webpack 4 intenta hacer la vida lo más fácil posible para el desarrollador, por lo que hace todo lo que puede y no requiere que se le diga exactamente cómo dividir los paquetes en partes.
Tal comportamiento automático del programa lleva a algunas delicias, como: "Bueno, qué encanto es este Webpack", y a muchas preguntas en el espíritu: "¿Qué se hace con mis paquetes?".
En cualquier caso, agregar
optimization.splitChunks.chunks = 'all'
a la configuración de configuración le dice a Webpack que necesitamos que tome todo de
node_modules
y lo coloque en el
vendors~main.js
Después de hacer una separación tan básica del paquete, Alice, que visita regularmente nuestro sitio semanalmente, descargará el archivo
main.js
de 200 Kb cada vez que lo visite. Pero ella descargará el archivo
vendor.js
solo tres veces. Esto sucederá durante las visitas en la primera, quinta y octava semana. Aquí está la tabla correspondiente, en la cual, por voluntad del destino, los tamaños de los
vendor.js
main.js
y
vendor.js
en las primeras cuatro semanas coinciden y equivalen a 200 Kb.
La cantidad de datos cargados por el usuarioComo resultado, resulta que la cantidad de datos descargados por el usuario durante 10 semanas fue de 2.64 MB. Es decir, en comparación con lo que era antes de la separación del paquete, el volumen disminuyó en un 36%. No se logró un resultado tan malo al agregar algunas líneas al archivo de configuración. Por cierto, antes de seguir leyendo, haga lo mismo en su proyecto. Y si necesita actualizar de Webpack 3 a 4, hágalo y no se preocupe, ya que el proceso es bastante simple y gratuito.
Me parece que la mejora considerada aquí parece algo abstracta, ya que se extiende durante 10 semanas. Sin embargo, si consideramos la cantidad de datos enviados a un usuario leal, esta es una reducción honesta en este volumen en un 36%. Este es un muy buen resultado, pero se puede mejorar.
▍ Resalte paquetes en archivos separados
El archivo
vendor.js
el mismo problema que el
main.js
original
main.js
Consiste en el hecho de que cambiar cualquier paquete incluido en este archivo lleva a la necesidad de que un usuario regular descargue todo el archivo nuevamente.
¿Por qué no creamos archivos separados para cada paquete npm? No es difícil hacer esto, así que descompongamos nuestra
react
,
lodash
,
redux
,
moment
, etc. en archivos separados. La siguiente configuración de Webpack nos ayudará con esto:
const path = require('path'); const webpack = require('webpack'); module.exports = { entry: path.resolve(__dirname, 'src/index.js'), plugins: [ new webpack.HashedModuleIdsPlugin(), // ], output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash].js', }, optimization: { runtimeChunk: 'single', splitChunks: { chunks: 'all', maxInitialRequests: Infinity, minSize: 0, cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name(module) { // , node_modules/packageName/not/this/part.js // node_modules/packageName const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]; // npm- , , // URL, @ return `npm.${packageName.replace('@', '')}`; }, }, }, }, }, };
En la
documentación puede encontrar una excelente explicación de las construcciones utilizadas aquí, pero todavía dedico algo de tiempo a contar algunas cosas, ya que me tomó mucho tiempo usarlas correctamente.
- Webpack tiene instalaciones estándar bastante razonables, que, de hecho, no son tan razonables. Por ejemplo, el número máximo de archivos de salida se establece en 3, el tamaño mínimo del archivo es de 30 KB (es decir, se fusionarán archivos más pequeños). Lo redefiní
cacheGroups
es donde establecemos las reglas sobre cómo Webpack debería agrupar datos en archivos de salida. Tengo un grupo aquí, vendor
, que se usará para cualquier módulo cargado desde node_modules
. Por lo general, el nombre del archivo de salida se da como una cadena. Pero di name
como una función que se llamará para cada archivo procesado. Luego tomo el nombre del paquete de la ruta del módulo. Como resultado, obtenemos un archivo para cada paquete. Por ejemplo, npm.react-dom.899sadfhj4.js
.- Los nombres de paquetes, para que puedan publicarse en npm, deben ser adecuados para su uso en URL , por lo que no es necesario que
encodeURI
operación packageName
nombres de packageName
. Sin embargo, me encontré con un problema de que el servidor .NET se niega a trabajar con archivos que tienen el símbolo @
en sus nombres (tales nombres se usan para paquetes con un alcance de nombre dado, los llamados paquetes de ámbito), por lo que yo, en el correspondiente fragmento de código, me deshago de esos personajes.
La configuración anterior de Webpack es buena, ya que puede configurarla una vez y luego olvidarse de ella. No requiere hacer referencia a paquetes específicos por nombre, por lo tanto, después de su creación, incluso cuando se cambia la composición de los paquetes, sigue siendo relevante.
Alice, nuestra visitante habitual, todavía
main.js
200 kilobytes cada semana, y la primera vez que visita el sitio, tiene que descargar 200 Kb de paquetes npm, pero no tiene que descargar los mismos paquetes dos veces.
A continuación se muestra una nueva versión de la tabla con información sobre el volumen de descargas de datos semanales. Por una extraña coincidencia, el tamaño de cada archivo con paquetes npm es de 20 Kb.
La cantidad de datos cargados por el usuarioAhora el volumen de datos descargados en 10 semanas es de 2.24 Mb. Esto significa que hemos mejorado la tasa base en un 44%. El resultado ya es muy decente, pero surge la pregunta de si es posible hacerlo para lograr un resultado superior al 50%. Si esto sucede, será genial.
▍ Dividir el código de la aplicación en fragmentos
Regresamos al archivo
main.js
, que la desafortunada Alice tiene que descargar constantemente.
Como dije anteriormente, hay dos secciones separadas en nuestro sitio web. El primero es una lista de productos, el segundo es una página con información detallada sobre el producto. El tamaño del código, único para cada uno de ellos, es de 25 Kb (y se usan 150 Kb de código tanto allí como allí).
La página de información del producto no está sujeta a cambios, ya que la hemos perfeccionado. Por lo tanto, si extraemos su código en un archivo separado, este archivo, la mayoría de las veces trabajando con el sitio, se descargará al navegador desde el caché.
Además, resultó que tenemos un gran archivo SVG incorporado que se utiliza para la representación de iconos, que pesa hasta 25 KB y rara vez cambia.
Algo debe hacerse con esto.
Creamos manualmente varios puntos de entrada, diciéndole a Webpack que necesitaba crear un archivo separado para cada una de estas entidades.
module.exports = { entry: { main: path.resolve(__dirname, 'src/index.js'), ProductList: path.resolve(__dirname, 'src/ProductList/ProductList.js'), ProductPage: path.resolve(__dirname, 'src/ProductPage/ProductPage.js'), Icon: path.resolve(__dirname, 'src/Icon/Icon.js'), }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash:8].js', }, plugins: [ new webpack.HashedModuleIdsPlugin(), // ], optimization: { runtimeChunk: 'single', splitChunks: { chunks: 'all', maxInitialRequests: Infinity, minSize: 0, cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name(module) { // , node_modules/packageName/not/this/part.js // node_modules/packageName const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]; // npm- , , // URL, @ return `npm.${packageName.replace('@', '')}`; }, }, }, }, }, };
El Webpack que trabaja duro, además, creará archivos para lo que es común, por ejemplo,
ProductList
y
ProductPage
, es decir, no habrá código duplicado.
Lo que acabamos de hacer le permitirá a Alice ahorrar 50 Kb de tráfico casi todas las semanas. Tenga en cuenta que editamos el archivo de descripción del icono en la sexta semana. Aquí está nuestra mesa tradicional.
La cantidad de datos cargados por el usuarioAhora, en solo diez semanas, solo se han descargado 1.815 MB de datos. Esto significa que el ahorro de tráfico fue un impresionante 56%. De acuerdo con nuestro escenario teórico, un usuario habitual siempre trabajará con este nivel de ahorro.
Todo esto se hace debido a los cambios realizados en la configuración de Webpack. No cambiamos el código de la aplicación para lograr tales resultados.
Anteriormente, hablé sobre el hecho de que el escenario específico en el que se lleva a cabo dicha prueba, de hecho, no juega un papel especial. Esto se dice debido al hecho de que, independientemente del escenario utilizado, la conclusión de todo lo que hablamos será la misma: dividir la aplicación en pequeños archivos que tengan sentido en la aplicación a su arquitectura nos permite reducir el volumen de datos del sitio, cargado por sus usuarios habituales.
Pronto comenzaremos a hablar sobre la separación de códigos, pero primero me gustaría responder tres preguntas en las que probablemente esté pensando ahora.
▍ Pregunta número 1. ¿La necesidad de realizar muchas solicitudes no perjudica la velocidad de carga del sitio?
Puede dar una respuesta breve y simple a esta pregunta: "No, no hace daño". Una situación similar resultó en un problema en los viejos tiempos, cuando el protocolo HTTP / 1.1 estaba en uso, y cuando usaba HTTP / 2, esto ya no es relevante.
Sin embargo, debe tenerse en cuenta que en
este artículo, publicado en 2016, y en
este artículo de la Academia Khan 2015, se llega a la conclusión de que incluso cuando se usa HTTP / 2, el uso de demasiados archivos ralentiza la descarga. Pero en ambos materiales, "demasiado" significa "varios cientos". Por lo tanto, vale la pena recordar que si tiene que trabajar con cientos de archivos, las restricciones en el procesamiento de datos en paralelo pueden afectar su velocidad de descarga.
Si está interesado, el soporte HTTP / 2 está disponible en IE 11 en Windows 10. Además, hice un estudio exhaustivo entre aquellos que usan sistemas más antiguos. Por unanimidad declararon que la velocidad de carga de su sitio web no era particularmente preocupante.
▍ Pregunta número 2. Los paquetes de paquetes web tienen código auxiliar. ¿Crea carga adicional en el sistema?
Si lo es
▍ Pregunta número 3. Cuando se trabaja con muchos archivos pequeños, su nivel de compresión se deteriora, ¿verdad?
Sí, eso también es cierto. De hecho, me gustaría decir esto:
- Más archivos significa más código auxiliar de Webpack.
- Más archivos significa menos compresión.
Vamos a resolverlo para entender qué tan malo es esto.
Acabo de realizar una prueba en la que el código de un archivo de 190 Kb se dividió en 19 partes. Esto agregó aproximadamente un 2% a la cantidad de datos enviados al navegador.
Como resultado, resulta que en la primera visita al sitio, el usuario cargará un 2% más de datos, y en visitas posteriores, un 60% menos, y esto continuará durante mucho, mucho tiempo.
Entonces, ¿vale la pena preocuparse? No, no vale la pena.
Cuando comparé un sistema que usa 1 archivo y un sistema con 19 archivos, lo probé usando varios protocolos, incluido HTTP / 1.1. La siguiente tabla respalda firmemente la idea de que tener más archivos significa mejor.
Datos sobre el trabajo con 2 versiones de un sitio alojado en un alojamiento estático Firebase, cuyo código tiene un tamaño de 190 Kb, pero, en el primer caso, se empaqueta en 1 archivo, y en el segundo se divide en 19Cuando se trabaja en redes 3G y 4G, descargar un sitio con 19 archivos tomó un 30% menos de tiempo que descargar un sitio con un archivo.
Hay mucho ruido en los datos presentados en la tabla. Por ejemplo, una sesión de descarga de un sitio por 4G (ejecución 2 en la tabla) tomó 646 ms, otra (ejecución 4) - 1116 ms, que es un 73% más larga. Por lo tanto, existe la sensación de que decir que HTTP / 2 es "30% más rápido" es algo deshonesto.
Creé esta tabla para ver qué da el uso de HTTP / 2. Pero, de hecho, lo único que se puede decir aquí es que el uso de HTTP / 2 probablemente no afecta particularmente la carga de la página.
Las últimas dos líneas en esta tabla fueron una verdadera sorpresa. Aquí están los resultados para no la última versión de Windows con IE11 y HTTP / 1.1. Si tratara de predecir los resultados de la prueba por adelantado, definitivamente diría que tal configuración cargaría los materiales mucho más lentamente que otros. Es cierto, aquí se usó una conexión de red muy rápida, y yo, para tales pruebas, probablemente debería usar algo más lento.
Y ahora te contaré una historia. Para explorar mi sitio en un sistema muy antiguo, descargué la máquina virtual Windows 7 del
sitio web de Microsoft. Allí se instaló IE8, que decidí actualizar a IE9. Para hacer esto, fui a la página de Microsoft diseñada para descargar IE 9. Pero no pude hacer esto.
Que mala suerte ...Por cierto, si hablamos de HTTP / 2, quiero señalar que este protocolo está integrado en Node.js. Si desea experimentar, puede usar el pequeño
servidor HTTP / 2 que escribí con soporte para la caché de respuesta, gzip y brotli.
Tal vez, dije todo lo que quería sobre el método de separación de paquetes. Creo que el único inconveniente de este enfoque, cuando se utiliza qué usuarios tienen que cargar muchos archivos, de hecho, no es un "menos".
Ahora hablemos de la separación de código.
Separación de código
La idea principal de la técnica de división de código es: "No descargue código innecesario". Me dijeron que usar este enfoque solo tiene sentido para algunos sitios.
Prefiero, cuando se trata de la separación de código, usar la regla 20/20 que acabo de formular. Si hay una parte del sitio que es visitada solo por el 20% de los usuarios, y su funcionalidad es proporcionada por más del 20% del código JavaScript del sitio, entonces este código solo debe descargarse a pedido.
Estos, por supuesto, no son números absolutos, se pueden ajustar a una situación específica, y en realidad hay escenarios mucho más complejos que el descrito anteriormente. Lo más importante aquí es el equilibrio, y es completamente normal no utilizar la separación de código en absoluto, si esto no tiene sentido para su sitio.
▍ ¿Separado o no?
¿Cómo encontrar la respuesta a la pregunta de si necesita separación de código o no? Supongamos que tiene una tienda en línea y está pensando en separar del código el resto del código que se utiliza para recibir el pago de los clientes, ya que solo el 30% de los visitantes le compran algo.
Que puedo decir En primer lugar, debe trabajar para llenar la tienda y vender algo que sea interesante para más visitantes al sitio. En segundo lugar, debe comprender cuánto código es completamente único para la sección del sitio donde se acepta el pago. Como siempre debe hacer una "división de paquetes" antes de la "división de código", y con suerte lo hará, probablemente ya sepa qué tamaños tiene el código que nos interesa.
Quizás este código resulte ser más pequeño de lo que piensas, así que antes de alegrarte con la nueva oportunidad de optimizar tu sitio, debes calcular todo de manera segura. Si, por ejemplo, tiene un sitio React, el repositorio, los reductores, el sistema de enrutamiento y las acciones serán compartidos por todas las partes del sitio. Único para diferentes partes del código del sitio estará representado principalmente por componentes y funciones auxiliares para ellos.
Entonces, descubrió que un código completamente único de la sección del sitio utilizada para pagar las compras toma 7 Kb. El tamaño del resto del código del sitio es de 300 Kb. En tal situación, no participaría en la separación del código por varias razones:
- Si descarga estos 7 Kb por adelantado, el sitio no se ralentizará. Recuerde que los archivos se descargan en paralelo e intente medir la diferencia necesaria para descargar 300 Kb y 307 Kb de código.
- Si descarga este código más tarde, el usuario tendrá que esperar después de hacer clic en el botón "Pagar". Y este es el momento en que necesitas que todo salga de la mejor manera posible.
- La separación de código requiere cambios en la aplicación. En el código, en lugares donde todo se hizo sincrónicamente antes, aparece la lógica asincrónica. , , , , , .
, , , .
.
▍
, , , .
. , . :
require('whatwg-fetch'); require('intl'); require('url-polyfill'); require('core-js/web/dom-collections'); require('core-js/es6/map'); require('core-js/es6/string'); require('core-js/es6/array'); require('core-js/es6/object');
index.js
, :
import './polyfills'; import React from 'react'; import ReactDOM from 'react-dom'; import App from './App/App'; import './index.css'; const render = () => { ReactDOM.render(<App />, document.getElementById('root')); } render(); // ,
Webpack , , npm-. 25 , 90% , .
Webpack 4
import()
(
import
), :
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App/App'; import './index.css'; const render = () => { ReactDOM.render(<App />, document.getElementById('root')); } if ( 'fetch' in window && 'Intl' in window && 'URL' in window && 'Map' in window && 'forEach' in NodeList.prototype && 'startsWith' in String.prototype && 'endsWith' in String.prototype && 'includes' in String.prototype && 'includes' in Array.prototype && 'assign' in Object && 'entries' in Object && 'keys' in Object ) { render(); } else { import('./polyfills').then(render); }
, , , — . —
render()
. , Webpack npm-, ,
render()
.
,
import()
Babel
dynamic-import . , Webpack,
import() , .
, . .
▍ React,
. , , , .
, npm- . , , 100 .
, , URL
/admin
,
<AdminPage>
. Webpack ,
import AdminPage from './AdminPage.js'
.
. , ,
import('./AdminPage.js')
, Webpack , .
, .
, ,
AdminPage
, , URL
/admin
. , :
import React from 'react'; class AdminPageLoader extends React.PureComponent { constructor(props) { super(props); this.state = { AdminPage: null, } } componentDidMount() { import('./AdminPage').then(module => { this.setState({ AdminPage: module.default }); }); } render() { const { AdminPage } = this.state; return AdminPage ? <AdminPage {...this.props} /> : <div>Loading...</div>; } } export default AdminPageLoader;
. (, URL
/admin
),
./AdminPage.js
, .
render()
,
<AdminPage>
,
<div>Loading...</div>
,
<AdminPage>
, .
,
react-loadable
,
React .
Resumen
, , (, , CSS). :
Estimados lectores! ?