Capa do Ebook gratuito Performance Front-End: Otimizando Core Web Vitals sem Mistério

Performance Front-End: Otimizando Core Web Vitals sem Mistério

Novo curso

19 páginas

Gestão de JavaScript para reduzir INP: tarefas longas, event handlers e scheduling

Capítulo 11

Tempo estimado de leitura: 14 minutos

+ Exercício

INP (Interaction to Next Paint) mede a responsividade percebida: quanto tempo o usuário espera entre interagir (clique, toque, tecla) e ver a próxima atualização visual relevante. Na prática, o INP piora quando o thread principal está ocupado com JavaScript (ou com trabalho de layout/paint disparado por JS) e não consegue atender rapidamente o evento, executar o handler e produzir um novo frame. Este capítulo foca em gestão de JavaScript para reduzir INP por meio de três frentes: reduzir tarefas longas, tornar event handlers mais leves e aplicar scheduling (agendamento) para distribuir trabalho sem bloquear a interação.

O que realmente “entra” no INP: do evento ao próximo paint

Uma interação típica no navegador passa por etapas que competem pelo mesmo recurso: o thread principal. Um fluxo simplificado é:

  • O evento é enfileirado (por exemplo, pointerdown, click, keydown).
  • O navegador executa o(s) event handler(s) registrado(s).
  • O handler pode disparar trabalho adicional: atualizações de estado, manipulação de DOM, cálculos, requisições, timers, etc.
  • O navegador precisa recalcular estilos/layout (se necessário) e pintar o próximo frame.

O INP tende a ser dominado por duas causas: (1) o evento demora a ser atendido porque há trabalho anterior bloqueando o thread principal (fila de tarefas), e/ou (2) o handler e o trabalho subsequente demoram tanto que o próximo paint só acontece tarde. Portanto, reduzir INP é, em grande parte, reduzir “tempo de bloqueio” do thread principal no momento em que o usuário interage.

Tarefas longas: por que elas destroem a responsividade

Uma tarefa longa é um trecho de execução no thread principal que dura tempo suficiente para impedir o navegador de responder rapidamente (por exemplo, impedindo que ele processe eventos e renderize frames). Mesmo sem repetir conceitos já vistos, a implicação prática aqui é: se você tem blocos de JS que rodam por dezenas de milissegundos (ou mais), qualquer interação que ocorra durante esse período ficará esperando.

Fontes comuns de tarefas longas em apps reais

  • Processamento pesado em loops (filtragem, ordenação, parsing de JSON grande, normalização de dados).
  • Renderização de listas grandes com DOM “cru” (muitos nós criados/atualizados de uma vez).
  • Validações complexas em tempo real (por tecla) sem debounce/throttle.
  • Handlers que fazem trabalho “demais”: analytics síncrono, manipulação de classes em muitos elementos, medições de layout repetidas.
  • Inicializações de bibliotecas no momento errado (por exemplo, ao primeiro clique do usuário).

Estratégia 1: reduzir e fatiar trabalho (chunking) para evitar bloqueios

Quando um processamento é inevitável, a meta é dividir em pedaços menores e permitir que o navegador “respire” entre eles, processando input e renderizando. Isso reduz o tempo máximo contínuo de bloqueio, melhorando a chance de a interação ser atendida rapidamente.

Continue em nosso aplicativo

Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.

ou continue lendo abaixo...
Download App

Baixar o aplicativo

Passo a passo: fatiando um loop pesado com yield para o navegador

Suponha que você precise processar 50 mil itens após uma interação (ex.: aplicar filtros). Em vez de fazer tudo dentro do handler, você pode:

  • 1) Capturar a intenção do usuário no handler (rápido).
  • 2) Agendar o processamento em lotes.
  • 3) Entre lotes, devolver o controle ao navegador para permitir paint e input.
// Utilitário: aguarda o próximo frame (bom para liberar o main thread e permitir paint/input)  function nextFrame() {   return new Promise(resolve => requestAnimationFrame(() => resolve())); }  async function processInChunks(items, chunkSize, onChunk) {   for (let i = 0; i < items.length; i += chunkSize) {     const chunk = items.slice(i, i + chunkSize);     onChunk(chunk);     // Devolve o controle ao navegador a cada lote     await nextFrame();   } }  // Handler leve: apenas dispara o trabalho async  button.addEventListener('click', () => {   startFiltering(); });  async function startFiltering() {   const items = getItems();   const result = [];    await processInChunks(items, 500, (chunk) => {     for (const item of chunk) {       if (matchesFilter(item)) result.push(item);     }   });    renderResults(result); }

O ponto-chave é que o handler não fica preso em um loop gigante. Ele inicia um fluxo assíncrono que processa em lotes, permitindo que o navegador intercale frames e eventos.

Quando usar requestAnimationFrame vs setTimeout(0)

  • requestAnimationFrame é útil quando você quer cooperar com o ciclo de renderização (permitir paint) e manter a UI fluida.
  • setTimeout(0) (ou pequeno) também “quebra” tarefas, mas não se alinha tão bem ao frame; pode introduzir jitter e atrasos desnecessários.

Se o objetivo é responsividade visual, requestAnimationFrame costuma ser uma escolha melhor para yields frequentes. Para trabalho que não depende de renderização, você pode combinar estratégias (ver scheduling adiante).

Estratégia 2: event handlers rápidos e previsíveis

O handler é o “caminho quente” da interação. Mesmo que você faça chunking depois, se o handler inicial faz trabalho pesado, o INP sofre. A regra prática: o handler deve fazer o mínimo necessário para registrar a intenção do usuário e disparar trabalho fora do caminho crítico.

Checklist do que evitar dentro do handler

  • Evitar loops grandes e processamento de dados.
  • Evitar leituras e escritas de layout intercaladas (podem causar reflows repetidos).
  • Evitar manipular muitos nós do DOM de uma vez.
  • Evitar chamadas síncronas a APIs que podem bloquear (ex.: serializações pesadas, parsing grande).
  • Evitar inicializar bibliotecas inteiras no clique.

Passo a passo: transformar um handler “pesado” em leve

Exemplo: um clique abre um painel e também recalcula estatísticas de uma lista grande. Versão problemática (tudo no handler):

button.addEventListener('click', () => {   panel.classList.add('open');    // Trabalho pesado no caminho crítico   const stats = computeStats(hugeArray);   renderStats(stats); });

Versão melhor: primeiro atualize a UI (feedback imediato), depois agende o pesado:

button.addEventListener('click', () => {   panel.classList.add('open');   // Feedback imediato: o usuário vê o painel abrir   scheduleStatsComputation(); });  function scheduleStatsComputation() {   // Adia para depois do paint do painel   requestAnimationFrame(() => {     const stats = computeStats(hugeArray);     renderStats(stats);   }); }

Se computeStats ainda for pesado, combine com chunking ou mova para Web Worker (ver abaixo).

Delegação de eventos para reduzir overhead

Em interfaces com muitos itens (listas, grids), adicionar listeners em cada item aumenta custo de memória e pode aumentar trabalho por interação (especialmente se há lógica repetida). Delegação reduz isso: um listener no contêiner decide o alvo real via event.target e closest.

const list = document.querySelector('.list');  list.addEventListener('click', (e) => {   const item = e.target.closest('[data-item-id]');   if (!item) return;    const id = item.dataset.itemId;   selectItem(id); });

Além de reduzir listeners, isso facilita manter handlers consistentes e enxutos.

Estratégia 3: scheduling (agendamento) para priorizar input e renderização

Scheduling é a prática de decidir quando executar trabalho JavaScript para não competir com input e paint. Em vez de “executar assim que possível”, você escolhe janelas mais seguras: após o próximo frame, em períodos ociosos, ou com prioridade baixa.

Separando trabalho em: imediato, próximo frame e “quando der”

  • Imediato: mudanças mínimas para feedback instantâneo (ex.: adicionar classe de estado, mostrar spinner, desabilitar botão).
  • Próximo frame: trabalho necessário para a UI logo em seguida, mas que pode esperar um paint (ex.: medir dimensões após abrir um painel).
  • Quando der: pré-processamento, prefetch, cálculos não críticos, aquecimento de cache, indexação local.

Usando requestIdleCallback (com fallback) para trabalho não crítico

requestIdleCallback tenta rodar quando o navegador está ocioso. É útil para tarefas que não precisam acontecer imediatamente. Como nem todos os ambientes suportam, use fallback.

function runWhenIdle(fn, timeout = 1000) {   if ('requestIdleCallback' in window) {     requestIdleCallback((deadline) => {       fn(deadline);     }, { timeout });   } else {     setTimeout(() => fn({ timeRemaining: () => 0 }), 0);   } }  // Exemplo: preparar um índice de busca local sem afetar INP  runWhenIdle(() => {   buildSearchIndex(largeDataset); });

Boa prática: dentro do callback, respeitar deadline.timeRemaining() e fatiar o trabalho se necessário, para não transformar o “idle” em bloqueio.

Scheduler API (quando disponível) para prioridades

Alguns navegadores oferecem scheduler.postTask para enfileirar tarefas com prioridade (por exemplo, user-blocking, user-visible, background). Quando disponível, você pode empurrar trabalho não crítico para background e manter o caminho de interação livre.

function postTask(fn, priority = 'user-visible') {   if ('scheduler' in window && 'postTask' in window.scheduler) {     return window.scheduler.postTask(fn, { priority });   }   // Fallback simples   return Promise.resolve().then(fn); }  // Exemplo: após clique, faça o essencial e jogue o resto para background  button.addEventListener('click', () => {   panel.classList.add('open');   postTask(() => warmUpRecommendations(), 'background'); });

Mesmo com fallback, a ideia permanece: separar o que é crítico para a resposta imediata do que pode esperar.

Estratégia 4: mover CPU pesada para Web Workers

Se o gargalo é CPU (cálculos, parsing, compressão, ranking, etc.), Web Workers tiram trabalho do thread principal. Isso não elimina todo custo (há serialização/mensageria), mas costuma ser uma das formas mais eficazes de proteger INP quando há processamento inevitável.

Passo a passo: extraindo um cálculo para um Worker

  • 1) Identifique uma função pura (entrada → saída) que consome CPU.
  • 2) Crie um arquivo de worker que recebe mensagem, processa e responde.
  • 3) No main thread, o handler dispara o worker e atualiza UI quando a resposta chegar.
// worker.js  self.onmessage = (e) => {   const { items } = e.data;   const result = heavyCompute(items);   self.postMessage({ result }); };  function heavyCompute(items) {   // Simulação de CPU pesada   let sum = 0;   for (const n of items) sum += Math.sqrt(n);   return sum; }
// main.js  const worker = new Worker('/worker.js');  function computeAsync(items) {   return new Promise((resolve) => {     worker.onmessage = (e) => resolve(e.data.result);     worker.postMessage({ items });   }); }  button.addEventListener('click', async () => {   panel.classList.add('open');   showLoading(true);    const result = await computeAsync(bigNumbers);   showLoading(false);   renderResult(result); });

Para dados grandes, prefira estruturas transferíveis (quando aplicável) para reduzir cópia. Mesmo sem entrar em detalhes avançados, a regra é: quanto menos bytes você manda/recebe, melhor para a responsividade.

Estratégia 5: evitar “layout thrashing” dentro de interações

Muitas interações disparam leituras e escritas no DOM. Se você alterna leitura de layout (ex.: getBoundingClientRect, offsetWidth) com escrita (ex.: mudar classes/estilos) repetidamente, o navegador pode recalcular layout várias vezes, aumentando o tempo até o próximo paint.

Passo a passo: agrupar leituras e depois escritas

Versão problemática:

items.forEach((el) => {   const w = el.getBoundingClientRect().width;   el.style.width = (w + 10) + 'px'; });

Versão melhor: primeiro leia tudo, depois escreva:

const widths = items.map(el => el.getBoundingClientRect().width); widths.forEach((w, i) => {   items[i].style.width = (w + 10) + 'px'; });

Em interações frequentes (scroll, drag, typeahead), essa disciplina reduz picos de tempo no handler e ajuda o INP.

Estratégia 6: controlar frequência de handlers (debounce e throttle)

Nem toda interação é um clique isolado. Eventos como input, pointermove, scroll e resize podem disparar dezenas de vezes por segundo. Se cada disparo faz trabalho relevante, você cria uma fila de tarefas que degrada INP para outras interações.

Debounce para “esperar o usuário parar”

function debounce(fn, delay) {   let t;   return (...args) => {     clearTimeout(t);     t = setTimeout(() => fn(...args), delay);   }; }  const onSearch = debounce((value) => {   runSearch(value); }, 200);  input.addEventListener('input', (e) => {   onSearch(e.target.value); });

Throttle para limitar taxa (ex.: 1 vez a cada 100ms)

function throttle(fn, interval) {   let last = 0;   let pending = null;   return (...args) => {     const now = performance.now();     const remaining = interval - (now - last);     if (remaining <= 0) {       last = now;       fn(...args);     } else if (!pending) {       pending = setTimeout(() => {         pending = null;         last = performance.now();         fn(...args);       }, remaining);     }   }; }  window.addEventListener('resize', throttle(() => {   recalcLayout(); }, 150));

Use debounce quando o resultado só faz sentido após uma pausa (busca, validação). Use throttle quando você precisa atualizar continuamente, mas com limite (resize, scroll-driven effects).

Estratégia 7: reduzir trabalho por interação com cache e memoização

Se o mesmo cálculo acontece repetidamente em interações semelhantes, cache pode reduzir custo no caminho crítico. O cuidado é invalidar corretamente quando as entradas mudam.

Exemplo: memoização simples para formatação/cálculo repetido

function memoize(fn) {   const cache = new Map();   return (key) => {     if (cache.has(key)) return cache.get(key);     const val = fn(key);     cache.set(key, val);     return val;   }; }  const formatPrice = memoize((cents) => {   // Simule algo mais caro do que parece (i18n, regras, etc.)   return (cents / 100).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }); });  function renderRow(row) {   rowEl.textContent = formatPrice(row.priceCents); }

Isso não substitui scheduling, mas reduz o tempo de execução do handler e do render subsequente.

Roteiro prático: atacando INP a partir do JavaScript (sem repetir diagnóstico)

Um plano de ação objetivo para reduzir INP via JS, aplicável em projetos com UI rica:

1) Classifique interações críticas

Liste as interações mais frequentes e mais importantes (ex.: abrir menu, adicionar ao carrinho, digitar no campo de busca, trocar filtros). O objetivo é garantir que essas interações tenham handlers leves e previsíveis.

2) Faça o handler responder primeiro, trabalhar depois

  • Garanta feedback imediato (classe de estado, spinner, highlight).
  • Adie cálculos e renderizações pesadas para o próximo frame ou para idle.
  • Se o trabalho é grande, use chunking.

3) Quebre tarefas inevitáveis

  • Divida loops em lotes com yields (ex.: requestAnimationFrame).
  • Evite “um único bloco” que monopoliza o thread principal.

4) Tire CPU pesada do main thread

  • Extraia funções puras para Web Workers.
  • Envie apenas dados necessários (evite objetos enormes quando possível).

5) Controle a frequência de eventos de alta cadência

  • Debounce em input e validações.
  • Throttle em resize e atualizações contínuas.

6) Revise padrões que causam reflow repetido

  • Agrupe leituras e escritas de layout.
  • Evite medir layout dentro de loops de atualização de DOM.

Exemplo integrado: filtro de lista com UI responsiva

Este exemplo combina as técnicas: handler leve, debounce, chunking e render incremental. Cenário: um campo de busca filtra uma lista grande e atualiza o DOM.

const input = document.querySelector('#q'); const listEl = document.querySelector('#results'); let lastQuery = '';  function debounce(fn, delay) {   let t;   return (...args) => {     clearTimeout(t);     t = setTimeout(() => fn(...args), delay);   }; }  function nextFrame() {   return new Promise(resolve => requestAnimationFrame(() => resolve())); }  function renderBatch(items) {   const frag = document.createDocumentFragment();   for (const item of items) {     const li = document.createElement('li');     li.textContent = item.title;     frag.appendChild(li);   }   listEl.appendChild(frag); }  async function filterAndRender(query) {   // Limpa e dá feedback rápido   listEl.textContent = '';   listEl.classList.add('loading');    const data = window.__BIG_LIST__;   const matches = [];    // Filtra em chunks para não bloquear   const chunkSize = 400;   for (let i = 0; i < data.length; i += chunkSize) {     // Se o usuário digitou algo novo, abandone este trabalho (evita fila)     if (query !== lastQuery) return;      const chunk = data.slice(i, i + chunkSize);     for (const item of chunk) {       if (item.title.toLowerCase().includes(query)) matches.push(item);     }      // Render incremental para o usuário ver progresso     renderBatch(matches.splice(0, 50));      await nextFrame();   }    listEl.classList.remove('loading'); }  const onInput = debounce((value) => {   lastQuery = value.toLowerCase().trim();   // Handler efetivo é leve: só dispara o fluxo async   filterAndRender(lastQuery); }, 150);  input.addEventListener('input', (e) => {   onInput(e.target.value); });

Detalhes importantes para INP:

  • O evento input é debounced, reduzindo chamadas.
  • O processamento é chunked com await nextFrame(), evitando bloqueio contínuo.
  • Há cancelamento por comparação com lastQuery, impedindo que trabalho antigo acumule e atrase interações novas.
  • Render ocorre em lotes com DocumentFragment, reduzindo custo por atualização.

Armadilhas comuns que pioram INP sem você perceber

Inicializar tudo no primeiro clique

Um padrão frequente é adiar a carga de uma feature para “quando o usuário usar”, mas então inicializar uma biblioteca inteira dentro do handler (ex.: editor, mapa, gráficos). Isso cria um pico enorme exatamente no momento da interação. Prefira pré-aquecer em idle (scheduling) ou inicializar incrementalmente antes do usuário precisar.

Promessas e microtasks em excesso

Encadear muitas Promise.then pode criar uma sequência de microtasks que roda antes do navegador voltar a renderizar, atrasando o próximo paint. Se você tem pipelines longos de microtasks, considere inserir yields explícitos (por exemplo, requestAnimationFrame) em pontos estratégicos.

Handlers que fazem “medição + mutação + medição”

Esse padrão costuma causar recalculações repetidas. Reestruture para medir uma vez, aplicar mudanças, e só medir novamente no próximo frame se realmente necessário.

Agora responda o exercício sobre o conteúdo:

Ao tentar reduzir o INP, qual abordagem mais ajuda a manter a interação responsiva quando existe processamento pesado após um clique?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

O INP piora quando o thread principal fica bloqueado. Um handler leve dá resposta imediata e o trabalho pesado deve ser fatiado e agendado com yields (ex.: em frames) para permitir input e paint entre os lotes.

Próximo capitúlo

Divisão de bundles e carregamento sob demanda com code splitting

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.