Agendando eventos do dataLayer para otimizar o INP

Como adiar eventos do GTM para depois da renderização do navegador melhora suas pontuações de INP

Arjen Karel Core Web Vitals Consultant
Arjen Karel - linkedin
Last update: 2026-09-28

TL;DR: Melhorando o INP ao adiar o dataLayer.push()

Quando o dataLayer.push() dispara de forma síncrona em um manipulador de eventos, o GTM processa as tags antes do navegador renderizar o feedback visual. Isso bloqueia a main thread e infla o seu Interaction to Next Paint (INP). A correção: agende o dataLayer.push() para rodar após o navegador pintar o próximo frame usando o requestAnimationFrame. Esse padrão corta de 20 a 100 ms do INP na maioria dos sites. Ele frequentemente transforma uma nota ruim em aprovação. A coleta de dados é atrasada em 50 a 250 ms. Isso é irrelevante para analytics e rastreamento de marketing.

Como o dataLayer.push() prejudica seu INP

Em um de nossos clientes, medimos uma redução de 100 ms no Interaction to Next Paint (INP) ao reagendar quando o dataLayer.push() executa após a interação do usuário. A correção é um substituto direto em JavaScript que prioriza a renderização sobre o rastreamento.

Última revisão por Arjen Karel em março de 2026

O problema com o dataLayer.push() síncrono

Se você trabalha com o Google Tag Manager, conhece o dataLayer.push(). É o método padrão para enviar eventos ao dataLayer. Ele está profundamente integrado à maioria das funcionalidades dos sites. O problema é que ninguém questiona quando ele executa.

Quando o dataLayer.push() é chamado diretamente dentro de um manipulador de eventos (o clique de um botão, o envio de um formulário, a abertura de um menu), ele roda de forma síncrona. As tags do GTM configuradas para disparar nesse evento também executam imediatamente. Isso bloqueia a main thread antes que o navegador possa atualizar o layout. O visitante clica em um botão e não vê nada acontecer enquanto o GTM processa os scripts de rastreamento em segundo plano. Esse atraso é o seu atraso de apresentação. Ele é o principal responsável por um INP ruim.

O trace de performance abaixo, de um grande site de notícias, mostra a atividade do GTM após uma interação do usuário. As tarefas do GTM levaram aproximadamente 90 ms para executar. Isso empurrou o INP total para 263 ms. Essa interação reprova nos Core Web Vitals.

A correção: pinte primeiro, faça o push depois

A solução segue a diretriz de otimização de INP do web.dev: faça o yield para a main thread para que o navegador possa pintar antes de rodar código não crítico. Em vez de executar o rastreamento de forma síncrona no manipulador de eventos:

  1. Execute o código da atualização visual imediatamente.
  2. Deixe o navegador pintar.
  3. Então faça o push para o dataLayer.

Aqui está a mesma interação após aplicar esse padrão. A única mudança é que o dataLayer.push() agora roda após o navegador pintar o próximo frame.

A interação que antes reprovava agora passa com folga. Todos os dados ainda chegam ao dataLayer. A diferença é que os scripts do GTM executam após o navegador responder à ação do usuário, não antes.

O helper awaitPaint

Esse helper usa o requestAnimationFrame para agendar um callback após o navegador pintar. O setTimeout aninhado garante que a função rode após a pintura terminar, e não apenas antes dela. Esse é o padrão recomendado pelo web.dev para adiar trabalho não crítico após uma interação.

async function awaitPaint(fn) {
    await new Promise((resolve) => {
        // Fallback: garante que não vamos travar se o RAF não disparar (ex: abas em segundo plano)
        setTimeout(resolve, 200);

        // Agenda para depois da próxima pintura
        requestAnimationFrame(() => {
            setTimeout(resolve, 50);
        });
    });

    if (typeof fn === 'function') {
        fn();
    }
}

Usando o helper

Envolva qualquer chamada do dataLayer.push() no awaitPaint para adiá-la até depois da pintura:

function pushToDataLayer(event, data) {
    window.dataLayer = window.dataLayer || [];

    awaitPaint(() => {
        window.dataLayer.push({
            event,
            ...data,
            timestamp: new Date().toISOString()
        });
    });
}

// Uso
document.querySelector('.buy-button').addEventListener('click', () => {
    // O feedback visual acontece imediatamente
    showLoadingSpinner();

    // O rastreamento dispara após a pintura
    pushToDataLayer('purchase_click', { productId: '123' });
});

Sobrescrita global para testes rápidos

Para testar esse padrão em todo o seu site sem refatorar cada chamada do dataLayer.push(), você pode sobrescrever a função push globalmente. Coloque este script no <head> imediatamente após o snippet do contêiner do GTM.

<script type="module">
    window.dataLayer = window.dataLayer || [];

    async function awaitPaint(fn) {
        await new Promise((resolve) => {
            // Fallback: garante que não vamos travar se o RAF não disparar (ex: abas em segundo plano)
            setTimeout(resolve, 200);

            // Agenda para depois da próxima pintura
            requestAnimationFrame(() => {
                setTimeout(resolve, 50);
            });
        });

        if (typeof fn === 'function') {
            fn();
        }
    }

    if (window.dataLayer && typeof window.dataLayer.push === 'function') {
        const originalPush = window.dataLayer.push.bind(window.dataLayer);

        window.dataLayer.push = function (...args) {
            awaitPaint(() => {
                originalPush(...args);
            });
        };
    }
</script>

Isso sobrescreve todos os dataLayer.push() da página. Todos os gatilhos de evento do GTM existentes, chamadas de rastreamento hardcoded e integrações de plugins se beneficiam automaticamente sem nenhuma mudança no código.

Por que isso melhora o INP

O INP mede o tempo desde a interação do usuário até o navegador pintar a resposta visual. Se você executa o processamento de eventos do GTM de forma síncrona em um manipulador de eventos, você bloqueia a main thread e impede a renderização. O Web Almanac 2024 constatou que o atraso de apresentação é o maior contribuinte para um INP ruim na mediana. Os scripts de rastreamento estão entre as principais causas.

Os números confirmam isso. O Web Almanac 2025 mostra que 77% das páginas mobile passam no INP em geral. Porém, apenas 63% dos 1.000 sites mais visitados conseguem isso. Esses sites de ponta carregam mais scripts de terceiros. No Subito (o maior marketplace de classificados da Itália), desativar um único script de rastreamento do TikTok carregado via GTM derrubou o INP de 208 ms para cerca de 170 ms. Um script, 38 ms economizados.

Ao adiar o dataLayer.push() para depois da pintura, você tira todo o processamento do GTM do caminho crítico da interação. O usuário recebe feedback visual imediatamente. O GTM ainda dispara, apenas 50 a 250 ms depois.

Por que não um atraso fixo, Idle Callback ou scheduler.yield()?

  • setTimeout(delay) fixo: Um atraso hardcoded (ex: setTimeout(..., 100)) é tentar adivinhar quando a renderização vai terminar. Muito longo e você atrasa o rastreamento desnecessariamente. Muito curto e você ainda bloqueia a pintura.
  • requestIdleCallback: Agenda o trabalho para quando o navegador estiver ocioso. Mas não garante a execução imediata após uma interação específica. O callback pode rodar muito mais tarde, ou nem rodar antes do usuário sair da página.
  • scheduler.yield(): A API moderna para fazer o yielding para a main thread. Ela preserva a prioridade da tarefa (ao contrário do setTimeout, que manda sua continuação para o fim da fila). No entanto, o scheduler.yield() não garante que uma pintura ocorreu antes do seu código rodar. Para esse caso de uso específico, o requestAnimationFrame é a melhor ferramenta porque está atrelado ao ciclo de vida da renderização. Note que o scheduler.yield() ainda não tem suporte no Safari.

A combinação requestAnimationFrame + setTimeout é projetada especificamente para este cenário. O requestAnimationFrame dispara logo antes do navegador pintar. O setTimeout aninhado cria uma nova tarefa que roda após essa pintura terminar. Juntos, eles garantem que seu código adiado execute após a atualização visual. Isso é exatamente o que o INP mede.

Trade-offs

  • Microatraso na coleta de dados: Os eventos chegam ao GTM 50 a 250 ms mais tarde do que com o push síncrono. Para analytics e rastreamento de marketing, isso é irrelevante.
  • Perda de dados em saídas rápidas: Se um visitante dispara um evento e sai da página durante a janela de atraso, esse evento pode não disparar. Para eventos críticos antes de um redirecionamento ou descarregamento da página, use o navigator.sendBeacon() em vez do dataLayer.push(). Veja o argumento para limitar scripts de analytics para saber mais sobre a Beacon API.

Para a maioria dos rastreamentos, o ganho no INP supera de longe o atraso mínimo. Se você precisa de precisão de milissegundos no log de eventos (real-time bidding, dashboards financeiros), essa abordagem não é adequada. Para todo o resto, é uma vitória clara.

Após aplicar a sobrescrita, verifique a melhoria com Real User Monitoring. Testes de laboratório vão mostrar a diferença. Porém, o field data de usuários reais em redes reais é o que conta para as suas pontuações nos Core Web Vitals.

About the author

Arjen Karel is a web performance consultant and the creator of CoreDash, a Real User Monitoring platform that tracks Core Web Vitals data across hundreds of sites. He also built the Core Web Vitals Visualizer Chrome extension. He has helped clients achieve passing Core Web Vitals scores on over 925,000 mobile URLs.

O score do Lighthouse não conta a história toda.

Os teus visitantes estão em Android sobre 4G. Eu analiso o que eles apanham na prática.

Analisar dados reais
Agendando eventos do dataLayer para otimizar o INP Core Web Vitals Agendando eventos do dataLayer para otimizar o INP