Agendando eventos do dataLayer para otimizar o INP

Como o adiamento de 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-03-05

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

Quando o dataLayer.push() é acionado de forma síncrona dentro de um manipulador de eventos, o GTM processa as tags antes que o navegador possa renderizar o feedback visual. Isso bloqueia a thread principal e infla seu Interaction to Next Paint (INP). A correção: agende o dataLayer.push() para rodar depois que o navegador tiver renderizado o próximo quadro usando o requestAnimationFrame. Este padrão corta de 20 a 100ms do INP na maioria dos sites, muitas vezes transformando uma pontuação reprovada em aprovada. A coleta de dados é atrasada em 50 a 250ms, o que é irrelevante para analytics e rastreamento de marketing.

Como o dataLayer.push() prejudica seu INP

Para um de nossos clientes, medimos uma redução de 100ms no Interaction to Next Paint (INP) reagendando quando o dataLayer.push() é executado após uma interação do usuário. A correção é um substituto JavaScript drop-in que prioriza a renderização em vez do 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, você conhece o dataLayer.push(). É o método padrão para enviar eventos para o Data Layer, e está profundamente integrado na maioria das funcionalidades do site. O problema é que ninguém questiona quando ele é executado.

Quando o dataLayer.push() é chamado diretamente dentro de um manipulador de eventos (um clique de botão, um envio de formulário, uma alternância de menu), ele roda de forma síncrona. As tags do GTM configuradas para disparar nesse evento também são executadas imediatamente, bloqueando a thread principal 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 scripts de rastreamento em segundo plano. Esse atraso é o seu presentation delay, e é o principal contribuinte para 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 90ms para serem executadas, empurrando o INP geral para 263ms. Essa interação reprova no Core Web Vitals.

A correção: renderizar primeiro, depois enviar (push)

A solução está alinhada com as orientações de otimização de INP do web.dev: yield para a thread principal para que o navegador possa renderizar antes de executar código não crítico. Em vez de executar o rastreamento de forma síncrona dentro do manipulador de eventos:

  1. Execute o código para a atualização visual imediatamente.
  2. Deixe o navegador renderizar.
  3. Então envie (push) para o dataLayer.

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

A interação que antes falhava agora passa confortavelmente. Todos os dados ainda chegam ao dataLayer. A diferença é que os scripts do GTM são executados depois que o navegador respondeu à ação do usuário, e não antes.

O helper awaitPaint

Este helper usa o requestAnimationFrame para agendar um callback depois que o navegador tiver renderizado. O setTimeout aninhado garante que a função rode após a renderização ser concluída, não apenas antes dela. Este é o padrão recomendado pelo web.dev para adiar o trabalho não crítico após uma interação.

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

        // Agenda para depois da próxima renderização
        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 renderização:

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

Override global para testes rápidos

Para testar este padrão em todo o seu site sem refatorar cada chamada do dataLayer.push(), você pode sobrescrever globalmente a função push. 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) {
        return new Promise((resolve) => {
            const fallbackTimeout = setTimeout(() => {
                if (typeof fn === 'function') { fn(); }
                resolve();
            }, 200);
            requestAnimationFrame(() => {
                setTimeout(() => {
                    clearTimeout(fallbackTimeout);
                    if (typeof fn === 'function') { fn(); }
                    resolve();
                }, 50);
            });
        });
    }

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

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

Isso sobrescreve todo dataLayer.push() na página. Todos os gatilhos de eventos do GTM existentes, chamadas de rastreamento hardcoded e integrações de plugins se beneficiam automaticamente sem nenhuma alteração de código.

Por que isso melhora o INP

O INP mede o tempo desde uma interação do usuário até que o navegador renderize a resposta visual. Se você executar o processamento de eventos do GTM de forma síncrona dentro de um manipulador de eventos, você bloqueia a thread principal e impede a renderização. O Web Almanac de 2024 descobriu que o presentation delay é o maior contribuinte para um INP ruim na mediana, e os 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 mobile passam no INP em geral, mas apenas 63% dos 1.000 sites mais visitados o fazem. Esses sites principais carregam mais scripts de terceiros. No Subito (o maior marketplace de classificados da Itália), desabilitar um único script de rastreamento do TikTok carregado via GTM derrubou o INP de 208ms para cerca de 170ms. Um script, 38ms economizados.

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

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

  • setTimeout(delay) fixo: Um atraso hardcoded (ex: setTimeout(..., 100)) é adivinhar quando a renderização será concluída. Muito longo e você atrasa o rastreamento desnecessariamente. Muito curto e você ainda bloqueia a renderização.
  • requestIdleCallback: Agenda o trabalho quando o navegador está ocioso, mas não garante a execução prontamente após uma interação específica. O callback pode rodar muito mais tarde, ou nem rodar antes que o usuário navegue para fora da página.
  • scheduler.yield(): A API moderna para fazer yield para a thread principal. Ela preserva a prioridade da tarefa (ao contrário do setTimeout que envia sua continuação para o final da fila). No entanto, o scheduler.yield() não garante que uma renderização tenha ocorrido antes que seu código rode. Para este caso de uso específico, o requestAnimationFrame é a ferramenta melhor porque está atrelado ao ciclo de vida de renderização. Observe que o scheduler.yield() ainda não é suportado no Safari.

A combinação requestAnimationFrame + setTimeout é projetada especificamente para este cenário. O requestAnimationFrame dispara logo antes do navegador renderizar. O setTimeout aninhado cria uma nova tarefa que roda depois que essa renderização é concluída. Juntos, eles garantem que seu código adiado seja executado após a atualização visual, que é exatamente o que o INP mede.

Trade-offs

  • Micro-atraso na coleta de dados: Os eventos chegam ao GTM 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 um visitante disparar um evento e sair da página dentro da 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 caso para limitar scripts de analytics para mais detalhes sobre a API Beacon.

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

Após aplicar o override, verifique a melhoria com o Real User Monitoring. Testes de laboratório mostrarão a diferença, mas os dados de campo de usuários reais em redes reais são o que conta para suas pontuações 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.

Descubra o que está realmente lento.

Mapeio seu critical rendering path com dados reais de usuários. Você recebe uma lista priorizada de fixes, não mais um relatório do Lighthouse.

Quero a auditoria
Agendando eventos do dataLayer para otimizar o INPCore Web Vitals Agendando eventos do dataLayer para otimizar o INP