Alura > Cursos de Front-end > Cursos de React > Conteúdos de React > Primeiras aulas do curso React: utilizando SSR para otimizar a performance da aplicação

React: utilizando SSR para otimizar a performance da aplicação

Transformando o projeto em SSR - Apresentação

Olá, estudante da Alura! Tudo bem? Eu sou Pedro Mello, instrutor front-end e quero te dar as boas-vindas a mais um curso de React na plataforma da Alura.

Audiodescrição: Pedro se identifica como um homem branco. Tem olhos castanhos, cabelo e barba escuros. Usa piercing no septo e camiseta preta. Ao fundo, ambiente iluminado pela luz azul.

Para este curso, vamos trabalhar em um projeto muito interessante, o projeto da ByteBooks, uma editora de livros. Talvez seja um projeto que já conheçam de algum lugar.

O que vamos aprender?

Para esse projeto, vamos explorar a memorização de componentes e transformar o projeto que, no momento, é estático em um projeto server-side rendering (SSR ou renderização do lado do servidor). Vamos fazer esse processo de transformação manualmente, partindo do zero.

Além disso, entender os pontos de melhoria de desempenho com o nosso grande aliado, o Google Lighthouse.

No momento, ele mostra uma pontuação baixa para a aplicação, principalmente referente ao tempo da primeira e última aparição de conteúdo na tela.

E temos novidades! Nosso projeto agora, além da listagem e do filtro, tem a página de detalhes do livro. Também conseguimos adicionar e remover quantidade, passar para a sacola, finalizar um pedido.

É um projeto que vai agregar muito para você - tanto em projetos pessoais quanto na sua carreira. Todos os conteúdos que vamos abordar, desde memorização até transformação de um projeto em server-side rendering, podem ser utilizados em qualquer projeto que você tenha.

Pré-requisitos

É necessário que você já saiba o funcionamento dos Hooks e saiba mexer no React com TypeScript.

O ideal é que você já tenha um conhecimento prévio sobre o Google Lighthouse, pois vamos usá-lo durante este curso para avaliar métricas para conferir se as melhorias que fizemos realmente surtiram efeito.

Esperamos você no nosso primeiro vídeo!

Transformando o projeto em SSR - Transformando o projeto em SSR

Para esse curso, temos uma proposta diferente para o projeto ByteBooks.

Entendendo o problema

Nosso projeto construído com React e Vite sofreu algumas modificações, o que afetou as notas apresentadas na aba do Google Lighthouse. A performance diminuiu consideravelmente com a inclusão de novas rotas e funcionalidades na aplicação.

Atualmente, temos uma nova área de sacola na aplicação e uma página de detalhes do livro, onde podemos aumentar a quantidade de itens para compra.

Essas novas funcionalidades que fizeram com que, infelizmente, essa performance tivesse uma redução em seu resultado final.

Se pensamos num contexto de mercado de trabalho, isso é muito comum. Podemos estar trabalhando num projeto e outro time pode pegar esse projeto para trabalhar e fazer algumas modificações que não seguem completamente as boas práticas de performance que já aprendemos.

Nesse ponto, é preciso estudar alternativas para melhorar essa performance, não só com os mecanismos que já conhecemos, mas também com outras ferramentas e formas de fazer renderização da aplicação.

Client-Side Rendering (CSR) X Server-Side Rendering (SSR)

Para entender o contexto de como o Vite funciona para renderizar uma aplicação, quando iniciamos um projeto com o Vite e não passamos nenhuma flag sobre uma renderização específica ou alguma configuração extra, ele, por padrão, vai gerar um site que une o melhor do mundo de uma aplicação estática (como o Gatsby ou outros frameworks) e também traz o dinamismo de single-page applications (SPA ou aplicativo de página única).

Se estivéssemos iniciando um projeto com o Vue, React ou Angular no modelo sem ser de renderização pelo lado do servidor, o Vite vai unir o melhor do estático e o melhor do single-page application.

A single-page application, que é o caso do nosso projeto ByteBooks, funciona muito do lado do cliente, ou seja, ela depende 100% da máquina que a pessoa usuária está utilizando.

Portanto, se o computador da pessoa usuária está com algum problema, isso vai impactar na performance final dessa aplicação.

Mas, se utilizamos um mecanismo de renderização pelo lado do servidor, tiramos toda essa responsabilidade de renderização do lado do cliente.

Assim, ao invés de primeiro gerar o bundle, baixar o JavaScript no computador da pessoa usuária e depois renderizadar, passamos a ter um servidor que vai cuidar dessa geração de código.

Desse modo, ele vai gerar um index.html. Ou seja, o que vamos estar vendo não é mais um JavaScript, internamente vai ter o nosso JavaScript funcionando, mas ele nos retorna um HTML com o site funcionando normalmente, podendo utilizar os hooks do React sem nenhum problema.

Ao tirar essa responsabilidade da pessoa usuária e trazer para o servidor, vamos aumentar muito a performance da aplicação.

Vamos deixar o link da documentação do Vite para você, explicando sobre a renderização do lado da pessoa usuária e também explicando um pouco mais sobre como funciona a renderização do lado do servidor.

Transformando o projeto em SSR

Para começar a fazer a alteração do projeto, vamos explorar o código do ByteBooks. Se você fez a configuração inicial do seu projeto, seu código vai estar igual ao nosso nesse exato momento.

Primeiro, precisamos criar um arquivo de um servidor que vai fazer essa transformação do código JavaScript em um index.html. Na raiz do projeto, vamos criar um arquivo chamado server.js.

server.js:

//server.js
import fs from 'node:fs/promises';
import express from 'express';

// Constants
const isProduction = process.env.NODE_ENV === 'production';
const port = process.env.PORT || 5173;
const base = process.env.BASE || '/';

// Cached production assets
const templateHtml = isProduction ? await fs.readFile('./dist/client/index.html', 'utf-8') : '';
const ssrManifest = isProduction
    ? await fs.readFile('./dist/client/ssr-manifest.json', 'utf-8')
    : undefined;

// Create http server
const app = express();

// Add Vite or respective production middlewares
let vite;
if (!isProduction) {
    const { createServer } = await import('vite');
    vite = await createServer({
        server: { middlewareMode: true },
        appType: 'custom',
        base,
    });
    app.use(vite.middlewares);
} else {
    const compression = (await import('compression')).default;
    const sirv = (await import('sirv')).default;
    app.use(compression());
    app.use(base, sirv('./dist/client', { extensions: [] }));
}

// Serve HTML
app.use('*', async (req, res) => {
    try {
        const url = req.originalUrl.replace(base, '');

        let template;
        let render;
        if (!isProduction) {
            // Always read fresh template in development
            template = await fs.readFile('./index.html', 'utf-8');
            template = await vite.transformIndexHtml(url, template);
            render = (await vite.ssrLoadModule('/src/entry-server.tsx')).render;
        } else {
            template = templateHtml;
            render = (await import('./dist/server/entry-server.js')).render;
        }

        const rendered = await render(url, ssrManifest);

        const html = template.replace(`<!--app-html-->`, rendered.html ?? '');

        res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
    } catch (e) {
        vite?.ssrFixStacktrace(e);
        console.log(e.stack);
        res.status(500).end(e.stack);
    }
});

// Start http server
app.listen(port, () => {
    console.log(`Server started at http://localhost:${port}`);
});

Nesse arquivo, vamos colar um código para criar um servidor baseado no Express, no servidor Node. Também instanciamos o Vite, verificamos se é ambiente produtivo e aplicamos os middlewares do Vite para atuar na geração da build para o index.html.

E nessa no bloco do app.use(), entre a linha 37 e 63, é onde toda a mágica está acontecendo.

Dentro do if, o Vite lê o arquivo index.html que está na raiz do nosso projeto, faz a transformação, carrega o módulo de server-side-handling a partir de um arquivo que vamos criar, chamado entry-server.tsx, e depois faz o render fazendo um replace() no index.html com a tag <!--app-html-->, que vamos fazer agora também.

Vamos salvar o arquivo de servidor. Você não precisa se preocupar com os erros do process marcado pelo Lint.

Agora, vamos abrir o index.html, na linha 10, entre a tag de abertura e fechamento da div, vamos adicionar o comentário <!--app-html--> que também está no server.js.

index.html:

<div id="root"><!--app-html--></div>

Nosso script atualmente está carregando através do arquivo main, ou seja, o arquivo que gera o ReactDOM dentro do index.html. Nele, temos o provider do Redux, o gerenciador de rotas. Nesse momento, ele está utilizando esse arquivo como principal.

main.tsx:

ReactDOM.createRoot(document.getElementById('root')!).render(
    <Provider store={store}>
        <Header />
        <Router>
            <Routes />
        </Router>
        <Footer />
    </Provider>
);

Em server.js, reparem que o nosso entry point para a aplicação não vai ser mais o main, vamos ter um para o servidor e vamos ter um para o lado do cliente também. Só que essa configuração vamos fazer junto na nossa próxima aula.

Esperamos você lá para terminar de configurar a nossa aplicação para rodar a renderização do lado do servidor.

Transformando o projeto em SSR - Finalizando a configuração do projeto

Agora vamos finalizar a configuração do projeto para rodar no server-side rendering. Para isso, precisamos instalar algumas dependências no projeto.

Instalando dependências

Nas dependencies do arquivo package.json, adicionamos uma vírgula e colamos as dependências de compression, express e siv, que vão funcionar conjuntamente com nosso arquivo server.js.

package.json:

"dependencies": {
    "@fontsource/poppins": "^5.0.8",
    "@reduxjs/toolkit": "^1.9.6",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-icons": "^4.11.0",
    "react-redux": "^8.1.3",
    "react-router-dom": "^6.16.0",
    "sleep-promise": "^9.1.0",
    "compression": "^1.7.4",
    "express": "^4.18.2",
    "sirv": "^2.0.3"
},

Além disso, como estamos em um projeto de TypeScript, precisamos instalar algumas dependências de @types para o TypeScript poder reconhecer essas alterações que estamos fazendo.

Ainda no package.json, dentro de devDependencies, adicionamos uma vírgula e colamos as dependências do @types/express, @types/node, @vitejs/plugin-react, cross-env, typescript e vite.

Note que algumas dependências estão conflitando, pois estão com versões mais atualizadas do que as que já estavam presentes. Por isso, removemos as duplicadas e deixamos as mais atualizadas que inserimos.

Até 2022, o Vite não tinha suporte nativo para server-side rendering. A partir da versão 5.0, ele passa a ter esse suporte sem precisar de algum outro framework ou uma outra biblioteca para resolver isso.

"devDependencies": {
    "@types/react": "^18.2.15",
    "@types/react-dom": "^18.2.7",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "autoprefixer": "^10.4.16",
    "eslint": "^8.45.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.3",
    "postcss": "^8.4.30",
    "tailwindcss": "^3.3.3",
    "@types/express": "^4.17.21",
    "@types/node": "^20.9.0",
    "@vitejs/plugin-react": "^4.2.0",
    "cross-env": "^7.0.3",
    "typescript": "^5.2.2",
    "vite": "^5.0.0"
}

Agora, não temos mais nenhuma dependência duplicada no projeto.

Atenção! O Vite não funciona mais com a versão 18 do Node. É importante que esteja na versão 18 ou superior. Atualmente, a versão 20 é a versão long-term service (LTS), mantida por pelo menos mais 2 ou 3 anos.

Estamos utilizando a versão 18.18.0, que foi a versão que iniciamos esse projeto. Se você estiver na versão 20 também, o projeto também vai funcionar normalmente. Mas, se você quiser seguir a versão que utilizamos, basta executar o seguinte comando:

nvm use 18.18.0

Agora, vamos executar npm install, já que fizemos essas alterações no package.json.

npm i

Com todas as dependências finalmente instaladas, ainda temos mais algumas configurações para fazer no projeto.

Entrada do lado do cliente e do lado do servidor

Atualmente, nosso arquivo de entrada está sendo o main.tsx. Agora, como vamos trabalhar com a renderização do lado de servidor, precisamos de dois arquivos. Um que vai ser um entry-client, que será a entrada das informações do lado do cliente, e um entry-server, que será o arquivo responsável por gerar as informações do lado do servidor.

Dentro de "src" no projeto da ByteBooks, clicamos com o botão direito para criar um novo arquivo chamado entry-client.tsx. Colamos um código que já estava pronto, ajustamos a importação do arquivo de rotas.

entry-client.tsx:

import './index.css';
import ReactDOM from 'react-dom/client';
import { BrowserRouter as Router } from 'react-router-dom';
import { Routes } from './routes';

ReactDOM.hydrateRoot(
    document.getElementById('root') as HTMLElement,
    <Router>
        <Routes />
    </Router>
);

Podemos salvar o arquivo de entrada para o lado do cliente.

Agora, criamos o arquivo de entrada do lado do servidor, que será chamado entry-server.tsx e também estará no diretório "src".

entry-server.tsx:

import ReactDOMServer from 'react-dom/server';
import { StaticRouter as Router } from 'react-router-dom';
import { Routes } from './routes';

export function render(url: string) {
    const html = ReactDOMServer.renderToString(
        <Router location={url}>
            <Routes />
        </Router>
    );
    return { html };
}

O código é bem semelhante com o lado do cliente, com a diferença que o nosso render está recebendo uma url como parâmetro e está passando para o nosso arquivo de rotas.

O nosso arquivo de rotas do lado do servidor está importando o StaticRouter, ou seja, do lado do servidor, as nossas rotas vão ser tratadas como rotas estáticas.

Enquanto na nossa entrada do cliente, a função tem algumas informações de hidratação, que vamos lidar em uma aula futura. Mas, o Router está vindo do BrowserRouter.

Ou seja, a forma que uma aplicação lida com as rotas do lado do cliente e do lado do servidor é diferente. Por isso, precisamos fazer essa diferenciação na hora de importar o Router para as nossas rotas.

Com a entrada do servidor e a entrada do lado do cliente finalizadas, vamos passar o Provider do Redux, o Header e Footer para o arquivo de rotas.

Para isso, colocamos a tag de abertura do Provider e o Header antes do Switch e a tag de fechamento do Provider e o Footer depois do Switch, importando o que for necessário.

index.tsx:

import { Provider } from 'react-redux';
import { store } from '../store/store';
import Header from '../components/Header';
import Footer from '../components/Footer';

export const Routes = () => (
    <Provider store={store}>
        <Header />
        <Switch>
            <Route exact path='/' component={Catalog} />
            <Route path='/book' component={BookDetail} />
        </Switch>
        <Footer />
    </Provider>
);

Agora, não precisamos mais do arquivo main.tsx. Por quê? Quando rodarmos o nosso servidor, a partir de agora, o canal de entrada vai ser o entry-client ou o entry-server. Por isso, o main não vai ser mais utilizado a partir de agora.

Para finalizar essa configuração do arquivo HTML, precisamos acessar o index.html e trocar a referência de onde ele está pegando o script.

Ao invés de pegar do main, vamos fazer com que ele pegue do entry-client, que será responsável por exibir a entrada pelo lado do cliente após a compilação do lado do servidor.

index.html:

<script type="module" src="/src/entry-client.tsx"></script>

Com todos os arquivos configurados e todas as dependências instaladas, vamos rodar npm run dev para abrir o nosso projeto.

npm run dev

No navegador, o projeto agora está rodando. A navegação de rotas, a aba de sacola, o retorno para o início e o filtro continuam funcionando normalmente.

Próximos passos

Finalizamos a transformação do projeto que estava em um gerador estático, numa versão antiga do Vite, para agora um modelo SSR, sendo uma renderização do lado do servidor.

Na próxima aula, vamos adentrar em alguns outros conceitos do funcionamento do server-side rendering e também discutir sobre os componentes do lado do servidor.

Sobre o curso React: utilizando SSR para otimizar a performance da aplicação

O curso React: utilizando SSR para otimizar a performance da aplicação possui 119 minutos de vídeos, em um total de 38 atividades. Gostou? Conheça nossos outros cursos de React em Front-end, ou leia nossos artigos de Front-end.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda React acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas