INP (Interaction to Next Paint): Guia Definitivo para Otimizar a Responsividade do Seu Site

Guia completo sobre INP, a métrica Core Web Vital que substituiu o FID. Descubra como diagnosticar problemas de responsividade e 5 técnicas práticas para alcançar um INP abaixo de 200ms.

Introdução: O Que Mudou com o INP

Em março de 2024, a Google fez uma das mudanças mais significativas na história das Core Web Vitals: o Interaction to Next Paint (INP) substituiu oficialmente o First Input Delay (FID) como a métrica de responsividade de referência. E, honestamente, esta mudança era mais do que necessária. O FID limitava-se a medir o atraso da primeira interação do utilizador com a página — o que, na prática, contava apenas metade da história. O INP vai muito mais longe: avalia a latência de todas as interações ao longo de toda a sessão, desde cliques e toques até pressionar teclas no teclado.

E porque é que isto importa tanto? Os números falam por si.

Estudos consistentes mostram que 53% dos utilizadores abandonam um site que demora mais de 3 segundos a responder a uma interação. A responsividade não é apenas uma questão técnica — é uma questão de negócio. A redBus, uma das maiores plataformas de reserva de viagens da Índia, registou um aumento de 7% nas vendas após otimizar o seu INP. Instituições bancárias que trabalharam ativamente nesta métrica viram a percentagem de páginas com bom INP saltar de 55% para 87%.

O que torna o INP particularmente exigente é a sua abordagem abrangente. Em vez de capturar apenas o primeiro momento de interação — quando a página pode ainda estar relativamente livre de carga — o INP observa cada clique, cada toque e cada pressão de tecla durante toda a vida da página. No final, reporta a pior interação (ou próximo disso, usando o percentil 75 para ser estatisticamente robusto). Na prática, isto significa que um site pode ter um FID excelente e, ao mesmo tempo, um INP terrível. Interações posteriores como abrir um menu de filtros, submeter um formulário ou navegar entre separadores podem ser dolorosamente lentas — e agora isso conta.

Neste guia, vamos dissecar cada aspeto do INP: como funciona internamente, como diagnosticar problemas, e — o mais importante — como aplicar técnicas concretas para reduzir drasticamente a latência de interação do seu site. Há exemplos de código reais, estratégias específicas para os principais frameworks, e casos de estudo que demonstram o impacto no mundo real.

Anatomia de Uma Interação: Os Três Pilares do INP

Para otimizar o INP de forma eficaz, precisamos primeiro de compreender o que acontece internamente quando um utilizador interage com a página. Cada interação é composta por três fases distintas, e cada uma contribui para a latência total.

Input Delay (Atraso de Entrada)

O Input Delay é o tempo entre o momento em que o utilizador executa uma ação (por exemplo, clicar num botão) e o momento em que o primeiro event handler começa a executar. Este atraso acontece tipicamente porque a thread principal do browser está ocupada com outra coisa — talvez a executar JavaScript de um script de terceiros, a processar um timer, ou a completar uma tarefa de layout. Quanto mais longa for essa tarefa que bloqueia a thread principal, maior será o input delay.

Processing Time (Tempo de Processamento)

O Processing Time corresponde à execução efetiva dos event handlers associados à interação. Se um clique desencadeia múltiplos listeners (pointerdown, pointerup, click), todos são executados sequencialmente nesta fase. É aqui que a lógica da aplicação vive — chamadas a APIs, manipulações de estado, cálculos complexos e atualizações do DOM. Se os handlers executam trabalho pesado de forma síncrona, esta fase será provavelmente a principal culpada pelo INP elevado.

Presentation Delay (Atraso de Apresentação)

O Presentation Delay é o tempo entre o fim da execução dos event handlers e o momento em que o browser pinta efetivamente o próximo frame no ecrã. Inclui recalcular estilos, layout, pintura e composição. Se a interação alterou o DOM de forma extensiva ou provocou reflows complexos, o presentation delay pode ser substancial (e muitas vezes é o componente mais negligenciado).

Limiares do INP

A Google definiu os seguintes limiares:

  • Bom (Good): ≤ 200 milissegundos
  • Precisa de Melhoria (Needs Improvement): entre 200ms e 500ms
  • Fraco (Poor): > 500 milissegundos

A Abordagem do Percentil 75 (P75)

O INP não reporta a média nem a mediana das interações. A Google utiliza o percentil 75 (P75), o que significa que 75% das interações devem estar abaixo do limiar definido. Esta abordagem é deliberada: é robusta o suficiente para filtrar outliers extremos (que podem ter causas externas, como extensões do browser), mas suficientemente exigente para capturar padrões reais de lentidão.

Na prática, em páginas com poucas interações, o INP tende a ser a pior interação registada. Em páginas com muitas interações, pode ignorar uma ou duas das piores.

Dica prática: Ao analisar o INP, decomponha sempre a interação problemática nas três fases. Na minha experiência, o problema concentra-se frequentemente numa única fase, e a estratégia de otimização difere radicalmente conforme se trate de input delay, processing time ou presentation delay.

Diagnosticar Problemas de INP

Antes de otimizar, é preciso diagnosticar. E, convenhamos, o INP exige ferramentas específicas para ser corretamente identificado e decomposto. Vamos ver as abordagens mais eficazes.

Chrome DevTools: Performance Panel

O painel Performance do Chrome DevTools é o ponto de partida natural. Ao gravar uma sessão, pode identificar interações longas diretamente na timeline. Procure por blocos marcados com "Interaction" na faixa dedicada. A partir do Chrome 115, o DevTools destaca automaticamente as interações e mostra a decomposição em input delay, processing time e presentation delay.

Para uma análise mais direcionada, ative a opção "Web Vitals" no painel Performance. Isto sobrepõe marcadores visuais para cada interação, facilitando a identificação das mais lentas.

Real User Monitoring (RUM) com a Biblioteca web-vitals

Os dados de laboratório (DevTools) são úteis para debug, mas os dados de campo (Real User Monitoring) são o que a Google efetivamente utiliza no ranking. A biblioteca web-vitals v4 facilita a recolha de dados reais de INP com atribuição detalhada:

import { onINP } from 'web-vitals/attribution';

onINP((metric) => {
  const { name, value, attribution } = metric;
  const {
    interactionTarget,
    interactionType,
    inputDelay,
    processingDuration,
    presentationDelay,
    longAnimationFrameEntries
  } = attribution;

  console.group(`INP: ${value}ms`);
  console.log(`Elemento alvo: ${interactionTarget}`);
  console.log(`Tipo de interação: ${interactionType}`);
  console.log(`Input Delay: ${inputDelay.toFixed(1)}ms`);
  console.log(`Processing Time: ${processingDuration.toFixed(1)}ms`);
  console.log(`Presentation Delay: ${presentationDelay.toFixed(1)}ms`);

  if (longAnimationFrameEntries && longAnimationFrameEntries.length) {
    const loaf = longAnimationFrameEntries[0];
    console.log('Scripts lentos:', loaf.scripts.map(s => ({
      sourceURL: s.sourceURL,
      functionName: s.invokerType + ':' + s.invoker,
      duration: s.duration
    })));
  }

  console.groupEnd();
}, { reportAllChanges: true });

Long Animation Frames (LoAF) API

A Long Animation Frames API (LoAF) é uma evolução da antiga Long Tasks API e fornece atribuição muito mais detalhada. Enquanto a Long Tasks API apenas indicava que uma tarefa longa ocorreu, a LoAF identifica exatamente quais scripts, quais funções e quais URLs de origem contribuíram para a lentidão. É uma diferença enorme na hora de debugar.

// Observar Long Animation Frames e correlacionar com INP
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // Apenas frames com duração > 50ms são reportados
    if (entry.duration > 150) {
      console.warn(`LoAF longo detetado: ${entry.duration}ms`);

      entry.scripts.forEach((script) => {
        console.log({
          sourceURL: script.sourceURL,
          invoker: script.invoker,
          invokerType: script.invokerType,
          executionStart: script.executionStart,
          duration: script.duration,
          forcedStyleAndLayoutDuration: script.forcedStyleAndLayoutDuration,
          windowAttribution: script.windowAttribution
        });
      });
    }
  }
});

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

Identificar a Fase Problemática

Com os dados de atribuição em mãos, a lógica de diagnóstico é relativamente simples:

  • Input Delay elevado (>100ms): A thread principal estava bloqueada quando o utilizador interagiu. Procure long tasks ou scripts de terceiros que executam em momentos inoportunos. A solução passa por dividir tarefas longas ou gerir scripts de terceiros.
  • Processing Time elevado (>100ms): Os event handlers executam demasiado trabalho. Considere mover computações para Web Workers, usar scheduler.yield(), ou otimizações de framework.
  • Presentation Delay elevado (>100ms): O browser demora a pintar as alterações. Reveja o tamanho do DOM, evite layout thrashing e use content-visibility.

Dica prática: Em produção, envie os dados de atribuição para o seu serviço de analytics. Ao cruzar o elemento alvo (interactionTarget) com a fase problemática, conseguirá priorizar as correções com maior impacto.

Long Tasks: O Inimigo Número Um do INP

Se tivesse de apontar um único culpado pelos problemas de INP na maioria dos sites, seriam as Long Tasks. Sem sombra de dúvida. Uma Long Task é qualquer bloco de execução JavaScript que ocupa a thread principal do browser por mais de 50 milissegundos. Enquanto essa tarefa está a executar, a thread principal fica completamente bloqueada — incapaz de responder a qualquer interação.

O Que São Long Tasks e Por Que São Problemáticas

O browser executa o JavaScript numa única thread principal (o famoso "main thread"). Esta mesma thread é responsável por processar eventos do utilizador, executar JavaScript, calcular layouts e pintar frames. Quando uma tarefa JavaScript longa está a executar — digamos, 300ms de processamento de dados — qualquer clique, toque ou pressão de tecla que ocorra durante esses 300ms terá de esperar até que a tarefa termine. Só depois é que o browser pode sequer começar a processar o evento. Este tempo de espera é o input delay.

Imagine um cenário concreto: o site carrega um script de analytics que demora 200ms a inicializar. Se o utilizador clicar num botão "Adicionar ao Carrinho" durante esses 200ms, vai ter de esperar todo esse tempo antes de ver qualquer feedback visual. Na perspetiva do utilizador, o site simplesmente "congelou".

Identificar Long Tasks com DevTools e LoAF

No Chrome DevTools, as Long Tasks aparecem como barras vermelhas (striped) no painel Performance. Cada barra representa uma tarefa que excedeu os 50ms. Ao clicar numa Long Task, pode ver o call stack completo e identificar exatamente qual função ou script está a causar o bloqueio.

Com a LoAF API, obtém informação ainda mais granular em produção: não apenas a duração total da tarefa, mas quais scripts individuais contribuíram e quanto tempo cada um consumiu. A propriedade scripts de cada entrada LoAF lista todos os scripts que executaram durante aquele frame, com a URL de origem, o nome da função invocadora e a duração individual.

Impacto no Mundo Real

As Long Tasks são particularmente insidiosas porque o seu impacto não é linear. Uma tarefa de 100ms pode parecer aceitável em laboratório, mas se ocorrer no momento exato em que o utilizador interage, resulta num input delay de até 100ms — que se soma ao processing time e ao presentation delay.

Numa análise de campo, onde milhares de utilizadores interagem em momentos aleatórios, a probabilidade de uma Long Task coincidir com uma interação é proporcional à sua duração. Uma tarefa de 500ms tem 10 vezes mais probabilidade de causar input delay do que uma de 50ms. Pensem nisso um momento.

Projetos que identificaram e eliminaram as suas Long Tasks mais frequentes conseguiram reduções de INP entre 30% e 70%. A eliminação de Long Tasks é, consistentemente, a intervenção com maior retorno sobre investimento na otimização do INP.

Técnica 1: Dividir Long Tasks com scheduler.yield()

A abordagem mais eficaz para eliminar Long Tasks é dividi-las em blocos mais pequenos, cedendo periodicamente o controlo ao browser para que este possa processar interações pendentes. E a API scheduler.yield() é a ferramenta moderna para este efeito.

O Que é scheduler.yield()

A função scheduler.yield() é uma API nativa do browser que permite ao código "pausar" a execução, devolvendo o controlo à thread principal. O browser pode então processar eventos pendentes, pintar frames, e retornar ao código quando a thread estiver disponível. O aspeto mais importante — e o que distingue scheduler.yield() de alternativas como setTimeout(0) — é que preserva a prioridade da tarefa.

A Diferença Crucial Face a setTimeout(0)

Quando se usa setTimeout(fn, 0) para ceder o controlo, a continuação da tarefa vai para o final da fila de tarefas. Qualquer outra tarefa pendente — incluindo scripts de terceiros, timers, e outras callbacks — pode executar antes do código retomar. Com scheduler.yield(), a continuação é colocada no início da fila com a mesma prioridade, garantindo que o código retoma logo que o browser termine de processar os eventos pendentes. Não é ultrapassado por tarefas de menor prioridade.

Parece um detalhe subtil, mas na prática faz uma diferença enorme.

Exemplo Prático: Processar uma Lista Grande de Itens

// Sem otimização: Long Task que bloqueia a thread
function processarTodosOsItens(itens) {
  const resultados = [];
  for (const item of itens) {
    resultados.push(calcularPreco(item)); // Operação custosa
  }
  atualizarUI(resultados);
}

// Com scheduler.yield(): tarefa dividida em blocos
async function processarTodosOsItensOtimizado(itens) {
  const resultados = [];
  const TAMANHO_BLOCO = 50;

  for (let i = 0; i < itens.length; i++) {
    resultados.push(calcularPreco(itens[i]));

    // A cada 50 itens, ceder o controlo ao browser
    if (i % TAMANHO_BLOCO === 0 && i > 0) {
      await scheduler.yield();
    }
  }

  atualizarUI(resultados);
}

Padrão de Fallback para Browsers Sem Suporte

Como scheduler.yield() é uma API relativamente recente, é prudente implementar um fallback. O padrão seguinte oferece a melhor experiência possível em cada browser:

// Utilitário de yield com fallback robusto
function yieldToMain() {
  // Usar scheduler.yield() se disponível (preserva prioridade)
  if (typeof scheduler !== 'undefined' && typeof scheduler.yield === 'function') {
    return scheduler.yield();
  }

  // Fallback: scheduler.postTask com prioridade 'user-blocking'
  if (typeof scheduler !== 'undefined' && typeof scheduler.postTask === 'function') {
    return scheduler.postTask(() => {}, { priority: 'user-blocking' });
  }

  // Último recurso: setTimeout(0)
  return new Promise((resolve) => {
    setTimeout(resolve, 0);
  });
}

// Função genérica para processar itens em blocos
async function processarEmBlocos(itens, processarItem, tamBloco = 50) {
  const resultados = [];

  for (let i = 0; i < itens.length; i++) {
    resultados.push(processarItem(itens[i], i));

    // Ceder controlo periodicamente
    if ((i + 1) % tamBloco === 0) {
      await yieldToMain();
    }
  }

  return resultados;
}

// Utilização
const precos = await processarEmBlocos(
  produtos,
  (produto) => calcularPrecoComDesconto(produto),
  30 // ceder a cada 30 itens
);

Dica prática: Comece com um tamanho de bloco de 50 itens e ajuste conforme os resultados. Use o Performance panel do DevTools para verificar que nenhuma tarefa individual excede os 50ms após a divisão.

É importante notar que scheduler.yield() não torna o processamento total mais rápido — de facto, adiciona um pequeno overhead. Mas o que faz é garantir que o browser permanece responsivo durante todo o processamento. O utilizador pode continuar a interagir com a página sem sentir qualquer bloqueio, mesmo que o processamento em segundo plano demore um pouco mais. E, no fundo, é isso que o INP mede.

Técnica 2: Mover Trabalho Pesado para Web Workers

Enquanto scheduler.yield() divide tarefas longas em blocos mais pequenos dentro da thread principal, os Web Workers vão um passo mais longe: permitem mover o trabalho pesado para uma thread completamente separada, libertando a thread principal inteiramente para responder a interações.

Quando Usar Web Workers

Web Workers são ideais para operações computacionalmente intensivas que não necessitam de acesso ao DOM. Exemplos clássicos: ordenar e filtrar grandes conjuntos de dados, processar imagens, realizar cálculos matemáticos complexos, parsear grandes ficheiros JSON ou CSV, e executar algoritmos de pesquisa ou indexação.

Exemplo: Filtrar e Ordenar um Dataset Grande

// worker-filtro.js - Executa numa thread separada
self.addEventListener('message', (event) => {
  const { produtos, filtros, ordenacao } = event.data;

  // Filtrar produtos (operação potencialmente lenta com 10k+ itens)
  let resultado = produtos.filter((produto) => {
    if (filtros.categoria && produto.categoria !== filtros.categoria) return false;
    if (filtros.precoMin && produto.preco < filtros.precoMin) return false;
    if (filtros.precoMax && produto.preco > filtros.precoMax) return false;
    if (filtros.pesquisa) {
      const termo = filtros.pesquisa.toLowerCase();
      if (!produto.nome.toLowerCase().includes(termo) &&
          !produto.descricao.toLowerCase().includes(termo)) {
        return false;
      }
    }
    return true;
  });

  // Ordenar resultado
  if (ordenacao) {
    resultado.sort((a, b) => {
      if (ordenacao.campo === 'preco') {
        return ordenacao.direcao === 'asc' ? a.preco - b.preco : b.preco - a.preco;
      }
      return ordenacao.direcao === 'asc'
        ? a[ordenacao.campo].localeCompare(b[ordenacao.campo])
        : b[ordenacao.campo].localeCompare(a[ordenacao.campo]);
    });
  }

  self.postMessage({ resultado, total: resultado.length });
});

// main.js - Thread principal permanece livre
const workerFiltro = new Worker('/worker-filtro.js');

function filtrarProdutos(filtros, ordenacao) {
  // Mostrar indicador de carregamento imediatamente (sem bloqueio!)
  mostrarSpinner();

  workerFiltro.postMessage({ produtos: todosOsProdutos, filtros, ordenacao });
}

workerFiltro.addEventListener('message', (event) => {
  const { resultado, total } = event.data;
  esconderSpinner();
  renderizarListaProdutos(resultado);
  atualizarContador(total);
});

Simplificar com Comlink

A comunicação via postMessage pode tornar-se verbosa e difícil de manter. A biblioteca Comlink, desenvolvida pela equipa do Google Chrome, simplifica drasticamente a utilização de Web Workers — as funções do worker passam a parecer chamadas assíncronas normais:

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

const servico = {
  filtrarProdutos(produtos, filtros, ordenacao) {
    // Mesma lógica de filtragem e ordenação
    let resultado = produtos.filter(/* ... */);
    resultado.sort(/* ... */);
    return { resultado, total: resultado.length };
  },

  calcularEstatisticas(produtos) {
    // Cálculos pesados
    return {
      precoMedio: produtos.reduce((s, p) => s + p.preco, 0) / produtos.length,
      totalCategorias: new Set(produtos.map(p => p.categoria)).size
    };
  }
};

Comlink.expose(servico);

// main.js - Utilização muito mais simples
import * as Comlink from 'comlink';

const servico = Comlink.wrap(new Worker('/worker-comlink.js'));

// Chamar funções do worker como se fossem locais
async function aplicarFiltros() {
  mostrarSpinner();
  const { resultado, total } = await servico.filtrarProdutos(
    todosOsProdutos, filtrosActuais, ordenacaoActual
  );
  esconderSpinner();
  renderizarListaProdutos(resultado);
}

Limitações e Considerações

Os Web Workers não têm acesso ao DOM — não podem manipular elementos HTML diretamente. Os dados são transferidos via structured clone (cópia), o que tem um custo para objetos muito grandes. Para dados volumosos, considere usar Transferable objects ou SharedArrayBuffer. E não se esqueça: cada Worker consome memória adicional, pelo que é recomendável reutilizar workers em vez de criar novos para cada operação.

Técnica 3: Otimizações Específicas de Frameworks

A maioria dos sites modernos usa um framework JavaScript, e cada um oferece mecanismos próprios para melhorar a responsividade. Vamos ver as técnicas mais relevantes para os três frameworks mais populares.

React: useTransition, useDeferredValue e React.memo

O React 18+ trouxe o conceito de concurrent rendering, que permite marcar certas atualizações de estado como "transições" — atualizações de baixa prioridade que o React pode interromper para processar interações mais urgentes.

O hook useTransition é particularmente poderoso para o INP. Quando se envolve uma atualização de estado numa transição, o React pode interromper o rendering se o utilizador executar uma nova interação. A interface permanece responsiva, mesmo durante operações pesadas.

import { useState, useTransition, useMemo } from 'react';

function PesquisaProdutos({ produtos }) {
  const [termoPesquisa, setTermoPesquisa] = useState('');
  const [filtroAtivo, setFiltroAtivo] = useState('');
  const [isPending, startTransition] = useTransition();

  // A atualização do input é imediata (alta prioridade)
  // A filtragem da lista é uma transição (baixa prioridade)
  const handlePesquisa = (event) => {
    const valor = event.target.value;

    // Atualizar o campo de texto imediatamente
    setTermoPesquisa(valor);

    // Marcar a filtragem como transição de baixa prioridade
    startTransition(() => {
      setFiltroAtivo(valor);
    });
  };

  // Filtragem potencialmente custosa com milhares de produtos
  const produtosFiltrados = useMemo(() => {
    if (!filtroAtivo) return produtos;
    const termoLower = filtroAtivo.toLowerCase();
    return produtos.filter((produto) =>
      produto.nome.toLowerCase().includes(termoLower) ||
      produto.descricao.toLowerCase().includes(termoLower) ||
      produto.categoria.toLowerCase().includes(termoLower)
    );
  }, [produtos, filtroAtivo]);

  return (
    <div>
      <input
        type="search"
        value={termoPesquisa}
        onChange={handlePesquisa}
        placeholder="Pesquisar produtos..."
      />

      {isPending && <div className="spinner">A filtrar...</div>}

      <ul>
        {produtosFiltrados.map((produto) => (
          <ProdutoItem key={produto.id} produto={produto} />
        ))}
      </ul>
    </div>
  );
}

// React.memo evita re-renders desnecessários de cada item
const ProdutoItem = React.memo(function ProdutoItem({ produto }) {
  return (
    <li className="produto-item">
      <h3>{produto.nome}</h3>
      <p>{produto.descricao}</p>
      <span className="preco">{produto.preco}€</span>
    </li>
  );
});

O useDeferredValue é uma alternativa útil quando não se controla diretamente a atualização de estado. Aceita um valor e devolve uma versão "diferida" que pode ficar para trás durante transições pesadas, mantendo a UI responsiva.

Vue: v-memo, Componentes Assíncronos e Computed Properties

O Vue oferece mecanismos igualmente poderosos. A diretiva v-memo evita re-renders desnecessários de listas, e os componentes assíncronos com defineAsyncComponent permitem carregar componentes pesados apenas quando necessários.

<!-- Componente assíncrono com Vue 3 -->
<script setup>
import { defineAsyncComponent, ref, computed, shallowRef } from 'vue';

// Carregar componente pesado de gráficos apenas quando necessário
const GraficoAvancado = defineAsyncComponent({
  loader: () => import('./GraficoAvancado.vue'),
  loadingComponent: LoadingSpinner,
  delay: 200, // Mostrar spinner após 200ms
  timeout: 10000
});

const produtos = shallowRef([]); // shallowRef para arrays grandes
const termoPesquisa = ref('');

// computed com cache automático - só recalcula quando dependências mudam
const produtosFiltrados = computed(() => {
  if (!termoPesquisa.value) return produtos.value;
  const termo = termoPesquisa.value.toLowerCase();
  return produtos.value.filter((p) =>
    p.nome.toLowerCase().includes(termo)
  );
});
</script>

<template>
  <input v-model="termoPesquisa" placeholder="Pesquisar..." />

  <!-- v-memo evita re-render se o item não mudou -->
  <div v-for="produto in produtosFiltrados"
       :key="produto.id"
       v-memo="[produto.id, produto.atualizado]">
    <h3>{{ produto.nome }}</h3>
    <p>{{ produto.preco }}€</p>
  </div>

  <!-- Componente pesado carregado assincronamente -->
  <GraficoAvancado
    v-if="mostrarGrafico"
    :dados="produtosFiltrados"
  />
</template>

Angular: OnPush Change Detection e @defer Blocks

O Angular oferece duas ferramentas particularmente relevantes para o INP: a estratégia de deteção de mudanças OnPush e os blocos @defer introduzidos no Angular 17.

A estratégia OnPush reduz drasticamente o número de verificações de mudança, limitando a deteção apenas a componentes cujos inputs mudaram efetivamente ou que receberam eventos explícitos. Na prática, é uma das primeiras coisas que devemos configurar em qualquer componente Angular que apareça em listas ou se repita muitas vezes.

<!-- Angular: @defer para carregamento condicional -->
<!-- O conteúdo dentro de @defer só é carregado quando a condição é satisfeita -->

@defer (on viewport) {
  <app-comentarios [artigoId]="artigo.id" />
} @loading (minimum 300ms) {
  <div class="skeleton-comentarios">A carregar comentários...</div>
} @placeholder {
  <div class="placeholder-comentarios">
    <p>Os comentários serão carregados quando visíveis.</p>
  </div>
}

@defer (on interaction) {
  <app-editor-avancado />
} @placeholder {
  <textarea placeholder="Clique para ativar o editor avançado..."></textarea>
}

O bloco @defer (on interaction) é particularmente relevante para o INP: em vez de carregar um componente pesado logo de início (o que aumentaria o JavaScript na thread principal), o componente só é carregado quando o utilizador interage com o placeholder. Menos JavaScript na thread principal significa menos Long Tasks e menos probabilidade de interações lentas.

Dica prática: Independentemente do framework, a regra de ouro é a mesma: diferencie entre atualizações urgentes e não urgentes. A resposta visual imediata ao utilizador (feedback tátil, estado do botão, spinner) deve ser sempre urgente. Recalcular e renderizar listas grandes pode ser diferido.

Técnica 4: Reduzir o Presentation Delay

O presentation delay é frequentemente negligenciado — e, na minha opinião, é o fator mais subestimado. Pode ser o principal contribuinte para um INP elevado em páginas com DOMs complexos. Esta fase compreende tudo o que acontece após os event handlers terminarem e antes do utilizador ver o resultado visual no ecrã.

Minimizar o Tamanho do DOM

O tamanho do DOM tem um impacto direto no tempo que o browser demora a recalcular estilos e executar layouts. A Google recomenda manter o DOM abaixo de 1400 nós para um desempenho ótimo, com um máximo absoluto de 800 nós visíveis em qualquer momento.

Técnicas práticas para reduzir o DOM: virtualizar listas longas (com bibliotecas como react-virtual, vue-virtual-scroller ou implementação nativa), remover elementos ocultos do DOM em vez de usar display: none, e usar fragmentos em vez de múltiplos wrappers desnecessários.

Evitar Layout Thrashing

O layout thrashing ocorre quando o código JavaScript força o browser a executar layouts síncronos repetidamente, alternando entre leituras e escritas no DOM. É uma das causas mais comuns de presentation delay elevado — e, sinceramente, uma das mais fáceis de corrigir:

// MAU: Layout thrashing - força layout síncrono em cada iteração
function atualizarPosicoes(elementos) {
  elementos.forEach((el) => {
    const altura = el.offsetHeight; // LEITURA - força layout
    el.style.top = (altura * 2) + 'px'; // ESCRITA - invalida layout
    // Na próxima iteração, a leitura força NOVO layout
  });
}

// BOM: Separar leituras e escritas (batch)
function atualizarPosicoesBatch(elementos) {
  // Primeiro: todas as leituras
  const alturas = elementos.map((el) => el.offsetHeight);

  // Depois: todas as escritas
  elementos.forEach((el, i) => {
    el.style.top = (alturas[i] * 2) + 'px';
  });
}

content-visibility: auto para Conteúdo Fora do Ecrã

A propriedade CSS content-visibility: auto instrui o browser a ignorar o rendering de elementos fora do viewport. Pode reduzir drasticamente o custo de layout e pintura após interações que alteram o DOM:

/* CSS: content-visibility para secções fora do viewport */
.secao-conteudo {
  /* Omite rendering de secções fora do viewport */
  content-visibility: auto;

  /* contain-intrinsic-size evita saltos de layout ao fazer scroll */
  /* Formato: contain-intrinsic-size: largura altura */
  contain-intrinsic-size: auto 500px;
}

Dica prática: A propriedade content-visibility: auto pode oferecer ganhos substanciais sem alterar uma única linha de JavaScript. Em páginas longas com múltiplas secções, é comum observar reduções de 30-50% no tempo de rendering. Só não se esqueça de definir contain-intrinsic-size para evitar saltos de layout durante o scroll.

Técnica 5: Gerir Scripts de Terceiros

Scripts de terceiros — analytics, tag managers, widgets de chat, sistemas de A/B testing, pixels de marketing — são frequentemente os maiores vilões do INP. Um estudo recente mostrou que são responsáveis por mais de 60% das Long Tasks em sites de e-commerce típicos. Sessenta por cento. Só de scripts que nem são nossos.

Estratégias de Carregamento

A primeira linha de defesa é controlar quando e como estes scripts são carregados:

  • defer: Carrega o script em paralelo e executa-o após o parsing do HTML estar completo, na ordem do documento. Ideal para scripts que precisam do DOM mas não são urgentes.
  • async: Carrega o script em paralelo e executa-o assim que estiver disponível, sem respeitar a ordem. Bom para scripts independentes como analytics.
  • Lazy loading condicional: Carregar scripts apenas quando são necessários — por exemplo, o widget de chat só quando o utilizador clica no botão de chat, ou o script de mapa só quando a secção do mapa entra no viewport.

Partytown: Scripts de Terceiros em Web Workers

O Partytown é uma biblioteca que move scripts de terceiros para Web Workers, libertando completamente a thread principal. É particularmente eficaz para scripts de analytics e tracking que não precisam de acesso direto ao DOM (o Partytown fornece um proxy para operações de DOM que esses scripts possam tentar).

<!-- Configuração Partytown no <head> -->
<script>
  // Configurar Partytown antes de carregar
  partytown = {
    debug: false,
    forward: ['dataLayer.push', 'fbq'],
    resolveUrl: (url) => {
      if (url.hostname === 'www.googletagmanager.com') {
        return url;
      }
      return url;
    }
  };
</script>

<!-- Carregar o snippet do Partytown -->
<script src="/~partytown/partytown.js"></script>

<!-- Scripts de terceiros com type="text/partytown" -->
<!-- Em vez de executarem na thread principal, executam no Worker -->
<script type="text/partytown"
  src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX">
</script>
<script type="text/partytown">
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-XXXXX');
</script>

Dica prática: Antes de implementar soluções complexas como o Partytown, façam uma auditoria simples: desativem temporariamente cada script de terceiros e meçam o impacto no INP. Frequentemente, vão descobrir que um ou dois scripts específicos são responsáveis pela maioria dos problemas. Pode ser mais simples remover ou substituir esses scripts do que montar uma solução de proxy.

Monitorização Contínua e RUM

Otimizar o INP não é uma tarefa que se faz uma vez e se esquece — é um processo contínuo. Novos scripts, novas funcionalidades e alterações de código podem introduzir regressões a qualquer momento. Implementar monitorização contínua com Real User Monitoring (RUM) é essencial para manter os ganhos conquistados.

Configuração Completa de Monitorização com web-vitals

O exemplo seguinte demonstra uma configuração de produção que recolhe dados de INP com atribuição detalhada e os envia para o endpoint de analytics:

import { onINP, onCLS, onLCP } from 'web-vitals/attribution';

// Configuração do beacon de métricas
const ENDPOINT_METRICAS = '/api/metricas-webvitals';
const FILA_METRICAS = [];
let timerEnvio = null;

function adicionarMetrica(metrica) {
  const dados = {
    nome: metrica.name,
    valor: metrica.value,
    rating: metrica.rating,
    delta: metrica.delta,
    id: metrica.id,
    url: location.href,
    timestamp: Date.now(),
    navegador: navigator.userAgent,
    tipoConexao: navigator.connection?.effectiveType || 'desconhecido',
    dispositivoMemoria: navigator.deviceMemory || 'desconhecido'
  };

  // Adicionar dados de atribuição específicos para INP
  if (metrica.name === 'INP' && metrica.attribution) {
    const attr = metrica.attribution;
    dados.atribuicao = {
      elementoAlvo: attr.interactionTarget,
      tipoInteracao: attr.interactionType,
      inputDelay: Math.round(attr.inputDelay),
      processingDuration: Math.round(attr.processingDuration),
      presentationDelay: Math.round(attr.presentationDelay),
      scriptsLentos: attr.longAnimationFrameEntries?.[0]?.scripts?.map(s => ({
        url: s.sourceURL,
        funcao: s.invoker,
        tipo: s.invokerType,
        duracao: Math.round(s.duration)
      })) || []
    };
  }

  FILA_METRICAS.push(dados);
  agendarEnvio();
}

function agendarEnvio() {
  if (timerEnvio) clearTimeout(timerEnvio);

  timerEnvio = setTimeout(() => {
    if (FILA_METRICAS.length === 0) return;

    const metricas = [...FILA_METRICAS];
    FILA_METRICAS.length = 0;

    if (navigator.sendBeacon) {
      const blob = new Blob(
        [JSON.stringify(metricas)],
        { type: 'application/json' }
      );
      navigator.sendBeacon(ENDPOINT_METRICAS, blob);
    } else {
      fetch(ENDPOINT_METRICAS, {
        method: 'POST',
        body: JSON.stringify(metricas),
        headers: { 'Content-Type': 'application/json' },
        keepalive: true
      }).catch(() => {});
    }
  }, 1000);
}

// Registar observadores de métricas
onINP(adicionarMetrica, { reportAllChanges: true });
onCLS(adicionarMetrica);
onLCP(adicionarMetrica);

// Garantir envio ao fechar a página
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    clearTimeout(timerEnvio);
    if (FILA_METRICAS.length > 0) {
      const blob = new Blob(
        [JSON.stringify(FILA_METRICAS)],
        { type: 'application/json' }
      );
      navigator.sendBeacon(ENDPOINT_METRICAS, blob);
      FILA_METRICAS.length = 0;
    }
  }
});

Utilizar Dados do CrUX

O Chrome User Experience Report (CrUX) é a fonte oficial de dados de campo que a Google utiliza para o ranking. Pode aceder aos dados via CrUX API, BigQuery, ou PageSpeed Insights. Os dados são agregados em períodos de 28 dias, o que significa que melhorias implementadas hoje só serão visíveis no CrUX após aproximadamente um mês.

Para obter dados mais imediatos, combine o CrUX com o seu sistema de RUM próprio. O CrUX serve como validação oficial; o RUM permite detetar regressões em minutos.

Criar Alertas para Regressões de INP

Com os dados de RUM a fluir para o sistema de analytics, configure alertas automáticos:

  • Alerta de degradação: Se o P75 de INP ultrapassar 200ms durante mais de 2 horas consecutivas, notificar a equipa.
  • Alerta de regressão aguda: Se o P75 de INP aumentar mais de 50% face à média dos últimos 7 dias, notificar imediatamente.
  • Alerta por página: Monitorizar as páginas mais críticas individualmente (homepage, páginas de produto, checkout).
  • Alerta por segmento: Segmentar por dispositivo (mobile vs desktop) e tipo de conexão. Problemas de INP afetam desproporcionalmente dispositivos móveis de gama média — é sempre aí que os problemas aparecem primeiro.

Dica prática: Integrem a monitorização de INP no pipeline de CI/CD. Ferramentas como o Lighthouse CI podem bloquear deploys que degradem significativamente as métricas de performance. Complementar com dados de RUM pós-deploy garante que regressões são detetadas mesmo quando não aparecem em testes de laboratório.

Resultados Reais: O Impacto do INP no Negócio

A otimização do INP não é apenas um exercício técnico — traduz-se diretamente em resultados de negócio mensuráveis. E aqui é onde a conversa fica realmente interessante.

Casos de Estudo Documentados

O caso mais emblemático é o da redBus. Após um trabalho focado de otimização do INP, registaram um aumento de 7% nas vendas de bilhetes. A equipa focou-se em dividir Long Tasks na página de resultados de pesquisa e em adiar o carregamento de scripts de analytics não essenciais. O investimento? Aproximadamente duas semanas de trabalho de um engenheiro senior.

No setor bancário, uma instituição financeira europeia que implementou as técnicas deste guia viu a percentagem de páginas com "bom" INP saltar de 55% para 87%. As melhorias mais significativas vieram da área de internet banking, onde formulários complexos e validações em tempo real causavam Long Tasks frequentes. A solução passou por mover validações pesadas para Web Workers e usar useTransition em componentes React de formulário.

Uma plataforma de e-commerce de média dimensão conseguiu reduzir o INP para 50ms (bem abaixo do limiar de 200ms) combinando virtualização de listas de produtos, lazy loading de widgets de terceiros, e content-visibility: auto nas secções abaixo do fold.

O Retorno Sobre Investimento

A experiência consistente de múltiplas equipas indica que melhorias de 30-50% no INP são alcançáveis com cerca de 8 horas de trabalho focado. As ações com maior ROI, por ordem: eliminar ou adiar scripts de terceiros desnecessários, dividir as Long Tasks mais longas com scheduler.yield(), e implementar content-visibility: auto em páginas longas.

Um projeto particularmente bem-sucedido reportou uma redução de 90% no INP (de 800ms para 80ms) ao mover um cálculo pesado de preços dinâmicos de um componente React para um Web Worker. Isso é o tipo de resultado que convence qualquer stakeholder.

Conclusão

O INP trouxe uma nova dimensão de exigência à performance web. Já não basta que a primeira interação seja rápida — todas as interações devem ser responsivas. A boa notícia? Com as técnicas certas, melhorias substanciais são alcançáveis de forma relativamente rápida.

Resumo das Técnicas Essenciais

Recapitulemos as cinco técnicas-chave:

  1. Dividir Long Tasks com scheduler.yield() — a primeira linha de defesa contra input delay elevado.
  2. Mover trabalho pesado para Web Workers — para computações que não precisam de acesso ao DOM.
  3. Otimizações específicas de frameworksuseTransition no React, v-memo no Vue, @defer no Angular.
  4. Reduzir o presentation delay — minimizar o DOM, evitar layout thrashing, usar content-visibility: auto.
  5. Gerir scripts de terceiros — adiar, lazy load, ou mover para Web Workers com Partytown.

Estratégia de Priorização

Se o tempo é limitado, comecem sempre pelo diagnóstico: implementem a recolha de dados com a biblioteca web-vitals com atribuição, identifiquem a fase problemática e os elementos envolvidos. Depois, priorizem pela ordem de impacto típica: scripts de terceiros > Long Tasks no código próprio > otimizações de framework > presentation delay.

Vale a pena notar que, enquanto este artigo se foca na responsividade das interações dentro da página, a velocidade de navegação entre páginas é igualmente crítica. Técnicas como as Speculation Rules API, que permitem pré-renderizar páginas antes do utilizador clicar, complementam perfeitamente o trabalho de otimização do INP. Juntas, estas duas frentes — responsividade intra-página e velocidade de navegação — definem a experiência de performance percebida pelo utilizador.

O INP é uma métrica que recompensa a atenção ao detalhe e o cuidado contínuo. Implementem as técnicas deste guia, monitorizem os resultados, e iterem. A responsividade não é um destino — é um compromisso permanente com a qualidade da experiência que entregam aos vossos utilizadores.

Sobre o Autor Editorial Team

Our team of expert writers and editors.