Durante mucho tiempo hemos ignorado el tema de los navegadores, CSS y accesibilidad y decidimos volver a él con la traducción del material general de hoy (original - febrero de 2020). Estoy especialmente interesado en su opinión sobre la tecnología de renderización del servidor mencionada aquí, así como sobre la urgencia de la necesidad de un libro completo sobre HTTP / 2; sin embargo, hablemos de todo en orden.
Esta publicación describe algunas técnicas para acelerar la carga de aplicaciones front-end y así mejorar la usabilidad.
Echemos un vistazo a la arquitectura general de la interfaz. ¿Cómo se asegura de que los activos críticos se carguen primero y maximice la probabilidad de que esos activos ya terminen en la caché?
No me detendré en cómo el backend debe entregar recursos, si su página necesita ser renderizada como una aplicación cliente o cómo optimizar el tiempo de renderizado de su aplicación.
Visión de conjunto
Analicemos el proceso de descarga de la aplicación en tres pasos separados:
- Representación primaria: ¿cuánto tiempo pasará antes de que el usuario vea algo?
- Descarga de la aplicación: ¿cuánto tiempo pasará antes de que el usuario vea la aplicación?
- – ?
Hasta la etapa de renderizado primario (renderizado), el usuario simplemente no puede ver nada. Para renderizar una página, necesita al menos un documento HTML, pero en la mayoría de los casos también necesita cargar recursos adicionales, como archivos CSS y JavaScript. Si está disponible, el navegador puede comenzar a renderizar en la pantalla.
En esta publicación, usaré diagramas de cascada de WebPageTest . La cascada de solicitudes para su sitio se verá así.
Se carga un conjunto de otros archivos junto con el documento HTML y la página se procesa después de que estén todos en la memoria. Tenga en cuenta que los archivos CSS se cargan en paralelo, por lo que cada solicitud posterior no aumenta significativamente el retraso.
Reducir la cantidad de solicitudes de bloqueo de procesamiento
Las hojas de estilo y los elementos de secuencia de comandos (de forma predeterminada) no permiten que se muestre ningún contenido debajo de ellos.
Hay varias formas de solucionar este problema:
- Colocamos etiquetas de script en la parte inferior de la etiqueta.
body - Cargue scripts de forma asincrónica usando
async - Escriba pequeños trozos de JS o CSS en línea si desea cargarlos sincrónicamente
Evite renderizar cadenas de consultas bloqueadas
No es solo la cantidad de solicitudes de bloqueo de procesamiento lo que puede ralentizar su sitio. Lo que importa es el tamaño de cada uno de esos recursos que deben descargarse, y también cuándo exactamente el navegador detecta que el recurso debe descargarse.
Si el navegador se da cuenta de la necesidad de descargar el archivo solo después de que se complete otra solicitud, entonces puede terminar con una cadena de solicitudes sincrónicas. Esto puede suceder por varias razones:
- Tener reglas
@importen CSS - Usar fuentes web a las que se hace referencia en un archivo CSS
- Enlace de inyección de JavaScript o etiquetas de script
Considere este ejemplo:
uno de los archivos CSS de este sitio contiene una regla
@importpara cargar una fuente de Google. Así, el navegador debe ejecutar las siguientes solicitudes una a una, en este orden:
- HTML del documento
- CSS de la aplicación
- CSS de fuentes de Google
- Archivo Google Font Woff (no se muestra en cascada)
Para solucionar esto, primero muevamos la solicitud CSS de Google Fonts desde
@importla etiqueta de enlace en el documento HTML. Esto acortará la cadena en un eslabón.
Para acelerar aún más, incruste el archivo CSS de Google Fonts directamente en su archivo HTML o CSS.
(Recuerde que la respuesta CSS de Google Fonts depende del agente de usuario. Si realiza una solicitud usando IE8, CSS hará referencia a un archivo EOT (incrustado por OpenType), IE11 recibirá un archivo woff y los navegadores modernos recibirán un woff2. Pero si está satisfecho con él funciona como con navegadores relativamente antiguos que utilizan fuentes del sistema, simplemente puede copiar y pegar el contenido del archivo CSS).
Incluso después de que la página comience a renderizarse, es posible que el usuario no pueda hacer nada con ella, ya que no se mostrará ningún texto hasta que la fuente esté completamente cargada. Esto se puede evitar utilizando la propiedad de intercambio de visualización de fuentes , que ahora es la predeterminada en Google Fonts.
A veces no es posible deshacerse por completo de la cadena de solicitudes. En tales casos, intente utilizar la etiqueta
preloado preconnect. Por ejemplo, el sitio que se muestra arriba podría conectarse fonts.googleapis.comantes de que se realice la solicitud CSS real.
Reutilizar las conexiones del servidor para acelerar las solicitudes
Por lo general, el establecimiento de una nueva conexión con el servidor requiere 3 pases de ida y vuelta entre el navegador y el servidor:
- Búsqueda de DNS
- Establecer una conexión TCP
- Establecer una conexión SSL
Una vez establecida la conexión, se requiere al menos 1 viaje de ida y vuelta más: envía una solicitud y descarga una respuesta.
Como se muestra en la cascada a continuación, las conexiones se inician en cuatro servidores diferentes: hostgator.com, optimizely.com, googletagmanager.com y googelapis.com.
Sin embargo , las solicitudes posteriores al servidor afectado pueden reutilizar la conexión existente. Por lo tanto,
base.csso index1.cssse cargan rápidamente, ya que también se encuentran en hostgator.com.
Reducir el tamaño del archivo y usar redes de entrega de contenido (CDN)
La duración de la solicitud, junto con el tamaño del archivo, está influenciada por otros dos factores que controlas: el tamaño del recurso y la ubicación de tus servidores.
Envía al usuario la cantidad mínima requerida de datos, además, ocúpate de su compresión (por ejemplo, usando brotli o gzip).
Las redes de entrega de contenido (CDN) proporcionan servidores en una amplia variedad de ubicaciones, por lo que es muy probable que una de ellas esté ubicada cerca de sus usuarios. Puede conectarlos no a su servidor de aplicaciones central, sino al servidor más cercano en la CDN. Por lo tanto, la ruta de datos hacia y desde el servidor se reducirá significativamente. Esto es especialmente útil cuando se trabaja con recursos estáticos como CSS, JavaScript e imágenes porque son fáciles de distribuir.
Evitando la red con trabajadores de servicios
Los trabajadores del servicio le permiten interceptar solicitudes antes de que ingresen a la red. Por lo tanto, ¡el primer renderizado puede ocurrir casi instantáneamente !
Por supuesto, esto solo funciona si desea que la red simplemente envíe una respuesta. Esta respuesta ya debería estar almacenada en caché, lo que facilitará la vida de sus usuarios cuando vuelvan a descargar su aplicación.
El trabajador del servicio que se muestra a continuación almacena en caché el HTML y CSS necesarios para representar la página. Cuando se vuelve a cargar, la aplicación intenta emitir recursos en caché y, si no están disponibles, recurre a la red como respaldo.
self.addEventListener("install", async e => {
caches.open("v1").then(function (cache) {
return cache.addAll(["/app", "/app.css"]);
});
});
self.addEventListener("fetch", event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
return cachedResponse || fetch(event.request);
})
);
});
Para obtener más información sobre la precarga y el almacenamiento en caché de recursos con trabajadores del servicio, consulte este tutorial .
Descarga de la aplicación
Bien, nuestro usuario ya ha visto algo. ¿Qué más necesita para poder utilizar nuestra aplicación?
- Cargando la aplicación (JS y CSS)
- Cargando los datos más importantes de una página
- Descargar datos e imágenes adicionales
Tenga en cuenta que no solo cargar datos a través de la red puede ralentizar el procesamiento. Cuando se carga el código, el navegador deberá analizarlo, compilarlo y ejecutarlo.
Dividir el paquete: cargue solo el código necesario y maximice los accesos al caché.
Al dividir el paquete, puede descargar solo el código que necesita solo para esta página, y no descargar la aplicación completa. Al dividir un paquete, se puede almacenar en caché en partes, incluso si otras partes del código han cambiado y deben recargarse.
Normalmente, el código consta de tres tipos diferentes de archivos:
- Código específico de esta página
- Código de aplicación compartido
- Módulos de terceros que rara vez cambian (¡excelente para el almacenamiento en caché!)
Webpack puede dividir automáticamente el código dividido para reducir el peso total de las descargas, esto se hace usando optimization.splitChunks . Asegúrese de habilitar el fragmento de tiempo de ejecución para que los hash de los fragmentos permanezcan estables y el almacenamiento en caché a largo plazo se pueda usar de manera útil. Ivan Akulov ha escrito una guía completa sobre cómo compartir y almacenar en caché el código de Webpack.
La división del código específico de la página no se puede hacer automáticamente, por lo que debe identificar los fragmentos que se pueden cargar por separado. Suele ser una ruta o un conjunto de páginas específicos. Utilice importaciones dinámicas para cargar de forma diferida dicho código.
Dividir el paquete da como resultado que se realicen más solicitudes para cargar completamente su aplicación. Pero, si las solicitudes se paralelizan, este problema no es grande, especialmente en sitios que usan HTTP / 2. Observe las tres primeras consultas en esta cascada: sin
embargo, esta cascada también muestra 2 consultas ejecutadas secuencialmente. Estos fragmentos son necesarios solo para esta página y se cargan dinámicamente mediante una llamada
import().
Puede solucionar este problema insertando una etiqueta
preload linksi sabe que definitivamente necesitará estos fragmentos.
Sin embargo, como puede ver, la ganancia de velocidad en este caso puede ser pequeña en comparación con el tiempo total de carga de la página.
Además, el uso de la precarga a veces puede ser contraproducente y causar retrasos cuando se cargan otros archivos más importantes. Consulte la publicación de Andy Davis sobre la precarga de fuentes y cómo bloquear la representación primaria cargando primero las fuentes y luego el CSS que evita la representación.
Cargando datos de la página
Probablemente, su aplicación esté diseñada para mostrar algún tipo de datos. A continuación, se ofrecen algunos consejos sobre cómo cargar datos con anticipación y evitar retrasos en el procesamiento.
No espere los paquetes, comience a cargar datos de inmediato.
Puede haber un caso especial de encadenamiento de solicitudes secuenciales: carga un paquete de aplicaciones y este código ya solicita datos de la página.
Hay dos formas de evitar esto:
- Insertar datos de la página en un documento HTML
- Comience a solicitar datos a través de un script en línea dentro del documento
Incrustar datos en HTML garantiza que su aplicación no tenga que esperar a que se cargue. También reduce la complejidad general de la aplicación al no tener que manejar el estado de carga.
Sin embargo, esta idea no es tan buena si la obtención de datos da como resultado un retraso significativo en la respuesta de su documento, ya que también ralentizará el procesamiento inicial.
En este caso, o al entregar un documento HTML almacenado en caché con un trabajador del servicio, puede incrustar un script en línea en el HTML que cargará estos datos. Se puede proporcionar como una promesa global, como esta:
window.userDataPromise = fetch("/me")
Luego, si los datos ya están listos, su aplicación puede comenzar a renderizar inmediatamente o esperar hasta que esté lista.
Al usar ambos métodos, necesita saber exactamente qué datos deben mostrarse en la página, e incluso antes de que la aplicación comience a renderizarse. Esto suele ser fácil de proporcionar para datos específicos del usuario (nombre, notificaciones ...) pero no es fácil cuando se trata de contenido específico de una página. Intente resaltar las páginas más importantes usted mismo y escriba su propia lógica para cada una de ellas.
No bloquee el renderizado mientras espera datos irrelevantes
A veces, la generación de datos paginados requiere una lógica lenta y compleja implementada en el backend. En tales casos, la capacidad de cargar primero una versión simplificada de los datos es útil, si eso es suficiente para que su aplicación sea funcional e interactiva.
Por ejemplo, una herramienta analítica puede cargar primero todos los gráficos y luego acompañarlos con datos. Por lo tanto, el usuario podrá ver inmediatamente el diagrama que le interesa y usted tendrá tiempo para distribuir las solicitudes de backend en diferentes servidores.
Evite las cadenas de consultas de datos secuenciales
Este consejo puede parecer contradecir mi punto anterior en el que hablé de aplazar la carga de datos irrelevantes a una segunda solicitud. Sin embargo, evite encadenar solicitudes consecutivas si una solicitud posterior en la cadena no proporciona al usuario ninguna información nueva.
En lugar de preguntar primero a qué usuario ha iniciado sesión y luego pedir una lista de grupos a los que pertenece el usuario, devuelva la lista de grupos junto con la información sobre el usuario. Puede usar GraphQL para esto , pero un punto final personalizado también está
user?includeTeams=truebien.
Representación del lado del servidor
En este caso, nos referimos a la representación anticipada de la aplicación en el servidor, de modo que una página HTML completa se sirve como respuesta a una solicitud de un documento. Por lo tanto, el cliente puede ver la página completa sin esperar a que se carguen códigos o datos adicionales.
Dado que el servidor envía solo HTML estático al cliente, su aplicación aún carece de interactividad en esta etapa. La aplicación debe cargarse, debe volver a ejecutar la lógica de renderizado y luego adjuntar los oyentes de eventos requeridos al DOM.
Utilice la representación del lado del servidor cuando descubra que el contenido no interactivo es valioso por sí solo. Además, este enfoque ayuda a almacenar en caché el HTML que se mostró allí en el servidor y luego transferirlo a todos los usuarios sin demora cuando se solicita el documento por primera vez. Por ejemplo, la renderización del lado del servidor es genial si está renderizando un blog usando React.
Lea este artículo de Michal Janaszek; describe bien cómo combinar los trabajadores del servicio con la representación del lado del servidor.
Siguiente página
En algún momento, el usuario que trabaja con su aplicación deberá ir a la página siguiente. Cuando se abre la primera página, usted tiene el control de todo lo que sucede en el navegador, por lo que puede prepararse para la próxima interacción.
Obtención
previa de recursos La obtención previa del código necesario para mostrar la página siguiente puede ayudar a evitar retrasos en la navegación personalizada. Utilice etiquetas
prefetch linko webpackPrefetchpara importaciones dinámicas:
import(
/* webpackPrefetch: true, webpackChunkName: "todo-list" */ "./TodoList"
)
Considere cuántos datos de usuario está utilizando y cuál es el ancho de banda, especialmente cuando se trata de conexión móvil. Es en la versión móvil del sitio que no puede ser celoso con la precarga, y también si el modo de ahorro de datos está activado.
Seleccione estratégicamente los datos que más necesitan sus usuarios.
Reutilice los datos que ya están cargados en
caché localmente los datos Ajax en su aplicación para evitar solicitudes innecesarias más adelante. Si el usuario navega a la lista de grupos en la página Editar grupo, la transición se puede hacer instantáneamente reutilizando los datos ya seleccionados anteriormente.
Tenga en cuenta que esto no funcionará si otros usuarios editan con frecuencia su objeto y los datos que ha cargado pueden quedar obsoletos rápidamente. En tales casos, intente mostrar primero los datos existentes en modo de solo lectura y, mientras tanto, seleccione los datos actualizados.
Conclusión
En este artículo, analizamos una serie de factores que pueden ralentizar una página en varios puntos del proceso de carga. Utilice herramientas como Chrome DevTools , WebPageTest y Lighthouse para determinar qué consejos son relevantes para su aplicación.
En la práctica, rara vez es posible llevar a cabo una optimización completa. Determina qué es más importante para tus usuarios y céntrate en eso.
Mientras trabajaba en este artículo, me di cuenta de que comparto una creencia profundamente arraigada de que las consultas múltiples son problemas de mal rendimiento. Esto era cierto en el pasado, cuando cada solicitud requería una conexión separada y los navegadores solo permitían algunas conexiones por dominio. Pero este problema desapareció con la llegada de HTTP / 2 y los navegadores modernos.
Existen sólidos argumentos para dividir las consultas. Al hacer esto, puede cargar los recursos estrictamente necesarios y hacer un mejor uso del contenido en caché, ya que solo necesita volver a cargar los archivos que han cambiado.