Hola a todos! En el transcurso del año, cambiamos a Reaccionar y pensamos en cómo asegurarnos de que nuestros usuarios no esperaran la plantilla del cliente, sino que vieran la página lo más rápido posible. Para este propósito, decidimos hacer la representación del lado del servidor (SSR - Representación del lado del servidor) y optimizar el SEO, porque no todos los motores de búsqueda pueden ejecutar JS, y aquellos que pueden pasar tiempo en la ejecución, y el tiempo de rastreo de cada sitio es limitado.

Permítame recordarle que la representación del servidor es la ejecución de código JavaScript en el lado del servidor para darle al cliente HTML listo. Esto afecta el rendimiento percibido por el usuario, especialmente en máquinas más lentas y en Internet lento. No es necesario esperar hasta que JS se descargue, analice y ejecute. El navegador solo puede procesar HTML inmediatamente, sin esperar a JSa, el usuario ya puede leer el contenido.
Por lo tanto, la fase de espera pasiva se reduce. Después de renderizar, el navegador solo tendrá que pasar por el DOM terminado, verifique que coincida con lo que se procesó
en el cliente y agregue oyentes de eventos. Este proceso se llama hidratación . Si en el proceso de hidratación existe una discrepancia entre el contenido del servidor y el generado por el navegador, recibiremos una advertencia en la consola y un procesador adicional en el cliente. Esto no debería ser, es necesario asegurarse de que los resultados del servidor y la representación del cliente coincidan. Si divergen, entonces esto debería tratarse como un error, ya que esto niega las ventajas de la representación del servidor. Si algún elemento fuera diferente, agregue suppressHydrationWarning={true}
.
Además, hay una advertencia: no hay window
en el servidor. El código que accede debe ejecutarse en métodos de ciclo de vida que no se invocan en el lado del servidor. Es decir, no puede usar window
en UNSAFE_componentWillMount () o, en el caso de ganchos, uselayouteffect .
De hecho, el proceso de representación del lado del servidor se reduce a obtener el estado inicial del backend, ejecutarlo a través de renderToString()
, recoger el estado inicial y HTML finalizados en la salida y enviarlo al cliente.
En hh.ru, las caminatas desde el cliente JS solo se permiten en la puerta de enlace api en python. Esto es para seguridad y equilibrio de carga. Python ya va a los backends necesarios para los datos, los prepara y los entrega al navegador. Node.js se usa solo para la representación del servidor. En consecuencia, después de preparar los datos, la pitón necesita un viaje adicional al nodo, esperando el resultado y transmitiendo la respuesta al cliente.
Primero tenía que elegir un servidor para trabajar con HTTP. Nos detuvimos en Koa . Me gustó la sintaxis moderna con await
. La modularidad es un middleware ligero, que, si es necesario, se instala por separado o se escribe fácilmente de forma independiente. El servidor en sí es ligero y rápido . Sí, y escrito por koa por el mismo equipo de desarrollo que escriben express, su experiencia cautiva.
Después de que aprendimos cómo implementar nuestro servicio, escribimos el código más simple en KOA, que podía dar 200, y lo subimos al producto. Se veía así:
const Koa = require('koa'); const app = new Koa(); const SERVER_PORT = 9400; app.use(async (ctx) => { ctx.body = 'Hello World'; }); app.listen(SERVER_PORT);
En hh.ru, todos los servicios viven en contenedores acoplables . Antes de la primera versión, debe escribir libros de jugadas ansibles , con la ayuda de los cuales el servicio se implementa en entornos de producción y en bancos de pruebas. Cada desarrollador y probador tiene su propio entorno de prueba, que es más similar a la producción. Pasamos la mayor parte de nuestro tiempo y energía escribiendo libros de jugadas. Sucedió porque dos renders front-end hicieron esto, y este es el primer servicio en un nodo en hh.ru. Tuvimos que descubrir cómo cambiar el servicio al modo de desarrollo, hacerlo en paralelo con el servicio para el que se está realizando el renderizado. Entregar archivos a un contenedor. Inicie un servidor desnudo para que el contenedor acoplable se inicie sin esperar la compilación. Compile y reconstruya el servidor junto con el servicio que lo utiliza. Determine cuánta RAM necesitamos.
En el modo de desarrollo, proporcionaron la posibilidad de reconstrucción automática y posterior reinicio del servicio al cambiar los archivos incluidos en la compilación final. El nodo debe reiniciarse para cargar el código ejecutable. Webpack monitorea cambios y compilaciones . Se necesita Webpack para convertir ESM a CommonJS común. Para reiniciar, tomaron nodemon , que se ocupa de los archivos recopilados.
Luego aprendimos el servidor de enrutamiento. Para un equilibrio adecuado, necesita saber qué instancias del servidor están activas. Para verificar esto, el latido cardíaco operacional pasa al /status
una vez cada pocos segundos y espera recibir 200 en respuesta. Si el servidor no responde más de la cantidad de veces especificada en la configuración, se elimina del equilibrio. Esto resultó ser una tarea simple, un par de líneas y enrutamiento listo:
export default async function(ctx, next) { if (routeMap[ctx.request.path]) { routeMap[ctx.request.path](ctx); } else { ctx.throw(NOT_FOUND, getStatusText(NOT_FOUND)); } next(); }
Y respondemos 200 en la url correcta:
export default (ctx) => { ctx.status = 200; ctx.body = '200'; };
Después de eso, creamos un servidor primitivo que devolvía el estado en <script>
y HTML listo.
Era necesario controlar cómo funciona el servidor. Para hacer esto, debe ajustar el registro y la supervisión. Los registros no están escritos en JSON, pero para admitir el formato de registro de nuestros otros servicios, principalmente Java. Log4js fue elegido de acuerdo con los puntos de referencia : es rápido, fácil de configurar y escribe en el formato que necesitamos. Se necesita un formato de registro común para simplificar el soporte de monitoreo; no es necesario escribir regulares adicionales para analizar registros. Además de los registros, todavía escribimos errores en centinela . No daré el código de los registradores, es muy simple, básicamente, hay configuraciones.
Luego fue necesario prever un apagado correcto: cuando el servidor se enferma o cuando se lanza la versión, el servidor no acepta más conexiones entrantes, sino que realiza todas las solicitudes que se cuelgan de él. Hay muchas soluciones preparadas para un nodo. Tomaron http-graceful-shutdown , todo lo que tenía que hacer era finalizar la llamada gracefulShutdown(app.listen(SERVER_PORT))
En este punto, obtuvimos una solución lista para la producción. Para comprobar cómo funciona, activaron la representación del servidor para el 5% de los usuarios en una página. Observamos las métricas, nos dimos cuenta de que mejoraron significativamente la FMP para teléfonos móviles, para computadoras de escritorio, el valor no ha cambiado. Comenzaron a probar el rendimiento, descubrieron que un servidor tiene ~ 20 RPS (este hecho fue muy divertido para los Javists). Descubrí las razones por las que esto es así:
Uno de los principales problemas resultó ser que se construyeron sin NODE_ENV = producción (configuramos el ENV que necesitábamos para la compilación del servidor). En este caso, la reacción da el ensamblaje que no es de producción, que funciona aproximadamente un 30% más lento.
Elevamos la versión del nodo de 8 a 10, obtuvimos otro 20-25% del rendimiento.
Lo que dejamos por última vez es lanzar un nodo en varios núcleos. Sospechábamos que era muy difícil, pero aquí también, todo resultó ser muy prosaico. El nodo tiene un mecanismo incorporado: clúster . Le permite ejecutar el número requerido de procesos independientes, incluido un proceso maestro que dispersa las tareas por ellos.
if (cluster.isMaster) { cluster.on('exit', (worker, exitCode) => { if (exitCode !== SUCCESS) { cluster.fork(); } }); for (let i = 0; i < serverConfig.cpuCores; i++) { cluster.fork(); } } else { runApp(); }
En este código, comienza el proceso maestro, los procesos comienzan de acuerdo con el número de CPU asignadas para el servidor. Si uno de los procesos secundarios falla: el código de salida no es 0
(nosotros mismos apagamos el servidor), el proceso maestro lo reinicia.
Y el rendimiento aumenta en aproximadamente el número de CPU asignadas para el servidor.
Como escribí anteriormente, la mayoría del tiempo dedicado a escribir los libros de jugadas originales, aproximadamente 3 semanas. Tomó alrededor de 2 semanas escribir el SSR completo, y durante aproximadamente un mes lentamente lo recordamos. Todo esto fue hecho por fuerzas de 2 frentes, sin experiencia empresarial del nodo js. No tenga miedo de hacer SSR, lo más importante: no olvide especificar NODE_ENV=production
, no hay nada complicado al respecto. Los usuarios y el SEO te lo agradecerán.