Olá pessoal! Ao longo do ano, mudamos para o React e pensamos em como garantir que nossos usuários não esperassem a modelização do cliente, mas visualizassem a página o mais rápido possível. Para isso, decidimos fazer a renderização no lado do servidor (SSR - Server Side Rendering) e otimizar o SEO, porque nem todos os mecanismos de pesquisa são capazes de executar o JS, e os que conseguem gastar tempo na execução, e o tempo de rastreamento de cada site é limitado.

Deixe-me lembrá-lo de que a renderização do servidor é a execução do código JavaScript no lado do servidor para fornecer HTML pronto ao cliente. Isso afeta o desempenho percebido pelo usuário, especialmente em máquinas mais lentas e na Internet lenta. Não há necessidade de esperar até que o JS seja baixado, analisado e executado. O navegador pode renderizar HTML imediatamente, sem esperar pelo JSa, o usuário já pode ler o conteúdo.
Assim, a fase de espera passiva é reduzida. Após a renderização, o navegador só precisará passar pelo DOM finalizado, verifique se ele corresponde ao que foi renderizado
no cliente e adicione ouvintes de eventos. Esse processo é chamado de hidratação . Se no processo de hidratação houver uma discrepância entre o conteúdo do servidor e o gerado pelo navegador, receberemos um aviso no console e um renderizador extra no cliente. Isso não deve ser, é necessário garantir que os resultados da renderização do servidor e do cliente correspondam. Se eles divergirem, isso deve ser tratado como um bug, pois isso nega as vantagens da renderização do servidor. Se algum elemento divergir, adicione suppressHydrationWarning={true}
.
Além disso, há uma ressalva: não há window
no servidor. O código que o acessa deve ser executado nos métodos do ciclo de vida que não são chamados no lado do servidor. Ou seja, você não pode usar a window
em UNSAFE_componentWillMount () ou, no caso de ganchos, uselayouteffect .
Em essência, o processo de renderização do lado do servidor se resume a obter o initialState do back-end, executando-o através de renderToString()
, escolhendo o initialState e o HTML finalizados na saída e enviando-o para o cliente.
No hh.ru, as caminhadas do cliente JS são permitidas apenas no gateway da API em python. Isto é para segurança e balanceamento de carga. O Python já acessa os dados necessários para os dados, os prepara e os fornece ao navegador. O Node.js é usado apenas para renderização do servidor. Assim, depois de preparar os dados, o python precisa de uma viagem adicional ao nó, aguardando o resultado e transmitindo a resposta ao cliente.
Primeiro você tinha que escolher um servidor para trabalhar com HTTP. Paramos em koa . Gostei da sintaxe moderna com await
. A modularidade é um middleware leve, que, se necessário, é instalado separadamente ou é fácil de escrever de forma independente. O servidor em si é leve e rápido . Sim, e escritas por koa pela mesma equipe de desenvolvimento que eles escrevem express, sua experiência cativa.
Depois que aprendemos a implementar nosso serviço, escrevemos o código mais simples no KOA, que era capaz de fornecer 200, e o carregamos no prod. Parecia assim:
const Koa = require('koa'); const app = new Koa(); const SERVER_PORT = 9400; app.use(async (ctx) => { ctx.body = 'Hello World'; }); app.listen(SERVER_PORT);
No hh.ru, todos os serviços ficam em contêineres de docker . Antes do primeiro lançamento, você precisa escrever playbooks ansíveis , com a ajuda dos quais o serviço é implementado em ambientes de produção e em bancadas de teste. Cada desenvolvedor e testador tem seu próprio ambiente de teste, que é mais semelhante ao prod. Passamos a maior parte do tempo e energia escrevendo manuais. Isso aconteceu porque duas renderizações de front-end fizeram isso, e este é o primeiro serviço em um nó no hh.ru. Tivemos que descobrir como alternar o serviço para o modo de desenvolvimento, fazê-lo em paralelo com o serviço para o qual a renderização está ocorrendo. Entregue arquivos para um contêiner. Inicie um servidor bare para que o contêiner do docker seja iniciado sem aguardar a construção. Crie e reconstrua o servidor junto com o serviço que o utiliza. Determine quanta RAM precisamos.
No modo de desenvolvimento, eles previam a possibilidade de reconstrução automática e subsequente reinicialização do serviço ao alterar os arquivos incluídos na compilação final. O nó precisa reiniciar para carregar o código executável. O Webpack monitora alterações e compilações . O Webpack é necessário para converter o ESM em CommonJS comum. Para reiniciar, eles usaram o nodemon , que cuida dos arquivos coletados.
Então aprendemos o servidor de roteamento. Para um balanceamento adequado, você precisa saber quais instâncias do servidor estão ativas. Para verificar isso, o batimento cardíaco operacional vai para /status
uma vez a cada poucos segundos e espera receber 200 em resposta. Se o servidor não responder mais do que o número de vezes especificado na configuração, ele será removido do balanceamento. Acabou sendo uma tarefa simples, algumas linhas e roteamento pronto:
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(); }
E nós respondemos 200 no URL correto:
export default (ctx) => { ctx.status = 200; ctx.body = '200'; };
Depois disso, criamos um servidor primitivo que retornava o estado em <script>
e preparava o HTML.
Era necessário controlar como o servidor funciona. Para fazer isso, você precisa acelerar o registro e o monitoramento. Os logs não são gravados em JSON, mas para suportar o formato de log de outros serviços, principalmente Java. O Log4js foi escolhido de acordo com os benchmarks - é rápido, fácil de configurar e grava no formato que precisamos. É necessário um formato de log comum para simplificar o suporte ao monitoramento - não é necessário gravar regulares extras para analisar logs. Além dos logs, ainda escrevemos erros na sentinela . Não vou dar o código dos loggers, é muito simples, basicamente, existem configurações.
Era necessário prever um desligamento normal - quando o servidor fica doente ou quando a liberação rola, o servidor não aceita mais conexões de entrada, mas executa todas as solicitações pendentes. Existem muitas soluções prontas para um nó. Eles usaram o http-graceful-shutdown , tudo o que precisava ser feito era encerrar a chamada gracefulShutdown(app.listen(SERVER_PORT))
Nesse ponto, temos uma solução pronta para produção. Para verificar como funciona, eles ativaram a renderização do servidor para 5% dos usuários em uma página. Analisamos as métricas, percebemos que elas melhoraram significativamente o FMP para telefones celulares, para desktops o valor não mudou. Eles começaram a testar o desempenho, descobriram que um servidor possui ~ 20 RPS (esse fato foi muito divertido para os javistas). Descobriu as razões pelas quais isso é assim:
Um dos principais problemas acabou sendo que eles foram criados sem NODE_ENV = produção (definimos o ENV necessário para a construção do servidor). Nesse caso, a reação fornece a montagem de não produção, que é cerca de 30% mais lenta.
Aumentamos a versão do nó de 8 para 10, obtendo outros 20 a 25% do desempenho.
O que deixamos pela última vez é o lançamento de um nó em vários kernels. Suspeitamos que fosse muito difícil, mas aqui também tudo se mostrou muito prosaico. O nó possui um mecanismo interno - cluster . Ele permite executar o número necessário de processos independentes, incluindo um processo mestre que dispersa tarefas para eles.
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(); }
Nesse código, o processo mestre inicia, os processos iniciam de acordo com o número de CPUs alocadas para o servidor. Se um dos processos filhos travar - o código de saída não é 0
(nós mesmos desligamos o servidor), o processo mestre o reinicia.
E o desempenho aumenta aproximadamente o número de CPUs alocadas para o servidor.
Como escrevi acima, a maior parte do tempo gasto na redação dos manuais originais - cerca de 3 semanas. Demorou cerca de duas semanas para escrever o SSR inteiro e, durante cerca de um mês, lentamente o lembramos. Tudo isso foi feito por forças de 2 frentes, sem experiência empresarial do nó js. Não tenha medo de fazer SSR, o mais importante - não se esqueça de especificar NODE_ENV=production
, não há nada de complicado nisso. Usuários e SEO agradecerão.