Agendar eventos do dataLayer para otimizar o INP

Como adiar eventos do GTM para depois do paint do navegador melhora a pontuação do INP

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

TL;DR: como melhorar o INP adiando o dataLayer.push()

Quando o dataLayer.push() dispara de forma síncrona dentro de um event handler, o GTM processa as tags antes que o navegador possa pintar o feedback visual. Isso bloqueia a main thread e infla o Interaction to Next Paint (INP). A solução: agende o dataLayer.push() para rodar após o navegador pintar o próximo frame usando requestAnimationFrame. Esse padrão reduz de 20 a 100ms do INP na maioria dos sites, muitas vezes transformando uma pontuação reprovada em aprovada. A coleta de dados sofre um atraso de 50 a 250ms, o que é irrelevante para analytics e rastreamento de marketing.

Como o dataLayer.push() prejudica o INP

Em um dos nossos clientes, medimos uma redução de 100ms no Interaction to Next Paint (INP) apenas reagendando a execução do dataLayer.push() após a interação do usuário. A solução é um drop-in replacement em JavaScript que prioriza a renderização em vez do rastreamento.

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

O problema do dataLayer.push() síncrono

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

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

O trace de performance abaixo, de um grande portal de notícias, mostra a atividade do GTM após uma interação do usuário. As tarefas do GTM levaram cerca de 90ms para rodar, elevando o INP total para 263ms. Essa interação reprova no Core Web Vitals.

A solução: primeiro o paint, depois o push

A solução segue o guia de otimização de INP do web.dev: faça o yield to the main thread para que o navegador pinte a tela antes de rodar código não crítico. Em vez de executar o rastreamento de forma síncrona dentro do event handler:

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

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

A interação que antes reprovava agora passa com folga. Todos os dados continuam chegando ao dataLayer. A diferença é que os scripts do GTM executam depois que o navegador responde à ação do usuário, e não antes.

O helper awaitPaint

Esse helper usa requestAnimationFrame para agendar um callback após o navegador pintar a tela. O setTimeout aninhado garante que a função execute após o término do paint, e não logo antes. Esse é o padrão recomendado pelo web.dev para adiar tarefas não críticas após uma interação.

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

        // Agenda para depois do próximo paint
        requestAnimationFrame(() => {
            setTimeout(resolve, 50);
        });
    });

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

Como usar o helper

Envolva qualquer chamada de dataLayer.push() no awaitPaint para adiá-la para depois do paint:

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 o paint
    pushToDataLayer('purchase_click', { productId: '123' });
});

Override global para testes rápidos

Para testar esse padrão no site inteiro sem refatorar cada chamada de dataLayer.push(), você pode sobrescrever a função push globalmente. Coloque este script no <head> logo após o snippet do container do GTM.

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

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

            // Agenda para depois do próximo paint
            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 todo dataLayer.push() da página. Todos os triggers existentes do GTM, chamadas de rastreamento hardcoded e integrações de plugins se beneficiam automaticamente, sem nenhuma alteração 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 na tela. Se você executa o processamento de eventos do GTM de forma síncrona dentro de um event handler, bloqueia a main thread e impede a renderização. O Web Almanac de 2024 constatou que o presentation delay é o maior responsável por um INP ruim na mediana, e scripts de rastreamento estão entre as principais causas.

Os números confirmam isso. O Web Almanac de 2025 mostra que 77% das páginas no celular passam no INP no geral, mas apenas 63% dos 1.000 sites mais acessados conseguem aprovação. Esses sites do topo carregam mais scripts de terceiros. Na Subito (o maior portal de classificados da Itália), desativar um único script de rastreamento do TikTok carregado via GTM reduziu o INP de 208ms para cerca de 170ms. Um único script, 38ms economizados.

Ao adiar o dataLayer.push() para depois do paint, você remove todo o processamento do GTM do caminho crítico da interação. O usuário recebe o feedback visual imediatamente. O GTM continua disparando, apenas 50 a 250ms mais tarde.

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

  • setTimeout(delay) fixo: Um delay hardcoded (ex.: setTimeout(..., 100)) é tentar adivinhar quando a renderização vai terminar. Se for longo demais, atrasa o rastreamento sem necessidade. Se for curto demais, continua bloqueando o paint.
  • requestIdleCallback: Agenda tarefas para quando o navegador estiver ocioso, mas não garante a execução logo após uma interação específica. O callback pode rodar muito mais tarde — ou nem rodar antes de o usuário sair da página.
  • scheduler.yield(): A API moderna de yielding para a main thread. Ela preserva a prioridade da tarefa (ao contrário do setTimeout, que joga a continuação para o final da fila). No entanto, o scheduler.yield() não garante que o paint aconteceu antes da execução do código. Para este caso de uso específico, o requestAnimationFrame é a melhor ferramenta porque está atrelado ao ciclo de vida da renderização. Observe que o Safari ainda não é compatível com o scheduler.yield().

A combinação de requestAnimationFrame + setTimeout foi feita sob medida para esse cenário. O requestAnimationFrame dispara logo antes de o navegador pintar a tela. O setTimeout aninhado cria uma nova tarefa que roda após o término desse paint. Juntos, eles garantem que o código adiado execute após a atualização visual — exatamente o que o INP mede.

Trade-offs

  • Microatraso na coleta de dados: Os eventos chegam ao GTM de 50 a 250ms 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 o visitante dispara um evento e sai da página dentro da janela desse atraso, o evento pode não ser enviado. Para eventos críticos antes de um redirecionamento ou do unload da página, use navigator.sendBeacon() em vez do dataLayer.push(). Veja por que limitar scripts de analytics para saber mais sobre a Beacon API.

Para a maioria dos rastreamentos, o ganho no INP supera de longe esse pequeno atraso. Se você precisa de precisão de milissegundos no registro de eventos (real-time bidding, dashboards financeiros), essa abordagem não é indicada. Para todo o resto, a vantagem é clara.

Depois de aplicar o override, comprove a melhoria com Real User Monitoring. Testes em laboratório vão mostrar a diferença, mas são os field data de usuários reais em conexões reais que realmente contam para a pontuação do 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.

Escrevo código, não relatório.

Entro no seu time por 1 ou 2 sprints. Monto o monitoramento e garanto que o time mantém as métricas no verde depois que eu saio.

Entra em contato
Agendar eventos do dataLayer para otimizar o INP Core Web Vitals Agendar eventos do dataLayer para otimizar o INP