Core Web Vitals & SEO Técnico

Otimização Avançada de INP: Como Diagnosticar Long Tasks, Yielding na Main Thread e Web Workers

Core Web Vitals & SEO Técnico
Otimização Avançada de INP
Cayo Galotti Especialista Google

O Interaction to Next Paint (INP) avalia a responsividade de uma página ao longo de toda a sessão de navegação, medindo o tempo decorrido entre o comando do usuário e a atualização visual subsequente na tela. Diferente do antigo First Input Delay (FID), que registrava apenas o atraso inicial da primeira interação, o INP captura a pior latência de interação (ou o percentil 98 em páginas com muitas ações), tornando imperativa a gestão contínua dos recursos da thread principal.

Aprovar nos limites recomendados pelo Google exige manter o INP abaixo de 200 milissegundos no percentil 75 dos acessos reais (dados do CrUX). Quando o valor ultrapassa 500 milissegundos, a página entra na faixa crítica, degradando a experiência do visitante e prejudicando a performance orgânica nas buscas.

Dominar essa métrica exige entender a fundo o ciclo de vida do event loop do navegador, identificar gargalos de processamento com telemetria moderna e adotar estratégias estruturais como yielding e paralelismo via Web Workers.

Anatomia das três fases do INP

Toda interação do usuário com a página (seja um clique, um toque na tela sensível ou o pressionamento de uma tecla) percorre três etapas cronológicas distintas:

[Interação do Usuário]
         │
         ▼
┌──────────────────┐
│   Input Delay    │  Tempo até o callback começar a rodar
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ Processing Time  │  Execução síncrona dos event handlers
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│Presentation Delay│  Recalculate Style, Layout, Paint, Compositing
└────────┬─────────┘
         │
         ▼
[Next Paint no Display]

1. Input delay

Representa o intervalo entre a ação física do usuário e o momento em que a thread principal inicia a execução do primeiro manipulador de eventos registrado.

Se a main thread estiver ocupada processando uma tarefa longa anterior (como scripts pesados de terceiros, compilação de código ou processamento assíncrono), o evento fica retido na fila de tarefas. Em sites com arquiteturas ricas em JavaScript, como discutido na análise de SEO para headless WordPress e Next.js, a fase de hidratação frequentemente congestiona essa fila, inflando o input delay mesmo antes do usuário tentar interagir.

2. Processing time

Consiste na soma da duração de execução de todos os callbacks associados ao evento (click, pointerdown, keydown, entre outros).

Atrasos nesta fase costumam decorrer de manipulações densas do DOM, cálculos matemáticos síncronos, filtragens de grandes arrays ou execução de lógicas de negócio diretamente na thread de interface.

3. Presentation delay

Mede o tempo entre o término dos manipuladores de eventos e a apresentação visual do próximo quadro no monitor.

Esta etapa engloba o recálculo de estilos (Recalculate Style), o cálculo de posicionamento (Layout/Reflow), o desenho das camadas (Paint) e a composição final executada pelo processo de GPU. Operações que forçam sincronização de layout (layout thrashing) ou pinturas complexas em áreas extensas do documento disparam este tempo de forma substancial.


Diagnóstico avançado: da Long Tasks API à Long Animation Frames API (LoAF)

Identificar a raiz das falhas de INP requer ferramentas capazes de decompor a execução de scripts e relacioná-la diretamente aos frames perdidos.

Historicamente, a Long Tasks API monitorava qualquer tarefa da main thread com duração superior a 50 milissegundos. Embora útil, a especificação tradicional não fornecia visibilidade sobre o que ocorria após o término da tarefa (como estilos e renderização subsequentes), nem identificava com clareza o script causador.

O Chrome introduziu a Long Animation Frames API (LoAF), projetada especificamente para suprir essa lacuna e viabilizar o diagnóstico granular de problemas de INP.

Coletando dados com a LoAF API em tempo de execução

A LoAF API agrupa a execução de scripts e as fases de renderização em um único objeto de telemetria sempre que a atualização de um quadro excede 50 milissegundos.

O script a seguir implementa um coletor de telemetria utilizando PerformanceObserver:

const observer = new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    // Registra animações com renderização comprometida
    const totalDuration = entry.duration;
    const cpuTime = entry.cpuDuration;
    const renderDuration = entry.renderDuration;
    const styleAndLayoutDuration = entry.styleAndLayoutDuration;

    console.group(`Long Animation Frame detectado: ${totalDuration.toFixed(2)}ms`);
    console.log(`Carga de CPU: ${cpuTime.toFixed(2)}ms`);
    console.log(`Renderização: ${renderDuration.toFixed(2)}ms`);
    console.log(`Estilo e Layout: ${styleAndLayoutDuration.toFixed(2)}ms`);

    // Detalha scripts individuais responsáveis pelo travamento
    for (const script of entry.scripts) {
      console.log({
        invoker: script.invoker,
        sourceFile: script.sourceURL,
        functionName: script.sourceFunctionName,
        duration: script.duration.toFixed(2),
        executionStart: script.executionStart
      });
    }
    console.groupEnd();
  }
});

observer.observe({ type: 'long-animation-frame', buffered: true });

A LoAF API expõe a propriedade scripts, apontando a URL de origem do arquivo (sourceURL), o nome da função causadora (sourceFunctionName) e o tipo de invocador (invoker, como HTMLButtonElement.onclick ou Promise.then). Isso permite distinguir se o culpado pela lentidão é um componente interno ou uma biblioteca de tag manager.

Extração direta de métricas de interação no campo

Para isolar o INP diretamente em sessões de usuários reais, utilize a API de eventos com buffered: true:

let maxDuration = 0;

const interactionObserver = new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!entry.interactionId) continue;

    const inputDelay = entry.processingStart - entry.startTime;
    const processingTime = entry.processingEnd - entry.processingStart;
    const presentationDelay = entry.duration - (entry.processingEnd - entry.startTime);

    if (entry.duration > maxDuration) {
      maxDuration = entry.duration;
      console.warn(`Pior interação registrada (ID: ${entry.interactionId}):`, {
        totalINP: entry.duration.toFixed(2) + 'ms',
        inputDelay: inputDelay.toFixed(2) + 'ms',
        processingTime: processingTime.toFixed(2) + 'ms',
        presentationDelay: presentationDelay.toFixed(2) + 'ms',
        target: entry.target
      });
    }
  }
});

interactionObserver.observe({ type: 'event', durationThreshold: 16, buffered: true });

Estratégias de yielding na main thread

O navegador processa as tarefas em uma fila única na main thread. Quando um script roda ininterruptamente por 150 milissegundos, qualquer interação do usuário que chegue durante esse intervalo fica retida.

Yielding é o ato de pausar voluntariamente a execução de uma tarefa complexa, devolvendo o controle ao navegador para que ele possa processar eventos de entrada pendentes e pintar a tela antes de retomar a computação restante.

A evolução dos métodos de yielding

A abordagem tradicional utilizava setTimeout(callback, 0). Apesar de funcional, essa técnica apresenta problemas sérios:

  1. O navegador impõe um atraso mínimo de 4 milissegundos quando timeouts são aninhados repetidamente.
  2. A tarefa restante é enviada para o final da fila de tarefas ordinárias, competindo com requisições de rede, analíticos e outros timers.

Uma evolução temporária envolvia usar postMessage ou canais de mensagem com MessageChannel, que evitam o clamping de 4ms, mas não garantem prioridade semântica.

Yielding nativo com scheduler.yield()

A especificação moderna de agendamento de tarefas introduziu scheduler.yield(). Essa API suspende a execução da função assíncrona corrente e a agenda no topo da fila de tarefas da main thread, priorizando a continuidade do trabalho imediatamente após o navegador processar as entradas do usuário.

Veja a comparação entre uma rotina bloqueante e uma rotina otimizada com yielding:

// ABORDAGEM BLOQUEANTE: Mantém a main thread ocupada por centenas de milissegundos
function processarListaElementos(elementos) {
  for (const el of elementos) {
    executarCalculoPesado(el);
    atualizarPropriedadesDOM(el);
  }
}
// ABORDAGEM MODERNA COM YIELDING: Divide o processamento sem bloquear a UI
async function processarListaElementosOtimizado(elementos) {
  const LIMITE_FATIA_TEMPO = 30; // 30ms para evitar exceder o limite de 50ms de Long Task
  let inicioFatia = performance.now();

  for (const el of elementos) {
    executarCalculoPesado(el);
    atualizarPropriedadesDOM(el);

    if (performance.now() - inicioFatia > LIMITE_FATIA_TEMPO) {
      if ('scheduler' in window && 'yield' in window.scheduler) {
        await window.scheduler.yield();
      } else {
        // Fallback robusto usando MessageChannel
        await yieldFallback();
      }
      inicioFatia = performance.now();
    }
  }
}

function yieldFallback() {
  return new Promise((resolve) => {
    const channel = new MessageChannel();
    channel.port1.onmessage = () => resolve();
    channel.port2.postMessage(null);
  });
}

Agendamento inteligente com scheduler.postTask()

Para tarefas que não precisam responder imediatamente a uma ação do usuário, scheduler.postTask() permite definir prioridades explícitas:

  • user-blocking: Tarefas críticas para a interação atual.
  • user-visible: Tarefas perceptíveis, mas que não bloqueiam diretamente o frame seguinte.
  • background: Registros de telemetria, pré-carregamento e processamentos analíticos.
// Desloca tarefas analíticas secundárias para o background sem afetar o INP
if ('scheduler' in window && 'postTask' in window.scheduler) {
  window.scheduler.postTask(() => {
    enviarMetricasAnaliticas();
  }, { priority: 'background' });
}

Combinar o uso de scheduler.postTask para deferir código não essencial com a pré-renderização controlada via Speculation Rules API garante transições de página velozes sem penalizar a responsividade durante a leitura.


Paralelismo real: delegando trabalho pesado para Web Workers

Mesmo com yielding frequente, manter operações com grande demanda computacional na main thread degrada o frame rate e consome ciclos úteis de CPU. A solução definitiva para desacoplar cálculo pesado da interface é o uso de Web Workers.

Web Workers operam em threads de sistema operacionais isoladas. Eles não têm acesso direto ao DOM, o que impede problemas de concorrência e garante que a main thread permaneça dedicada exclusivamente a escutar eventos, atualizar a árvore de renderização e pintar o display.

Exemplo prático: processamento com Transferable Objects

A comunicação entre a main thread e um Worker ocorre tradicionalmente via clonagem estruturada (Structured Clone Algorithm), o que copia os dados na memória. Ao lidar com grandes massas de dados, o próprio processo de serialização pode gerar pausas na main thread.

O uso de Transferable Objects (como ArrayBuffer) transfere a posse da memória de uma thread para outra instantaneamente com custo zero de cópia (zero-copy):

// main.js: Thread principal que escuta o clique do usuário
const worker = new Worker('/workers/analise-dados.js');

document.getElementById('btnCalcular').addEventListener('click', () => {
  // Feedback visual imediato na tela (garante Presentation Delay baixo)
  document.getElementById('status').textContent = 'Processando...';

  // Cria um buffer binário de 10MB
  const buffer = new ArrayBuffer(10 * 1024 * 1024);
  const view = new Uint8Array(buffer);
  for (let i = 0; i < view.length; i++) {
    view[i] = i % 256;
  }

  // Transfere o buffer para o worker sem copiar memória
  worker.postMessage({ buffer }, [buffer]);
  console.log('Buffer transferido. Comprimento na main thread agora:', buffer.byteLength); // 0
});

worker.onmessage = (event) => {
  const { resultado } = event.data;
  document.getElementById('status').textContent = `Concluído: ${resultado}`;
};
// workers/analise-dados.js: Thread secundária isolada
self.onmessage = (event) => {
  const { buffer } = event.data;
  const view = new Uint8Array(buffer);

  // Executa processamento pesado sem interferir no event loop principal
  let soma = 0;
  for (let i = 0; i < view.length; i++) {
    soma += view[i];
  }

  self.postMessage({ resultado: soma });
};

Gerenciar eventos de mensagem manuais (postMessage e onmessage) pode tornar o código difícil de manter. A biblioteca Comlink (mantida pelo time do Google Chrome) cria um canal RPC (Remote Procedure Call) tipado, permitindo invocar métodos dentro do Worker como se fossem funções assíncronas comuns da main thread:

// worker.js
import * as Comlink from 'comlink';

const servicoPesquisa = {
  filtrarCatalogo(itens, termo) {
    const regex = new RegExp(termo, 'i');
    return itens.filter(item => regex.test(item.nome) || regex.test(item.descricao));
  }
};

Comlink.expose(servicoPesquisa);
// main.js
import * as Comlink from 'comlink';

const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
const servico = Comlink.wrap(worker);

// A main thread nunca trava durante a busca no catálogo
inputPesquisa.addEventListener('input', async (e) => {
  const resultados = await servico.filtrarCatalogo(catalogoCompleto, e.target.value);
  renderizarResultadosNaTela(resultados);
});

Otimização de presentation delay e layout thrashing

O Presentation Delay costuma ser a fase mais negligenciada da otimização de INP. Mesmo que os event handlers rodem em apenas 5 milissegundos, a renderização subsequente pode consumir 250 milissegundos se a estrutura visual for mal planejada.

O perigo da sincronização forçada de layout

O navegador calcula layouts de maneira preguiçosa (lazy layout). Quando modificamos classes ou estilos via JavaScript, o navegador agenda o recálculo para a fase de renderização do frame.

Se o código lê uma propriedade geométrica do DOM logo após alterar um estilo, o navegador é forçado a interromper a execução do script e rodar o recálculo de estilo e layout imediatamente de forma síncrona. Esse padrão é conhecido como layout thrashing:

// PADRÃO RUIM: Gera múltiplos cálculos forçados de layout (Thrashing)
elementos.forEach((el) => {
  el.style.width = '100px'; // Modificação (Write)
  const altura = el.offsetHeight; // Leitura (Read): Força Recalculate Style e Layout síncrono
  el.style.height = `${altura * 2}px'; // Modificação (Write)
});
// PADRÃO OTIMIZADO: Separação estrita de fases (Read then Write)
// 1. Fase de leitura limpa sem layout síncrono intermediário
const alturas = elementos.map(el => el.offsetHeight);

// 2. Fase de gravação agrupada
elementos.forEach((el, index) => {
  el.style.width = '100px';
  el.style.height = `${alturas[index] * 2}px`;
});

Regras para manter o presentation delay sob controle

  1. Evite animações com propriedades geométricas: Prefira sempre animar propriedades manipuladas diretamente pelo compositor da GPU (transform e opacity). Propriedades como top, left, width, height ou margin invalidam a árvore de layout e forçam repintura completa do nó e de seus descendentes.
  2. Utilize content-visibility: auto: Em documentos extensos ou páginas de listagem com rolagem longa, aplique content-visibility: auto em blocos secundários. O navegador suspende o trabalho de layout e pintura de elementos fora da viewport, diminuindo o custo das operações de estilo disparadas por interações.
  3. Divida atualizações com requestAnimationFrame: Atualizações cosméticas de interface devem aguardar o momento exato em que o display está pronto para desenhar o frame.

Monitoramento contínuo e validação de regressões no pipeline de CI/CD

Melhorias de INP não podem ser encaradas como um ajuste único de código. A cada nova biblioteca adicionada ou script analítico instalado via tag manager, o event loop pode voltar a ficar sobrecarregado.

Para prevenir que o código em produção sofra regressões nos Core Web Vitals, implemente rotinas automatizadas no seu fluxo de desenvolvimento, integrando testes de regressão de Web Vitals com GitHub Actions e Playwright.

Ao configurar testes automatizados com Playwright, simule condições de hardware restrito aplicando CPU throttling (4x ou 6x slowdown) e emulação de rede móvel:

// tests/inp.spec.ts
import { test, expect } from '@playwright/test';

test('A interação com o menu lateral deve ter latência inferior a 150ms', async ({ page }) => {
  const client = await page.context().newCDPSession(page);

  // Emula dispositivo móvel mediano com CPU 4x mais lenta
  await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });

  await page.goto('https://cayogalotti.com.br/');

  // Dispara interação e mensura com precisão
  const [duration] = await page.evaluate(async () => {
    return new Promise((resolve) => {
      const observer = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (entry.name === 'pointerdown' || entry.name === 'click') {
            observer.disconnect();
            resolve([entry.duration]);
          }
        }
      });
      observer.observe({ type: 'event', buffered: true });

      document.getElementById('menuToggle')?.click();
    });
  });

  expect(duration).toBeLessThan(150);
});

Além de testes sintéticos, monitore o impacto de mudanças de engenharia na indexação e tráfego orgânico com metodologias científicas como SEO split testing com Python e CausalImpact. Validar o lift causal de uma redução consistente de INP demonstra formalmente o valor dos investimentos em performance técnica de runtime perante as áreas de negócio e liderança.


Próximos passos para otimização de runtime

Atingir pontuações perfeitas de INP requer rigor na gestão da thread principal do navegador. Ao separar a computação em fatias com scheduler.yield(), mover processamento volumoso para Web Workers e erradicar o layout thrashing, sua aplicação garante interações imediatas mesmo em dispositivos móveis modestos.

Se você gerencia uma aplicação de grande escala e precisa mapear os gargalos de interação que afetam suas posições no Google, conheça nossos serviços de SEO técnico e auditoria de performance ou solicite um diagnóstico detalhado via WhatsApp.