Scripts de terceiros da forma correta

Como utilizamos Partytown com Next.JS para aumentar nossa performance, significamente

Problema

Para explicar o problema, primeiro precisamos citar A main thread. No qual, de modo simplificado, é responsável pela renderização da página, executando algumas tarefas, como:

  1. Parseando HTML.
  2. Construindo o DOM.
  3. Parseando o CSS e aplicando os estilos.
  4. Parseando, analisando e executando o Javasript.
  5. Processando eventos do usuário.

Então, é coerente dizer que, quanto mais demorado é a finalização da execução da Main Thread, mais demorado fica para nosso site terminar de renderizar.

E esse problema só aumenta conforme vamos adicionando mais Javascript na página, pois toda vez que o a main thread está ocupada, sua página web não responde a eventos do usuário e isso impacta diretamente na performance e indexação do seu site.

Então resumidamente, quanto menos Javascript rodarmos na Main Thread enquanto renderizamos nossa página, melhor.

De onde vem tantos Scripts?

Os frameworks atuais (Next.JS & React.JS) já adicionam relativamente bastante Javascript às páginas e esses sim são essenciais para a renderização dos sites e, utilizando tecnicas como:

  1. tree shaking
  2. CSS puro (e não css-in-js)
  3. Cache de Scripts
  4. Async/Defer Carregamento de Scripts

Conseguimos atingir uma performance muito boa no Core Web Vitals.

Porém, você provavelmente já se deparou com o cenário em que precisa adicionar algum Script de terceiro no seu frontend, certo? E, dependendo em qual contexto ja trabalhou, pode já ter passado pela situação onde esses Scripts são tantos, que começam a causar mais problemas do que prometem auxiliar.

Um domínio de frontend que esse problema costuma ser comúm, são nos e-commerces e blogs de notícias, por conta de ser uma forma de marketing. Onde, por exemplo, para cada rede social que precise coletar informações da navegação do usuário, o modo mais rápido acaba sendo a adição de um código Javascript.

Exemplo

Script de terceiro do TikTok Pixel. Onde utilizamos o Script do Tiktok para gerar métricas de produtos vistos, por exemplo.

O que fizemos no Magalu

Aqui vou usar o site da Magazineluiza como exemplo, pois foi onde conseguimos (eu e meu time), evoluir de forma significativa o uso dos scripts de terceiros, dado que temos muitas integrações (mais de 10 Scripts).

[Spoiler]:

Antes

Depois

Partytown + Next.JS

Em teoria, o Next.JS em suas últimas versões já contempla com a possibilidade de carregar seus scripts de terceiros utilizando o Partytown.JS, Script Optimization - Next.JS.

Porém, a configuração padrão atualmente contém algumas limitações, principalmente para cenários mais complexos de configuração do próprio PartyTown. Exemplo, que irei entrar em mais detalhes a seguir, precisamos escrever a config de resolveUrl de forma customizada.

E atualmente a configuração padrão do Next.JS não permite tal nível de configuração.

Sendo assim, não iremos utilizar a configuração no next.config.js:

.json

  experimental: {
    nextScriptWorkers: true,
  },

E sim configurar diretamente no arquivo do _document.jsx:

.jsx

import { Partytown } from '@builder.io/partytown/react';
 
export default class CustomDocument extends Document {
  const partytownHtml = {
    __html: `
      partytown = {
        resolveUrl: (url, location) => {
            return url;
        }
      }
    `
  }
  render() {
    return (
        <Head>
          <script dangerouslySetInnerHTML={partytownHtml} />
          <Partytown debug={ false } />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }

Reparando que, o primeiro script antes da lib, é para escrevermos as configurações necessárias, que é lida no objeto window.partytown.

Com a configuração que mostramos a cima, já é possível utilizar a biblioteca para o carregamento de seus scripts, apenas adicionando o type "text/partytown". Exemplo:

.jsx

 
import Script from 'next/script'
 
export default function Home() {
  return (
    <>
      <Script src="https://example.com/script.js" type="text/partytown" />
    </>
  )
}

Porém, como dito anteriormente, nosso caso no Magalu é mais complexo, pois temos muitos scripts. E muitos deles ao serem carregados dentro do Web Worker, retornam erro de CORS, por se tratar de um fetch() direto do browser.

Por isso, precisamos evoluir nossas configurações de resolveUrl que consequentemente consiste na criação de um Reverse Proxy.

resolveUrl()

Como descrito aqui na documentação, essa configuração é usada para decidir de onde o Script será pego, ou seja, conseguimos desenvolver uma lógica para que dada url, usamos nosso proxy reverso para resolver de onde será pego o conteúdo do script JS, assim resolvendo o problema de CORS.

Exemplo:

.js

    partytown = {
      resolveUrl: (url, location) => {
          const reverseProxyUrl = 'https://reverse-proxy';
          const dnsToProxy = {
            'https://example.com': reverseProxyUrl,
          };
          if(dnsToProxy[url.hostname]) {
            return new URL(dnsToProxy[url.hostname]);
          }
          return url;
      }
    }

Configurando Reverse Proxy

Para ter uma configuração totalmente completa, precisamos construir um Proxy Reverso. Pois, como dissemos anteriormente, quando carregamos os scripts dentro de um Web Worker, estamos utilizando puramente uma request HTTP com fetch(), as requests precisam conter os headers corretos de CORS.

Existem inúmeras formas de se criar um Reverse Proxy, na própria documentação do PartyTown tem exemplos.

Arquitetura final

Bonus

Podemos rodar todos os Scripts via Partytown?

Recomendamos que não! Principalmente Scripts que fazem muitas leituras no DOM. Por exemplo, bibliotecas de heatmap, como Hotjar.

Como já alertado na documentação oficial, pode acabar gerando um lag nas interações da página.

Sendo assim, estamos buscando uma forma mais performática de como mantermos esses scripts de heatmap no site, porém sem penalizar tanto nosso score de web core vitals.

Exemplo do score sem bibliotecas de heatmap.