O site da Alice, assim como o da maior parte das empresas de tecnologia hoje, nasceu como uma página estática. Mas, ao longo do tempo, incorporou uma gama de funcionalidades que refletem nossos diferenciais como healthtech. Viabilizar essa transição, no entanto, exigiu migrar toda sua base de código para um novo framework, um projeto árduo que gerou melhora substancial na experiência do usuário, na performance do site e reduziu os custos de manutenção.
Hoje, o projeto faz uso do Next.js, num contexto de Server-Side Rendering (SSR) e um design system baseado em Stencil. Essa migração, porém, gerou desafios técnicos importantes que exigiram mudanças mais amplas em nossa arquitetura.
Neste artigo, exploramos os problemas e desafios que enfrentamos ao adotar essa stack, com foco em “piscas” na interface com o usuário final, e como os solucionamos e garantimos a melhor experiência e funcionalidades para quem está na ponta.
Next.js, Stencil e SSR
Quando criamos o site adotamos o Nuxt 2 como framework. Foi a escolha apropriada para o momento da empresa e as opções disponíveis à época. À medida que a Alice se desenvolveu, porém, o projeto foi crescendo em complexidade. Em poucos anos, implementamos subprodutos no site como uma calculadora de saúde, um simulador de planos e um sistema de consulta à nossa rede credenciada.
Migrar para o Nuxt 3 se provou uma opção indevida – tratava-se de um framework ainda imaturo que, por características de nossa base de código, exigiria tanto esforço quanto o necessário para migrar para outro framework. Em vez disso, optamos pelo Next.js.
Qualquer migração, porém, traz desafios. No nosso caso, precisávamos integrar perfeitamente uma aplicação em Next.js em SSR com o design system da Alice, baseado em Stencil.js, que exporta web components que podem ser encapsulados para React.
Imediatamente, enfrentamos problemas: um constante “pisca” na tela, que afetava negativamente a experiência do usuário, prejudicava as métricas do Core Web Vitals e reduzia a performance de nossas páginas em buscas no Google.
Uma investigação revelou que esse problema, conhecido como “flash of unstyled content” (FOUC), resultava da interação entre nosso design system, em Stencil.js, com projetos em SSR, como o Next.js. Mais especificamente, tratava-se de problemas na etapa de hidratação da página.
Hidratação
Vamos comparar a construção de uma página web com a montagem de uma casa. Inicialmente, você organiza a casa com móveis essenciais e decoração, preparando-a para receber visitas. Esse processo se assemelha à pré-renderização de uma página no Next.js, onde a estrutura básica é montada no lado servidor antes de qualquer interação do usuário.
Quando alguém visita a casa, não se limita a observar os móveis; interage com o ambiente, abre portas ou acende luzes. Na web, essa interação é possível graças à “hidratação”. Após a página ser carregada, os elementos se tornam interativos, permitindo ao usuário realizar ações como cliques e toq
Como era em uma stack baseada em Nuxt 2
Numa stack baseada em Nuxt 2 já conseguíamos utilizar os Web components do design system encapsulados para Vue, mas eles eram considerados como client components. Ou seja, toda a hidratação e construção da página era feita do lado do cliente/navegador, prejudicando assim no tempo total de carregamento e, consequentemente, nas notas do Core Web Vitals do site como um todo.
O projeto já tinha outros problemas relacionados a performance e experiência de desenvolvimento, pois era muito desafiador escalar páginas mantendo uma boa performance combinado com uma boa experiência de codificação e implementação de testes (unitários e e2e).
Um dos pontos que identificamos como defasado no projeto era a versão do Vue utilizada pelo Nuxt 2, pois o framework estava travado na versão 2.6.14 do Vue, que já era considerada legado. Isso nos impedia de atualizar em relação a outras soluções, como por exemplo o testing-library, cypress, design system, storybook, node, etc.
Além disso, identificamos também outros problemas de experiência de desenvolvimento como auto importação de arquivos em testes, mock de endpoints e store, tempo exacerbado de execução de build, test e lint.
Como a página operava no Next.js
No entanto, no Next.js, apesar de não passarmos pelos problemas citados acima, ainda precisávamos resolver a questão da hidratação dos componentes do Design System com o Next server.
Tecnicamente, a hidratação vincula o HTML pré-renderizado ao JavaScript necessário para tornar a página interativa. Então, quando o navegador carrega a página, o JavaScript é executado, permitindo interações – e esse processo é conhecido como hidratação. Ele inicializa a aplicação no lado do cliente após o carregamento, reativando event handlers e estados previamente definidos no HTML estático.
Em um aplicativo Next.js, a página é parcialmente pré-renderizada no servidor e entregue como HTML estático. Em seguida, o JavaScript necessário para sua interatividade é enviado e executado pelo navegador. Durante essa execução, ocorre a hidratação, onde o React reconecta os manipuladores de eventos e estados à estrutura de componentes já renderizada. Esse intervalo de tempo foi o que causou o “pisca” em nossa página.
Embora o Stencil já esteja implementando suporte dos web components para SSR, não poderíamos esperar o lançamento dessa funcionalidade, então por meio de uma série de estudos e benchmarking, conseguimos encontrar uma solução entre SSR e client web components.
A ideia principal era garantir que os web components do design system chegassem no cliente final totalmente hidratado e, para isso, criamos um servidor customizado em node e substituímos pelo Next server padrão onde combinamos recursos de renderização do Next com a hidratação do Stencil nos componentes do design system antes mesmo da resposta chegar no navegador.
Essa camada funciona especificamente para páginas com conteúdos totalmente estáticos, então também precisávamos encontrar uma solução para páginas dinâmicas. Para isso, usamos um conceito de Provider, que se trata da injeção de um componente em React responsável por hidratar os componentes do design system. Tudo isso foi feito de forma transparente para o usuário final, justamente na camada de Layout, ou seja, uma camada que age logo depois da resposta do servidor no navegador e antes do usuário receber a página totalmente renderizada.
Observação importante: A proposta final do Stencil é usarmos os web components como server components. Esse suporte foi disponibilizado recentemente e estamos realizando uma série de testes para consolidar de vez o Stencil + Nextjs + SSR.
Resultados
Os resultados foram observados imediatamente. Nossos Core Web Vitals subiram de 72 para 90 na comparação com a implementação em Nuxt 2, com os escores de SEO saltando de 61 para 100.
Em média, o carregamento melhorou em 50%.
A logística da migração
Encontrar a solução técnica é, porém, apenas metade do caminho. Só conseguiríamos extrair valor dessa iniciativa quando implementássemos a nova stack em toda nossa base de código, um processo trabalhoso e arriscado.
Dois times se dedicaram a realizar a migração: o squad de engenharia de produto de vendas, que é formalmente responsável pelo site, além de uma série de outros sistemas e entregas; e o time de plataforma em engenharia.
A primeira grande decisão a ser tomada era entre migrar de uma só vez ou reescrever gradualmente o projeto. Ambas as abordagens exigiriam esforço significativo. No entanto, a reescrita gradual permitiria que realizássemos em paralelo a transição para o Next.js com as entregas regulares do squad de vendas. Além disso, poderíamos monitorar a performance e os resultados da migração de cada funcionalidade, gerando aprendizados contínuos e refinamentos a cada página.
Optamos, então, por reescrever gradualmente todo o projeto, começando pela página inicial, avaliando métricas relevantes como conversão e performance por meio de testes A/B. O resultado foi uma implementação suave, acelerada e bem-sucedida, demarcando o modelo para migrações semelhantes no futuro.
____________________________________________________________________
The challenges of migrating to a Next.js, Stencil and SSR stack
How Alice’s engineering teams implemented a new technology stack for our website, improving load times by 50% and enhancing user experience.
Alice’s website, like many technology companies today, started as a static page. As we evolved, we integrated several functionalities that highlight our unique position as a healthtech company. To support this transition, we migrated the entire codebase to a new framework, a taxing project that yielded substantial improvements in user experience, website performance, and slashed maintenance costs.
Our current tech stack is based on Next.js with Server-Side Rendering (SSR) and a design system powered by Stencil. This migration involved significant technical challenges that necessitated broader architectural changes.
In this article, we delve into the specific problems and challenges we encountered while adopting this stack, with a particular focus on addressing “flashes” in the interface. We’ll discuss how we overcame these issues to ensure the best possible experience and functionality for our end-users.
Next.js, Stencil and SSR
When we started the website, we adopted the Nuxt 2 framework. It was a suitable choice given the company’s stage and the available options at the time. As Alice evolved, however, the project’s complexity increased. Within a few years, we implemented subproducts into the website including a health calculator, a plan simulator and a system allowing users to browse our accredited network of health community.
Migrating to Nuxt 3 proved insufficient – it was a framework still in its early stages, , and due to specificities in our codebase, it would require as much effort to implement as a migration to another framework. Instead, we opted for Next.js.
Any migration, however, bears challenges. In our case, we needed to perfectly integrate an application based on Next.js in SSR with Alice’s design system, oriented around Stencil.js, that exports web components that can be encapsulated for React.
We immediately faced a significant challenge: a constant screen flashing, which negatively impacted user experience, depressed Core Web Vitals, and reduced the website’s performance in Google search results.
An investigation revealed that this issue, known as ‘Flash of Unstyled Content’ (FOUC), was a consequence of the interaction between our Stencil.js design system and SSR projects such as Next.js. More specifically, it was a problem during the page’s hydration step.
Hydration
Creating a webpage is somewhat like setting up your home. First, you organize your living room with essential furniture and decoration, preparing it to host visits. This process is similar to prerendering a Next.js page, where its basic structure is built on the server before any user interaction.
When someone visits that apartment, they don’t just look at the furniture; they interact with their surroundings, turning on lights, opening doors, and so on. On the web, these interactions are made possible by ‘hydration’. Once the page loads, all elements become interactive, allowing users to click, tap, and more.
Using a Nuxt 2 stack
In a Nuxt2 stack, we were able to utilize web components from the design system encapsulated for Vue, but they were considered client components. The whole hydration and construction of the web page happened at the client side, hampering overall load times and, consequently, the whole website’s Core Web Vitals.
We had already identified other problems related to performance and development experience. It was very challenging to scale up pages maintaining good performance, an effective coding experience and implementing e2e and unit tests.
One of the main improvement points was the version of Vue used by Nuxt 2: we were still stuck with Vue’s legacy 2.6.14 version. That prevented us from updating other solutions, such as testing-library, cypress, design system, storybook, node, etc.
We also identified other DevX problems such as auto-importing of test files, endpoint mocks and store, and overly long times for executing build, test and lint.
How the page operated under Next.js
Under Next.js, though we didn’t encounter the same issues, we still needed to solve the problems involved in hydrating the design system components with the Next server.
Technically, the hydrating step connects a pre-rendered HTML with the JavaScript needed to make the page interactive. In other words, when the browser loads the webpage, it executes the JavaScript, allowing for interactions – and this process is known as hydration. It starts the application in the client side after loading, reactivating event handlers and states previously defined in the static HTML.
In a Next.js application, the page is partially pre-rendered on the server and delivered as static HTML. Subsequently, the necessary JavaScript for interactivity is sent and executed by the browser. During this execution, hydration occurs, where React reconnects event handlers and state to the already rendered component structure. This time interval caused the flash on our page.
Although Stencil was developing SSR support for web components, we couldn’t wait for the launch. Through research and benchmarking, we found a solution between SSR and client web components.
Our primary goal was to ensure fully hydrated design system components on the client side. To make it happen, we created a custom Node.js server to replace the standard Next server, combining Next’s rendering resources with Stencil’s hydration in the design system components before the response reached the browser.
That layer worked well for fully static pages, but we also needed to find a solution for dynamic pages. So, we implemented a Provider concept, injecting a React component responsible for hydrating design system components. All that happened in the Layout layer, that is, a layer activated as soon as the server responds to the browser and before the end user receives the fully rendered webpage.
It’s worth noting that Stencil’s final proposal involves using web components as server components. While this feature is now available, we’re currently conducting several tests to fully integrate Stencil, Next.js and SSR.
Results
Results were observed immediately. Our Core Web Vitals rose from 72 to 90 compared to the Nuxt 2 implementation, with SEO scores jumping from 61 to 100.
Loading improved 50% on average.
Migration logistics
Finding a technical solution is, however, only half the trouble. We could only generate value from this initiative once we had implemented the new stack throughout our codebase – a long and risky endeavor.
Two teams were tasked with the migration: the engineering team in the sales squad, formally responsible for the website as well as other systems; and the engineering platform team.
The first big decision was whether to migrate the entire project at once or to gradually refactor it. While both approaches required substantial effort, a gradual refactoring would allow us to parallelize the transition with the sales squad’s ongoing work. Besides, we could monitor performance and results for each functionality, enabling continuous learning and refining at every step.
Ultimately, we chose to gradually refactor the entire project, starting with the homepage, assessing relevant metrics such as conversion and performance through A/B tests. The end result was a smooth, quick and successful implementation, setting the standard for future migrations.