El autor del material, cuya traducción estamos publicando, es uno de los fundadores del proyecto
Webiny : un CMS sin servidor basado en React, GraphQL y Node.js. Él dice que admitir una plataforma en la nube sin servidor de múltiples inquilinos es un negocio que tiene tareas específicas. Ya se han escrito muchos artículos en los que se discuten las tecnologías estándar para optimizar proyectos web. Entre ellos están la representación del servidor, el uso de tecnologías avanzadas de desarrollo de aplicaciones web, varias formas de mejorar las compilaciones de aplicaciones y mucho más. Este artículo, por un lado, es similar a los demás y, por otro, difiere de ellos. El hecho es que está dedicado a optimizar proyectos que se ejecutan en un entorno sin servidor.

Preparación
Para realizar mediciones que ayuden a identificar los problemas del proyecto, utilizaremos
webpagetest.org . Con la ayuda de este recurso, cumpliremos con las solicitudes y recopilaremos información sobre el tiempo de ejecución de varias operaciones. Esto nos permitirá comprender mejor lo que los usuarios ven y sienten al trabajar con el proyecto.
Estamos particularmente interesados en el indicador "Primera vista", es decir, cuánto tiempo se tarda en cargar un sitio de un usuario que lo visita por primera vez. Este es un indicador muy importante. El hecho es que el caché del navegador puede ocultar muchos cuellos de botella de proyectos web.
Indicadores que reflejan las características de la carga del sitio: identificación de problemas
Echa un vistazo a la siguiente tabla.
Análisis de indicadores antiguos y nuevos de un proyecto web.Aquí, el indicador más importante se puede reconocer como "Tiempo de inicio de renderizado", el tiempo antes del inicio del renderizado. Si observa detenidamente este indicador, puede ver que solo para comenzar a renderizar la página, en la versión anterior del proyecto, tardó casi 2 segundos. La razón de esto radica en la esencia misma de la Aplicación de Página Única (SPA). Para mostrar la página de dicha aplicación en la pantalla, primero debe cargar el voluminoso paquete JS (esta etapa de carga de la página está marcada en la siguiente figura como 1). Entonces este paquete necesita ser procesado en el hilo principal (2). Y solo después de eso, algo puede aparecer en la ventana del navegador.
(1) Descargue el paquete JS. (2) Esperando el procesamiento del paquete en el hilo principalSin embargo, esto es solo una parte de la imagen. Después de que el hilo principal procesa el paquete JS, realiza varias solicitudes a la API de Gateway. En esta etapa del procesamiento de la página, el usuario ve un indicador de carga rotativo. La vista no es la más agradable. Sin embargo, el usuario aún no ha visto ningún contenido de la página. Aquí hay un guión gráfico del proceso de carga de la página.
Carga de la páginaTodo esto sugiere que el usuario que visitó dicho sitio no experimenta sensaciones particularmente agradables al trabajar con él. Es decir, se ve obligado a mirar una página en blanco durante 2 segundos, y luego otro segundo, en el indicador de descarga. Este segundo se agrega al tiempo de preparación de la página debido al hecho de que después de cargar y procesar las solicitudes de API del paquete JS se ejecutan. Estas consultas son necesarias para cargar los datos y, como resultado, mostrar la página terminada.
Carga de la páginaSi el proyecto fue alojado en un VPS regular, entonces el tiempo requerido para completar estas solicitudes de API sería mayormente predecible. Sin embargo, los proyectos que se ejecutan en un entorno sin servidor se ven afectados por el notorio problema de "arranque en frío". En el caso de la plataforma en la nube Webiny, la situación es aún peor. Las características de AWS Lambda son parte de VPC (Virtual Private Cloud). Esto significa que para cada nueva instancia de dicha función, debe inicializar ENI (Elastic Network Interface, Elastic Network Interface). Esto aumenta significativamente el tiempo de arranque en frío de las funciones.
Aquí hay algunos plazos para cargar las características de AWS Lambda dentro de VPC y fuera de VPC.
Análisis de carga de la función AWS Lambda dentro de VPC y fuera de VPC (imagen tomada de aquí )De esto podemos concluir que en el caso de que la función se inicie dentro de la VPC, esto aumenta 10 veces el tiempo de arranque en frío.
Además, aquí debe tenerse en cuenta un factor más: los retrasos en la transmisión de datos de la red. Su duración ya está incluida en el tiempo que lleva ejecutar las solicitudes de API. Las solicitudes son iniciadas por el navegador. Por lo tanto, resulta que cuando la API responde a estas solicitudes, se agrega el tiempo necesario para llevar la solicitud del navegador a la API y el tiempo que tarda la respuesta en llegar desde la API al navegador. Estas demoras ocurren durante cada solicitud.
Tareas de optimización
Según el análisis anterior, formulamos varias tareas que necesitábamos resolver para optimizar el proyecto. Aquí están:
- Mejora la velocidad de ejecución de solicitudes de API o reduce la cantidad de solicitudes de API que bloquean la representación.
- Reducir el tamaño del paquete JS o convertir este paquete en recursos que no son necesarios para la salida de la página.
- Desbloqueo del hilo principal.
Enfoques de problemas
Aquí hay algunos enfoques para resolver los problemas que consideramos:
- Optimización de código con vistas a acelerar su ejecución. Este enfoque requiere mucho esfuerzo, tiene un alto costo. Los beneficios que se pueden obtener como resultado de dicha optimización son dudosos.
- Aumente la cantidad de RAM disponible para las funciones de AWS Lambda. Es fácil de hacer, el costo de tal solución está en algún lugar entre medio y alto. Solo se pueden esperar pequeños efectos positivos de la aplicación de esta solución.
- El uso de alguna otra forma de resolver el problema. Es cierto que en ese momento aún no sabíamos qué era este método.
Al final, elegimos el tercer elemento de esta lista. Razonamos así: “¿Qué pasa si no necesitamos absolutamente ninguna llamada a la API? ¿Qué pasa si podemos prescindir del paquete JS? Esto nos permitiría resolver todos los problemas del proyecto ".
La primera idea que nos pareció interesante fue crear una instantánea HTML de la página renderizada y compartirla con los usuarios.
Intento fallido
Webiny Cloud es una infraestructura sin servidor basada en AWS Lambda que admite sitios Webiny. Nuestro sistema puede detectar bots. Cuando resulta que el bot completó la solicitud, esta solicitud se redirige a la instancia de
Puppeteer , que muestra la página usando Chrome sin una interfaz de usuario. El código HTML listo de la página se envía al bot. Esto se hizo principalmente por razones de SEO, debido al hecho de que muchos bots no saben cómo ejecutar JavaScript. Decidimos utilizar el mismo enfoque para preparar páginas destinadas a usuarios comunes.
Este enfoque funciona bien en entornos que carecen de soporte JavaScript. Sin embargo, si intenta dar páginas preprocesadas a un cliente cuyo navegador admite JS, se muestra la página, pero luego, después de descargar los archivos JS, los componentes React simplemente no saben dónde montarlos. Esto da como resultado una gran cantidad de mensajes de error en la consola. Como resultado, tal decisión no nos convenía.
Introduciendo SSR
El punto fuerte de la representación del lado del servidor (SSR) es que todas las solicitudes de API se ejecutan dentro de la red local. Dado que son procesados por un determinado sistema o función que se ejecuta dentro de la VPC, los retrasos que se producen al ejecutar solicitudes desde el navegador al backend de recursos no son característicos. Aunque en este escenario, el problema de un "arranque en frío" persiste.
Una ventaja adicional de usar SSR es que le damos al cliente una versión HTML de la página, cuando trabaja con la cual, después de cargar los archivos JS, los componentes React no tienen problemas de montaje.
Y finalmente, no necesitamos un paquete JS muy grande. Además, podemos prescindir de las llamadas a la API para mostrar la página. Un paquete se puede cargar de forma asincrónica y esto no bloqueará el hilo principal.
En general, podemos decir que la representación del servidor, al parecer, debería haber resuelto la mayoría de nuestros problemas.
Así es como se ve el análisis del sitio después de aplicar la representación del lado del servidor.
Métricas del sitio después de aplicar la representación del servidorAhora las solicitudes de API no se ejecutan y la página se puede ver antes de que se cargue el paquete JS grande. Pero si observa de cerca la primera solicitud, puede ver que toma casi 2 segundos obtener un documento del servidor. Hablemos de eso.
Problema con TTFB
Aquí discutimos la métrica TTFB (Tiempo hasta el primer byte, tiempo hasta el primer byte). Aquí están los detalles de la primera solicitud.
Detalles de la primera solicitudPara procesar esta primera solicitud, debemos hacer lo siguiente: iniciar el servidor Node.js, realizar el procesamiento del servidor, realizar solicitudes de API y ejecutar código JS, y luego devolver el resultado final al cliente. El problema aquí es que todo esto, en promedio, toma 1-2 segundos.
Nuestro servidor, que realiza la representación del servidor, necesita hacer todo este trabajo, y solo después de eso podrá transmitir el primer byte de la respuesta al cliente. Esto lleva al hecho de que el navegador tiene mucho tiempo para esperar el inicio de la respuesta a la solicitud. Como resultado, resulta que ahora para la salida de la página necesita producir casi la misma cantidad de trabajo que antes. La única diferencia es que este trabajo se lleva a cabo no en el lado del cliente, sino en el servidor, en el proceso de representación del servidor.
Aquí puede tener una pregunta sobre la palabra "servidor". Hemos estado hablando sobre el sistema sin servidor todo este tiempo. ¿De dónde vino este "servidor"? Nosotros, por supuesto, intentamos renderizar la representación del servidor en las funciones de AWS Lambda. Pero resultó que este es un proceso que consume muchos recursos (en particular, era necesario aumentar mucho la cantidad de memoria para obtener más recursos del procesador). Además, el problema de "arranque en frío", que ya hemos mencionado, también se agrega aquí. Como resultado, la solución ideal era utilizar un servidor Node.js que cargara los materiales del sitio y los representara en el lado del servidor.
Volvamos a las consecuencias de usar la representación del lado del servidor. Echa un vistazo al siguiente guión gráfico. Es fácil ver que no es particularmente diferente de lo que se obtuvo en el estudio del proyecto, que se realizó en el cliente.
Carga de página cuando se usa la representación del lado del servidorEl usuario se ve obligado a mirar una página en blanco durante 2,5 segundos. Esto es triste
Aunque mirando estos resultados, uno podría pensar que no hemos logrado absolutamente nada, en realidad esto no es así. Teníamos una instantánea HTML de la página que contenía todo lo que necesitábamos. Esta foto estaba lista para funcionar con React. Al mismo tiempo, durante el procesamiento de la página en el cliente, no era necesario realizar ninguna solicitud de API. Todos los datos necesarios ya se han incrustado en HTML.
El único problema fue que crear esta instantánea HTML tomó demasiado tiempo. En este punto, podríamos invertir más tiempo en la optimización del procesamiento del servidor, o simplemente almacenar en caché sus resultados y dar a los clientes una instantánea de la página desde algo así como un caché Redis. Nosotros acabamos de hacer eso.
Caché de resultados de representación del servidor
Después de que un usuario visita el sitio web de Webiny, primero verificamos el caché centralizado de Redis para ver si hay una instantánea HTML de la página. Si es así, le damos al usuario una página del caché. En promedio, esto redujo el TTFB a 200-400 ms. Fue después de la introducción del caché que comenzamos a notar mejoras significativas en el rendimiento del proyecto.
Carga de página cuando se usa la representación del lado del servidor y la memoria cachéIncluso el usuario que visita el sitio por primera vez ve el contenido de la página en menos de un segundo.
Veamos cómo se ve ahora el diagrama de cascada.
Métricas del sitio después de aplicar el procesamiento y el almacenamiento en caché del lado del servidorLa línea roja indica una marca de tiempo de 800 ms. Aquí es donde el contenido de la página está completamente cargado. Además, aquí puede ver que los paquetes JS se cargan a aproximadamente 1.3 s. Pero esto no afecta el tiempo que el usuario necesita para ver la página. Al mismo tiempo, no necesita hacer llamadas a la API y cargar el hilo principal para mostrar la página.
Preste atención al hecho de que el tiempo relacionado con la carga del paquete JS, la ejecución de solicitudes de API y la realización de operaciones en el hilo principal todavía juegan un papel importante en la preparación de la página para el trabajo. Esta inversión de tiempo y recursos es necesaria para que la página se vuelva "interactiva". Pero esto no juega ningún papel, en primer lugar, para los robots de los motores de búsqueda, y en segundo lugar, para crear la sensación de "carga rápida de páginas" entre los usuarios.
Supongamos que una página es "dinámica". Por ejemplo, muestra un enlace en el encabezado para acceder a la cuenta de usuario en caso de que el usuario que está viendo la página haya iniciado sesión. Después de la representación del lado del servidor, la página de uso general se enviará al navegador. Es decir, uno que se muestra a los usuarios que no han iniciado sesión. El título de esta página cambiará, reflejando el hecho de que el usuario inició sesión, solo después de cargar el paquete JS y realizar las llamadas a la API. Aquí estamos tratando con el indicador
TTI (Time To Interactive, tiempo hasta la primera interactividad).
Después de algunas semanas, descubrimos que nuestro servidor proxy no cierra la conexión con el cliente donde es necesario, si la representación del servidor se inició como un proceso en segundo plano. La corrección de literalmente una línea de código condujo al hecho de que el indicador TTFB se redujo al nivel de 50-90 ms. Como resultado, el sitio ahora comenzó a mostrarse en el navegador después de unos 600 ms.
Sin embargo, enfrentamos otro problema ...
Problema de invalidación de caché
"En informática, solo hay dos cosas complejas: invalidación de caché y denominación de entidades".
Phil CarletonLa invalidación de caché es, de hecho, una tarea muy difícil. ¿Cómo solucionarlo? En primer lugar, a menudo puede actualizar la memoria caché configurando un tiempo de almacenamiento muy corto para los objetos en caché (TTL, Tiempo de vida, duración). Esto a veces hará que las páginas se carguen más lentamente de lo habitual. En segundo lugar, puede crear un mecanismo de invalidación de caché basado en ciertos eventos.
En nuestro caso, este problema se resolvió utilizando un TTL muy pequeño de 30 segundos. Pero también nos dimos cuenta de la posibilidad de proporcionar a los clientes datos obsoletos del caché. En un momento en que los clientes reciben dichos datos, la memoria caché se actualiza en segundo plano. Gracias a esto, nos deshicimos de problemas, como retrasos y "arranque en frío", que son típicos de las funciones de AWS Lambda.
Así es como funciona. Un usuario visita el sitio web de Webiny. Estamos revisando el caché HTML. Si hay una captura de pantalla de la página, se la damos al usuario. La edad de una imagen puede ser incluso unos pocos días. Al pasar esta antigua instantánea al usuario en unos pocos cientos de milisegundos, simultáneamente iniciamos la tarea de crear una nueva instantánea y actualizar el caché. Por lo general, lleva unos segundos completar esta tarea, ya que creamos un mecanismo gracias al cual siempre tenemos un cierto número de funciones de AWS Lambda que ya se están ejecutando y listas para funcionar. Por lo tanto, no tenemos que, durante la creación de nuevas imágenes, dedicar tiempo al arranque en frío de las funciones.
Como resultado, siempre devolvemos las páginas del caché a los clientes, y cuando la antigüedad de los datos almacenados en caché alcanza los 30 segundos, el contenido del caché se actualiza.
El almacenamiento en caché es definitivamente un área en la que aún podemos mejorar algo. Por ejemplo, estamos considerando la posibilidad de actualizar automáticamente el caché cuando el usuario publica una página. Sin embargo, dicho mecanismo de actualización de caché tampoco es ideal.
Por ejemplo, suponga que la página de inicio de un recurso muestra las tres publicaciones de blog más recientes. Si el caché se actualiza cuando se publica una nueva página, desde un punto de vista técnico, solo se generará el caché para esta nueva página después de la publicación. El caché para la página de inicio estará desactualizado.
Todavía estamos buscando formas de mejorar el sistema de almacenamiento en caché de nuestro proyecto. Pero hasta ahora, la atención se ha centrado en resolver los problemas de rendimiento existentes. Creemos que hemos hecho un buen trabajo en términos de resolución de estos problemas.
Resumen
Al principio, utilizamos la representación del lado del cliente. Luego, en promedio, el usuario podía ver la página en 3,3 segundos. Ahora, esta cifra se ha reducido a unos 600 ms. También es importante que ahora prescindamos del indicador de descarga.
Para lograr este resultado, se nos permitió, principalmente, el uso de la representación del servidor. Pero sin un buen sistema de almacenamiento en caché, resulta que los cálculos simplemente se transfieren del cliente al servidor. Y esto lleva al hecho de que el tiempo requerido para que el usuario vea la página no cambia mucho.
El uso de la representación del servidor tiene otra calidad positiva, no mencionada anteriormente. Estamos hablando del hecho de que facilita la visualización de páginas en dispositivos móviles débiles. La velocidad de preparación de una página para ver en dichos dispositivos depende de las modestas capacidades de sus procesadores. La representación del servidor le permite eliminar parte de la carga de ellos. Cabe señalar que no realizamos un estudio especial sobre este tema, pero el sistema que tenemos debería ayudar a mejorar la visualización del sitio en teléfonos y tabletas.
En general, podemos decir que la implementación de la representación del servidor no es una tarea fácil. Y el hecho de que usemos un entorno sin servidor solo complica esta tarea. La solución a nuestros problemas requería cambios de código, infraestructura adicional. Necesitábamos crear un mecanismo de almacenamiento en caché bien diseñado. Pero a cambio, obtuvimos mucho bien. Lo más importante es que las páginas de nuestro sitio ahora se están cargando y preparando para trabajar mucho más rápido que antes. Creemos que a nuestros usuarios les gustará.
Estimados lectores! ¿Utiliza tecnologías de almacenamiento en caché y representación de servidores para optimizar sus proyectos?
