Reconstruindo um grande site de comércio eletrônico com o Next.js (estudo de caso)
Publicados: 2022-03-10Em nossa empresa, Unplatform, criamos sites de comércio eletrônico há décadas. Ao longo desses anos, vimos a pilha de tecnologia evoluir de páginas renderizadas pelo servidor com alguns JavaScript e CSS menores para aplicativos JavaScript completos.
A plataforma que usamos para nossos sites de comércio eletrônico era baseada em ASP.NET e quando os visitantes começaram a esperar mais interação, adicionamos o React para o front-end. Embora misturar os conceitos de uma estrutura da Web de servidor como ASP.NET com uma estrutura da Web do lado do cliente como React tenha tornado as coisas mais complicadas, ficamos muito felizes com a solução. Isso foi até entrarmos em produção com nosso cliente de maior tráfego. Desde o momento em que entramos ao vivo, tivemos problemas de desempenho . Os Core Web Vitals são importantes, ainda mais no e-commerce. Neste estudo da Deloitte: Milissegundos Fazem Milhões, os investigadores analisaram dados de sites móveis de 37 marcas diferentes. Como resultado, eles descobriram que uma melhoria de desempenho de 0,1s pode levar a um aumento de 10% na conversão.
Para mitigar os problemas de desempenho, tivemos que adicionar muitos servidores extras (não orçados) e tivemos que armazenar em cache agressivamente as páginas em um proxy reverso. Isso até exigiu que desativássemos partes da funcionalidade do site. Acabamos tendo uma solução muito complicada e cara que, em alguns casos, apenas servia estaticamente algumas páginas.
Obviamente, isso não parecia certo, até que descobrimos o Next.js . Next.js é um framework web baseado em React que permite gerar páginas estaticamente, mas você também pode usar renderização do lado do servidor, tornando-o ideal para e-commerce. Ele pode ser hospedado em um CDN como Vercel ou Netlify, o que resulta em menor latência . Vercel e Netlify também usam funções sem servidor para o Server Side Rendering, que é a maneira mais eficiente de escalar horizontalmente.
Desafios
Desenvolver com Next.js é incrível, mas definitivamente existem alguns desafios. A experiência do desenvolvedor com o Next.js é algo que você só precisa experimentar. O código que você escreve é visualizado instantaneamente em seu navegador e a produtividade vai para o céu. Isso também é um risco porque você pode facilmente se concentrar demais na produtividade e negligenciar a manutenção do seu código. Com o tempo, isso e a natureza não tipada do JavaScript podem levar à degradação de sua base de código. O número de bugs aumenta e a produtividade começa a cair.
Também pode ser um desafio no lado do tempo de execução das coisas . As menores alterações em seu código podem levar a uma queda no desempenho e em outros Core Web Vitals. Além disso, o uso descuidado da renderização do lado do servidor pode levar a custos de serviço inesperados.
Vamos dar uma olhada mais de perto em nossas lições aprendidas na superação desses desafios.
- Modularize sua base de código
- Lint e formate seu código
- Usar TypeScript
- Planeje o desempenho e avalie o desempenho
- Adicione verificações de desempenho ao seu Quality Gate
- Adicionar testes automatizados
- Gerencie agressivamente suas dependências
- Use um serviço de agregação de log
- A funcionalidade de reescrita do Next.js permite a adoção incremental
Lição aprendida: modularize sua base de código
Estruturas de front-end como o Next.js tornam muito fácil começar hoje em dia. Você acabou de executar npx create-next-app e pode começar a codificar. Mas se você não for cuidadoso e começar a criar código sem pensar em design, pode acabar com uma grande bola de lama.
Ao executar npx create-next-app
, você terá uma estrutura de pastas como a seguinte (é assim que a maioria dos exemplos é estruturada):
/public logo.gif /src /lib /hooks useForm.js /api content.js /components Header.js Layout.js /pages Index.js
Começamos usando a mesma estrutura. Tínhamos algumas subpastas na pasta de componentes para componentes maiores, mas a maioria dos componentes estava na pasta de componentes raiz. Não há nada de errado com essa abordagem e é bom para projetos menores. No entanto, à medida que nosso projeto cresceu, tornou-se mais difícil raciocinar sobre os componentes e onde eles são usados. Encontramos até componentes que não eram mais usados! Também promove uma grande bola de lama, porque não há uma orientação clara sobre qual código deve depender de qual outro código.
Para resolver isso, decidimos refatorar a base de código e agrupar o código por módulos funcionais (como os módulos NPM) em vez de conceitos técnicos:
/src /modules /catalog /components productblock.js /checkout /api cartservice.js /components cart.js
Neste pequeno exemplo, há um módulo de checkout e um módulo de catálogo. Agrupar o código dessa maneira leva a uma melhor descoberta: apenas olhando para a estrutura de pastas, você sabe exatamente que tipo de funcionalidade está na base de código e onde encontrá-la. Também torna muito mais fácil raciocinar sobre dependências . Na situação anterior, havia muitas dependências entre os componentes. Tivemos pull requests para alterações no checkout que também impactaram os componentes do catálogo. Isso aumentou o número de conflitos de mesclagem e tornou mais difícil fazer alterações.
A solução que funcionou melhor para nós foi manter as dependências entre os módulos no mínimo absoluto (se você realmente precisar de uma dependência, certifique-se de que seja unidirecional) e introduzir um nível de “projeto” que une tudo:
/src /modules /common /atoms /lib /catalog /components productblock.js /checkout /api cartservice.js /components cart.js /search /project /layout /components /templates productdetail.js cart.js /pages cart.js
Uma visão geral desta solução:
O nível do projeto contém o código para o layout do site de comércio eletrônico e os modelos de página. Em Next.js, um componente de página é uma convenção e resulta em uma página física. Em nossa experiência, essas páginas geralmente precisam reutilizar a mesma implementação e é por isso que introduzimos o conceito de “modelos de página”. Os modelos de página usam os componentes dos diferentes módulos, por exemplo, o modelo de página de detalhes do produto usará componentes do catálogo para exibir informações do produto, mas também um componente de adição ao carrinho do módulo de checkout.
Também temos um módulo comum, pois ainda existe algum código que precisa ser reutilizado pelos módulos funcionais. Ele contém átomos simples que são componentes React usados para fornecer uma aparência consistente. Ele também contém código de infraestrutura, pense em certos ganchos de reação genéricos ou código de cliente GraphQL.
Aviso : Certifique-se de que o código no módulo comum esteja estável e sempre pense duas vezes antes de adicionar código aqui, para evitar código emaranhado.
Micro front-ends
Em soluções ainda maiores ou ao trabalhar com equipes diferentes, pode fazer sentido dividir ainda mais o aplicativo nos chamados micro-frontends. Em resumo, isso significa dividir ainda mais o aplicativo em vários aplicativos físicos hospedados independentemente em diferentes URLs. Por exemplo: checkout.mydomain.com
e catalog.meudominio.com. Estes são então integrados por um aplicativo diferente que atua como um proxy.
A funcionalidade de reescrita do Next.js é ótima para isso e usá-la assim é suportada pelas chamadas Multi Zones.
O benefício das multizonas é que cada zona gerencia suas próprias dependências. Também torna mais fácil evoluir incrementalmente a base de código: Se uma nova versão do Next.js ou React for lançada, você pode atualizar as zonas uma por uma em vez de ter que atualizar toda a base de código de uma vez. Em uma organização com várias equipes, isso pode reduzir bastante as dependências entre as equipes.
Leitura adicional
- “Estrutura do projeto Next.js”, Yannick Wittwer, Medium
- “Um guia de 2021 sobre como estruturar seu projeto Next.js de maneira flexível e eficiente”, Vadorequest, Dev.to.
- “Micro Frontends”, Michael Geers
Lição aprendida: lint e formate seu código
Isso é algo que aprendemos em um projeto anterior: se você trabalha na mesma base de código com várias pessoas e não usa um formatador, seu código logo se tornará muito inconsistente. Mesmo se você estiver usando convenções de codificação e fazendo revisões, logo começará a notar os diferentes estilos de codificação, dando uma impressão confusa do código.
Um linter verificará seu código em busca de possíveis problemas e um formatador garantirá que o código seja formatado de maneira consistente. Usamos ESLint & mais bonito e achamos que eles são incríveis. Você não precisa pensar no estilo de codificação, reduzindo a carga cognitiva durante o desenvolvimento.
Felizmente, o Next.js 11 agora suporta ESLint pronto para uso (https://nextjs.org/blog/next-11), tornando super fácil de configurar executando npx next lint. Isso economiza muito tempo porque vem com uma configuração padrão para Next.js. Por exemplo, já está configurado com uma extensão ESLint para React. Melhor ainda, ele vem com uma nova extensão específica do Next.js que detectará até mesmo problemas com seu código que poderiam impactar o Core Web Vitals de seu aplicativo! Em um parágrafo posterior, falaremos sobre portões de qualidade que podem ajudá-lo a evitar o envio de código para um produto que acidentalmente prejudique seu Core Web Vitals. Esta extensão fornece feedback muito mais rápido, tornando-se uma ótima adição.
Leitura adicional
- "ESLint," Documentos Next.js
- “ESLint”, site oficial
Lição aprendida: usar TypeScript
À medida que os componentes foram modificados e refatorados, notamos que alguns dos acessórios dos componentes não eram mais usados. Além disso, em alguns casos, tivemos bugs devido a tipos incorretos ou ausentes de adereços sendo passados para os componentes.
TypeScript é um superconjunto de JavaScript e adiciona tipos, o que permite que um compilador verifique seu código estaticamente, como um linter em esteróides.
No início do projeto, não vimos realmente o valor de adicionar o TypeScript. Sentimos que era apenas uma abstração desnecessária. No entanto, um de nossos colegas teve boas experiências com o TypeScript e nos convenceu a experimentá-lo. Felizmente, o Next.js tem um ótimo suporte a TypeScript pronto para uso e o TypeScript permite adicioná-lo à sua solução de forma incremental. Isso significa que você não precisa reescrever ou converter toda a sua base de código de uma só vez, mas pode começar a usá-la imediatamente e converter lentamente o restante da base de código.
Assim que começamos a migrar componentes para o TypeScript, imediatamente encontramos problemas com valores errados sendo passados para componentes e funções. Além disso, o ciclo de feedback do desenvolvedor ficou mais curto e você é notificado sobre problemas antes de executar o aplicativo no navegador. Outro grande benefício que descobrimos é que torna muito mais fácil refatorar o código: é mais fácil ver onde o código está sendo usado e você imediatamente identifica props e código de componentes não utilizados. Em resumo, os benefícios do TypeScript:
- Reduz o número de erros
- Facilita a refatoração do seu código
- Código fica mais fácil de ler
Leitura adicional
- "TypeScript," Documentos Next.js
- TypeScript, site oficial
Lição aprendida: planejar o desempenho e medir o desempenho
O Next.js oferece suporte a diferentes tipos de pré-renderização: geração estática e renderização do lado do servidor. Para melhor desempenho, é recomendável usar a geração estática, que acontece durante o tempo de compilação, mas nem sempre isso é possível. Pense nas páginas de detalhes do produto que contêm informações de estoque. Esse tipo de informação muda com frequência e executar uma compilação toda vez não é bem dimensionado. Felizmente, o Next.js também suporta um modo chamado Regeneração Estática Incremental (ISR), que ainda gera a página estaticamente, mas gera uma nova em segundo plano a cada x segundos. Aprendemos que esse modelo funciona muito bem para aplicativos maiores. O desempenho ainda é ótimo, requer menos tempo de CPU do que a renderização no lado do servidor e reduz os tempos de compilação: as páginas são geradas apenas na primeira solicitação. Para cada página adicionada, você deve pensar no tipo de renderização necessário. Primeiro, veja se você pode usar geração estática; se não, vá para Regeneração Estática Incremental e, se isso também não for possível, você ainda poderá usar a renderização do lado do servidor.
Next.js determina automaticamente o tipo de renderização com base na ausência dos métodos getServerSideProps
e getInitialProps
na página. É fácil cometer um erro, o que pode fazer com que a página seja renderizada no servidor em vez de ser gerada estaticamente. A saída de uma compilação Next.js mostra exatamente qual página usa qual tipo de renderização, portanto, verifique isso. Também ajuda a monitorar a produção e acompanhar o desempenho das páginas e o tempo de CPU envolvido. A maioria dos provedores de hospedagem cobra com base no tempo de CPU e isso ajuda a evitar surpresas desagradáveis. Descreverei como monitoramos isso no parágrafo Lição aprendida: usar um serviço de agregação de log.
Tamanho do pacote
Para ter um bom desempenho é crucial minimizar o tamanho do pacote. Next.js tem muitos recursos prontos que ajudam, por exemplo, divisão automática de código. Isso garantirá que apenas o JavaScript e o CSS necessários sejam carregados para cada página. Também gera diferentes bundles para o cliente e para o servidor. No entanto, é importante estar atento a estes. Por exemplo, se você importar módulos JavaScript da maneira errada, o JavaScript do servidor pode acabar no pacote do cliente, aumentando muito o tamanho do pacote do cliente e prejudicando o desempenho. A adição de dependências do NPM também pode afetar bastante o tamanho do pacote.
Felizmente, o Next.js vem com um analisador de pacotes que fornece informações sobre qual código ocupa qual parte dos pacotes.
Leitura adicional
- “Next.js + Webpack Bundle Analyzer,” Vercel, GitHub
- "Busca de dados", Documentos Next.js
Lição aprendida: adicione verificações de desempenho ao seu Quality Gate
Um dos grandes benefícios de usar o Next.js é a capacidade de gerar páginas estaticamente e poder implantar o aplicativo na borda (CDN), o que deve resultar em ótimo desempenho e Web Vitals. Aprendemos que, mesmo com uma ótima tecnologia como o Next.js, obter e manter uma ótima pontuação de farol é muito difícil. Aconteceu várias vezes que, depois que implantamos algumas alterações na produção, a pontuação do farol caiu significativamente. Para retomar o controle, adicionamos testes automáticos de farol ao nosso portão de qualidade. Com esta ação do Github, você pode adicionar automaticamente testes lighthouse às suas solicitações de pull. Estamos usando o Vercel e toda vez que uma solicitação de pull é criada, o Vercel a implanta em uma URL de visualização e usamos a ação do Github para executar testes de farol nessa implantação.
Se você não quiser configurar a ação do GitHub por conta própria, ou se quiser levar isso ainda mais longe, você também pode considerar um serviço de monitoramento de desempenho de terceiros como o DebugBear. A Vercel também oferece um recurso Analytics, que mede os principais Web Vitals de sua implantação de produção. O Vercel Analytics realmente coleta as medidas dos dispositivos de seus visitantes, então essas pontuações são realmente o que seus visitantes estão experimentando. No momento da redação deste artigo, o Vercel Analytics funciona apenas em implantações de produção.
Lição aprendida: adicionar testes automatizados
Quando a base de código fica maior, fica mais difícil determinar se suas alterações de código podem ter quebrado a funcionalidade existente. Em nossa experiência, é vital ter um bom conjunto de testes de ponta a ponta como uma rede de segurança. Mesmo se você tiver um projeto pequeno, pode tornar sua vida muito mais fácil quando você tem pelo menos alguns testes básicos de fumaça. Temos usado o Cypress para isso e adoramos. A combinação de usar Netlify ou Vercel para implantar automaticamente sua solicitação Pull em um ambiente temporário e executar seus testes E2E não tem preço.
Usamos cypress-io/GitHub-action
para executar automaticamente os testes de cipreste em nossas solicitações de pull. Dependendo do tipo de software que você está construindo, pode ser valioso também ter testes mais granulares usando Enzyme ou JEST. A desvantagem é que eles são mais fortemente acoplados ao seu código e exigem mais manutenção.
Lição aprendida: gerenciar agressivamente suas dependências
Gerenciar dependências se torna uma atividade demorada, mas tão importante ao manter uma grande base de código Next.js. O NPM tornou a adição de pacotes tão fácil e parece haver um pacote para tudo hoje em dia. Olhando para trás, muitas vezes, quando introduzimos um novo bug ou tivemos uma queda no desempenho, isso tinha algo a ver com um pacote NPM novo ou atualizado.
Portanto, antes de instalar um pacote, você deve sempre se perguntar o seguinte:
- Qual é a qualidade do pacote?
- O que a adição deste pacote significa para o tamanho do meu pacote?
- Este pacote é realmente necessário ou existem alternativas?
- O pacote ainda é mantido ativamente?
Para manter o tamanho do pacote pequeno e minimizar o esforço necessário para manter essas dependências, é importante manter o número de dependências o menor possível. Seu eu futuro agradecerá por isso quando você estiver mantendo o software.
Dica : A extensão Import Cost VSCode mostra automaticamente o tamanho dos pacotes importados.
Acompanhe as versões do Next.js
Manter-se atualizado com Next.js e React é importante. Não só lhe dará acesso a novos recursos, mas novas versões também incluirão correções de bugs e correções para possíveis problemas de segurança. Felizmente, o Next.js torna a atualização incrivelmente fácil fornecendo Codemods (https://nextjs.org/docs/advanced-features/codemods. Essas são transformações automáticas de código que atualizam automaticamente seu código.
Atualizar dependências
Pela mesma razão, é importante manter as versões Next.js e React atuais; também é importante atualizar outras dependências. O dependabot do Github (https://github.com/dependabot) pode realmente ajudar aqui. Ele criará automaticamente Pull Requests com dependências atualizadas. No entanto, a atualização de dependências pode potencialmente quebrar as coisas, portanto, ter testes automatizados de ponta a ponta aqui pode realmente ser um salva-vidas.
Lição aprendida: usar um serviço de agregação de log
Para garantir que o aplicativo esteja se comportando corretamente e encontrar problemas de forma preventiva, descobrimos que é absolutamente necessário configurar um serviço de agregação de log. O Vercel permite efetuar login e visualizar os logs, mas estes são transmitidos em tempo real e não são persistidos. Também não suporta a configuração de alertas e notificações.
Algumas exceções podem levar muito tempo para aparecer. Por exemplo, configuramos Stale-While-Revalidate para uma página específica. Em algum momento, percebemos que as páginas não estavam sendo atualizadas e que dados antigos estavam sendo veiculados. Após verificar o log do Vercel, descobrimos que estava ocorrendo uma exceção durante a renderização em segundo plano da página. Usando um serviço de agregação de log e configurando um alerta para exceções, poderíamos detectar isso muito antes.
Os serviços de agregação de logs também podem ser úteis para monitorar os limites dos planos de preços da Vercel. A página de uso do Vercel também fornece informações sobre isso, mas o uso de um serviço de agregação de log permite adicionar notificações quando você atinge um determinado limite. É melhor prevenir do que remediar, principalmente quando se trata de cobrança.
A Vercel oferece várias integrações prontas para uso com serviços de agregação de log, incluindo Datadog, Logtail, Logalert, Sentry e muito mais.
Leitura adicional
- “Integrações”, Vercel
Lição aprendida: a funcionalidade de reescrita do Next.js permite a adoção incremental
A menos que haja alguns problemas sérios com o site atual, muitos clientes não ficarão animados em reescrever todo o site. Mas e se você pudesse começar a reconstruir apenas as páginas que mais importam em termos de Web Vitals? Foi exatamente isso que fizemos para outro cliente. Em vez de reconstruir o site inteiro, apenas reconstruímos as páginas que mais importam para SEO e conversão. Neste caso, as páginas de detalhes e categorias do produto. Ao reconstruir aqueles com Next.js, o desempenho aumentou muito.
A funcionalidade de reescrita do Next.js é ótima para isso. Criamos um novo front-end Next.js que contém as páginas do catálogo e o implantamos na CDN. Todas as outras páginas existentes são regravadas pelo Next.js no site existente. Dessa forma, você pode começar a ter os benefícios de um site Next.js com baixo esforço ou baixo risco.
Leitura adicional
- "Reescreve", Documentos Next.js
Qual é o próximo?
Quando lançamos a primeira versão do projeto e começamos a fazer sérios testes de desempenho, ficamos entusiasmados com os resultados. Não apenas os tempos de resposta da página e os Web Vitals eram muito melhores do que antes, mas os custos operacionais também eram uma fração do que eram antes. Next.js e JAMStack geralmente permitem que você escale horizontalmente da maneira mais econômica.
Mudar de uma arquitetura mais orientada para o back-end para algo como o Next.js é um grande passo. A curva de aprendizado pode ser bastante íngreme e, inicialmente, alguns membros da equipe realmente se sentiram fora de sua zona de conforto. Os pequenos ajustes que fizemos, as lições aprendidas com este artigo, realmente ajudaram nisso. Além disso, a experiência de desenvolvimento com Next.js oferece um incrível aumento de produtividade. O ciclo de feedback do desenvolvedor é incrivelmente curto!
Leitura adicional
- “Indo para produção”, Documentos Next.js