RM
Voltar
Published on

Guia básico para otimizar a performance de aplicações Next.js

Passo a passo base de otimizações para melhorar a performance da sua aplicação Next.js, utilizando as métricas do Core Web Vitals.


Time to read
9min de leitura

Sempre senti falta de um lugar que eu pudesse centralizar as boas práticas de otimizações de performance que já apliquei em meus projetos Next.js, até para poder consultar depois. Decidi escrever esse post para centralizar tudo em um só lugar.

Este artigo serve como um guia básico (e talvez avançado) de otimizações para melhorar a performance de uma aplicação Next.js. Precisa melhorar a nota do seu site feito em Next.js? Utilize esse artigo como base, e otimize ponto a ponto a performance da sua aplicação.

Levarei em conta principalmente as métricas do Core Web Vitals, que são as métricas de performance que o Google utiliza para avaliar a experiência do usuário em um site.

Image Optimization

Eu diria que a otimização de imagens é uma das mais importantes, e uma das mais simples de se aplicar com o componente Image do Next.js. Apenas utilizando o componente você já ganha automaticamente uma série de otimizações. Se o seu site utiliza muitas imagens, por exemplo em um blog, ou ecommerce, essa otimização é essencial e provavelmente lhe dará muitos pontos na nota do Lighthouse.

O componente Image do Next.js é muito poderoso, e faz uma série de otimizações automáticas:

Otimizações Manuais

Para imagens que utilizam o atributo fill, sempre utilize o atributo sizes para definir a largura da sua imagem nos diferentes tamanhos de tela. Além de ajudar o Next a gerar tamanhos mais apropriados para o contexto de uso da sua imagem, ajudará o browser a decidir qual versão da imagem baixar. Caso você não utilize o atributo sizes, o Next.js irá utilizar o valor padrão de 100vw, o que na maioria dos casos é um grande desperdício.

Se a sua imagem por exemplo ocupa apenas 33% da largura da tela em desktop, e 50% de largura em mobile, você pode definir o atributo sizes da seguinte forma: sizes="(min-width: 1024px) 33vw, 50vw". Não definindo o atributo sizes neste contexto, faria com que uma imagem 2x maior que o necessário fosse exibida em mobile e 3x maior em desktop.

Com apenas essa otimização de imagens responsivas que o Next.js fornece automaticamente, com a adição do atributo sizes dando mais contexto da exibição da sua imagem, o tamanho da imagem servida ao usuário é reduzido drasticamente. Se por exemplo você tem uma imagem JPG ou PNG não otimizada de tamanho de 700kb, utilizando corretamente o atributo sizes, notará que a versão mobile conterá facilmente menos de 60kb.

É realmente impressionante o poder do componente Image do Next.js, mesmo com poucas configurações adicionais.

Além disso, mesmo depois de otimizadas, caso sua ferramenta de auditoria esteja acusando LCP em alguma imagem da primeira dobra, você pode utilizar o atributo priority para fazer o preload da imagem.

Ao contrário das imagens que podem ser carregadas apenas quando o usuário as vê, utilizando o recurso de Lazy Loading, existem as imagens que você deseja carregar antecipadamente, por exemplo imagens da primeira dobra da sua página. Geralmente essas imagens afetam o LCP do site. Utilizando o atributo priority no componente Image, o Next.js adiciona uma tag HTML link de preload para a imagem, fazendo com que o navegador carregue a imagem antecipadamente.

Font Optimization

Utilize o next/font do Next.js para carregar as fontes da sua aplicação. Com o next/font, é possível carregar as fontes com zero layout shift, e também melhoram a performance com o carregamento otimizado.

Você pode importar qualquer fonte do Google Fonts. Os arquivos de fonte são baixados em tempo de build e servidos junto com os seus static assets. Nenhuma request é feita para o Google no browser.

import { Inter } from 'next/font/google'
 
const inter = Inter({ subsets: ['latin'] })
 
export default function RootLayout({ children }) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

Code Splitting

Durante o desenvolvimento, é essencial pensar em Code Splitting. Code Splitting é o ato de dividir o código da aplicação em vários pedaços, e carregar apenas o código necessário para a experiência do usuário.

Dynamic Import

Utilize o Dynamic Import do Next.js para importar dinamicamente componentes apenas quando o seu uso for necessário.

'use client'

import { useState } from 'react'
import dynamic from 'next/dynamic'

import Component from '../components/a'

const DynamicComponent = dynamic(() => import('../components/b'))

export default function Example() {
  const [showMore, setShowMore] = useState(false)
 
  return (
    <div>
      {/* Carrega imediatamente */}
      <ComponentA />

      {/* Carrega sob demanda, apenas quando/se a condição for atendida */}
      {showMore && <ComponentB />}

      <button onClick={() => setShowMore(!showMore)}>Toggle</button>
    </div>
  )
}

Para uma boa experiência do usuário, é importante definir um fallback para o componente que está sendo carregado dinamicamente. O fallback é um componente que será exibido enquanto o componente dinâmico está sendo carregado. Isso evita que o usuário veja uma tela em branco enquanto o componente está sendo carregado, além de prevenir um possível CLS.

const DynamicComponent = dynamic(() => import('../components/b'), {
  loading: () => <p>Loading...</p>,
})

Carregando bibliotecas externas

Bibliotecas externas que são utilizadas apenas em momentos específicos do fluxo da aplicação, ou em interações específicas do usuário, não precisam ser carregadas no bundle inicial da aplicação.

Pra carregá-las dinamicamente, utilize o import dinâmico do javascript.

'use client'

import { useState } from 'react'

export default function Example() {
  const [showMore, setShowMore] = useState(false)

  return (
    <div>
      <button
        onClick={async () => {
          const { default: library } = await import('library')
          library.init()
          setShowMore(true)
        }}
      >
        Show More
      </button>
    </div>
  )
}

Server Components

Caso sua aplicação utilize uma versão igual ou superior ao Next.js 13, você pode utilizar Server Components. Quanto mais Server Components utilizados ao invés de Client Components, menor o bundle final e menos Javascript a ser carregado pelo usuário. Isso diminuirá o tempo de carregamento da página no geral, e ajudar em métricas como o TBT (Total Blocking Time) e o FCP (First Contentful Paint).

Utilize um desenvolvimento baseado em Server Components sempre que possível, e utilize Client Components apenas quando necessário. Além de diminuir o tamanho do bundle final, eles aumentam o tempo inicial de carregamento da página, já que eles não são enviados ao cliente e portanto o usuário não precisa esperar o Javascript ser baixado.

Otimização de Scripts de terceiros

Utilize o componente Script do Next.js para carregar scripts de terceiros de forma otimizada. Scripts como Google Analytics, Facebook Pixel, Hotjar, entre outros, afetam a performance da sua aplicação. Eles podem bloquear a renderização da página, ou adicionar um tempo de carregamento extra.

Para scripts que não exigem tempo de carregamento crítico ou com baixa prioridade, utilize o atributo strategy="lazyOnload". Essa estratégia de carregamento irá carregar o script apenas quando todos os recursos da página já tiverem sido carregados, e provavelmente não impactará na sua nota final de performance.

import Script from 'next/script'

export default function Home() {
  return (
    <div>
      <h1>My Homepage</h1>
      <Script src="https://www.google-analytics.com/analytics.js" strategy="lazyOnload" />
    </div>
  )
}

OBS: Tente evitar ao máximo utilizar o Google Tag Manager para carregar scripts e serviços na sua página. O GTM é um serviço que carrega scripts de terceiros de forma assíncrona. Priorize sempre carregar os scripts separadamente diretamente na sua página com estratégias de carregamento específicas para cada um utilizando o componente Script.

Bônus

Também é possível carregar o seu script em uma thread diferente da thread principal, utilizando o atributo strategy=worker. Com isso, o script será carregado em uma thread separada, e não irá bloquear os recursos que rodam na thread principal.

Pra habilitar o uso de strategy=worker, é necessário habilitar a flag nextScriptWorkers no seu arquivo next.config.js:

// next.config.js
module.exports = {
  experimental: {
    nextScriptWorkers: true,
  },
}

É importante entender que essa é uma estratégia avançada e não funciona para todos os scripts. É importante saber se o serviço utilizado depende de recursos da thread principal, e depois da implementação realizar uma bateria de testes para testar o seu funcionamento.

Infelizmente esse recurso ainda não está disponível para o App Router.

Estratégias de renderização

Utilize sempre que possível renderização estática. Se a sua página é um post de blog, ou um produto de e-commerce, ou qualquer página com um conteúdo não "real-time", e você possui uma base grande de usuários, não tem porquê não utilizar uma renderização estática.

Caso a sua página necessariamente precise e utilize de dados em tempo real, carregue a página utilizando a estratégia de streaming. Utilizar streaming para carregar a sua página permite que o conteúdo seja carregado de forma incremental, ou seja, não bloqueará a renderização da página enquanto todos os dados da página estão sendo carregados.

Exiba feedbacks ao usuário nas partes que estão sendo carregadas, como por exemplo com Skeletons, enquanto as partes que não exigem dados em tempo real são exibidas imediatamente.

Conclusão

As otimizações apresentadas podem ser feitas em quase qualquer aplicação. É importante entender que cada projeto possui seu próprio conjunto de desafios e otimizações, e que outras otimizações podem ser criadas dependendo do seu caso de uso.

No entanto, assegurando-se de que os pontos desse artigo foram aplicados ao seu projeto, você minimamente já estará no caminho certo para se ter uma boa performance em sua página.

É importante lembrar que otimização de performance é um processo contínuo, e um hábito que se deve ter durante o desenvolvimento. Fazer auditorias e gerar relatórios constantemente são partes do processo de desenvolvimento, e essenciais para continuar entregando boa performance aos seus usuários.

Essas são as otimizações que eu aplico em meus projetos, e que me ajudam a ter uma boa performance. Espero que elas também te ajudem.