Agendamento de Eventos dataLayer para Otimizar o INP
Adiando eventos GTM até a estabilização do layout para melhores valores de INP

TL;DR: Melhorando o INP ao Otimizar o Google Tag Manager
O Problema: Chamadas padrão do Google Tag Manager (dataLayer.push()), especialmente quando acionadas imediatamente por interações do usuário (como cliques ou toques), podem atrasar a capacidade do navegador de exibir atualizações visuais. Isso impacta negativamente a pontuação do Interaction to Next Paint (INP) porque o navegador é forçado a processar tarefas do GTM antes de renderizar o feedback visual daquela interação.
A Solução: Podemos adiar essas chamadas de dataLayer.push() até depois que o navegador tenha pintado o próximo quadro. Isso prioriza a renderização do feedback visual imediato para o usuário. A correção envolve um pequeno trecho de JavaScript que modifica o comportamento padrão do dataLayer.push() para incorporar esse adiamento.
O Benefício: Essa abordagem comumente resulta em uma redução de 20ms a 100ms no INP para nossos clientes, frequentemente transformando pontuações reprovadas nos Core Web Vitals em aprovadas. Os usuários experimentam uma interface visivelmente mais responsiva. Embora a coleta de dados para o GTM seja ligeiramente atrasada (tipicamente 50-250ms), essa é uma troca aceitável para a maioria dos propósitos de analytics e marketing.
Resolvendo Desafios de INP Causados pela Execução do Google Tag Manager
Para um de nossos clientes, observamos uma redução de 100ms na métrica Interaction to Next Paint (INP) simplesmente reagendando quando a função dataLayer.push() é executada após uma interação do usuário. Essa melhoria foi alcançada usando uma substituição JavaScript "drop-in" fácil de aplicar e testar que prioriza a renderização.
Table of Contents!
- TL;DR: Melhorando o INP ao Otimizar o Google Tag Manager
- O problema de INP com dataLayer.push()
- A solução: priorize a pintura, depois faça push para o datalayer!
- Aplicando o Código
- Teste Facilitado: Substituição Global
- Por que isso ajuda o Interaction to Next Paint
- Por Que Não Usar Apenas um Atraso Fixo, idle Callback ou Scheduler?
- Compensações
O problema de INP com dataLayer.push()
Se você já trabalhou com Google Tag Manager (GTM), está familiarizado com dataLayer.push(). É o método padrão para enviar dados ou eventos para o Data Layer, permitindo que tags sejam disparadas. É amplamente utilizado, profundamente integrado em muitas funcionalidades de sites, e suas implicações de desempenho raramente são questionadas. No entanto, assumir que ele sempre executa no momento ideal para a user experience pode ser problemático.
Quando dataLayer.push() é chamado diretamente dentro de um manipulador de eventos para uma interação do usuário (por exemplo, um clique em botão), ele tipicamente executa de forma síncrona. Isso significa que quaisquer tags GTM configuradas para disparar com base nesse evento também tentarão executar imediatamente e bloquear a thread principal antes de uma atualização de layout. Esse bloqueio impede o navegador de renderizar rapidamente as mudanças visuais esperadas da interação do usuário (por exemplo, abrir um menu, mostrar um spinner de carregamento), levando a uma pontuação ruim de INP.
Vamos examinar o que acontece. O trace de desempenho abaixo, de um grande site de notícias, destaca a atividade relacionada ao GTM após uma interação do usuário. Neste caso, as tarefas do GTM levaram aproximadamente 90ms para executar e com um valor geral de INP de 263ms esta interação reprova nos Core Web Vitals!

A solução: priorize a pintura, depois faça push para o datalayer!
A solução é simples e elegante, alinhando-se com as melhores práticas de otimização de INP: priorizar a percepção de velocidade do usuário. Em vez de executar todo o código (manipulação de interação, atualizações visuais e rastreamento GTM) de forma síncrona, devemos:
- Executar o código crítico para a atualização visual imediatamente.
- Permitir que o navegador pinte essas mudanças visuais.
- Em seguida, executar código menos crítico, como enviar eventos para o dataLayer.
Essa abordagem é frequentemente chamada de "yielding to the main thread." Vamos ver o impacto quando aplicamos esse padrão de yielding às chamadas de dataLayer.push() no mesmo site e para exatamente a mesma interação de antes. A única diferença é que agendamos o dataLayer.push() para ocorrer após o navegador ter tido a chance de renderizar o próximo quadro usando requestAnimationFrame.

Como você pode ver, a mesma interação que antes reprovava agora passa confortavelmente nos Core Web Vitals. Todos os dados necessários ainda são enviados para o dataLayer. A diferença crucial é que os scripts relacionados ao GTM agora executam após o navegador ter atualizado o layout em resposta à ação do usuário. Isso significa que seu visitante recebe feedback visual imediato, melhorando sua experiência, em vez de esperar que scripts de rastreamento sejam processados.
Aplicando o Código
O código funciona substituindo a função padrão dataLayer.push() por uma função modificada que envia os dados para o dataLayer após uma atualização de layout ter sido realizada.
Função auxiliar Await Paint
Esta função auxiliar usa requestAnimationFrame para agendar um callback para executar após o navegador ter pintado o próximo quadro.
// --- INP Yield Pattern Implementation ---
// This helper ensures that a function only runs after the next paint (or safe fallback)
async function awaitPaint(fn) {
await new Promise((resolve) => {
// Fallback timeout: ensures we don't hang forever if RAF never fires
setTimeout(resolve, 200);
// Request the next animation frame (signals readiness to paint)
requestAnimationFrame(() => {
// Small delay to ensure the frame is actually painted, not just queued
setTimeout(resolve, 50);
});
});
// Once the paint (or fallback) happens, run the provided function
if (typeof fn === 'function') {
fn();
}
}Exemplo de implementação
Este é um exemplo de uma função utilitária React que automaticamente agenda o push para o dataLayer.
export const pushToDataLayer = (event: string, data: Record<string, any> = {}): void => {
// Ensure dataLayer exists
if (typeof window !== 'undefined') {
window.dataLayer = window.dataLayer || [];
// wait for paint
awaitPaint(() => {
// Push event and data to dataLayer
window.dataLayer.push({
event,
...data,
timestamp: new Date().toISOString()
});
});
}
};
// Usage in a React component:
// import { useState, useEffect } from 'react';
// import { pushToDataLayer } from '../utils/analytics';
// function ProductCard({ product }) {
// const [isWishlisted, setIsWishlisted] = useState(false);
// // Track wishlist changes
// useEffect(() => {
// if (isWishlisted) {
// pushToDataLayer('addToWishlist', {
// productId: product.id,
// productName: product.name,
// productPrice: product.price
// });
// }
// }, [isWishlisted, product]);
// return (
// <div className="product-card">
// <h3>{product.name}</h3>
// <p>${product.price}</p>
// <button onClick={() => setIsWishlisted(!isWishlisted)}>
// {isWishlisted ? '♥' : '♡'} {isWishlisted ? 'Wishlisted' : 'Add to Wishlist'}
// </button>
// </div>
// );
// }
Teste Facilitado: Substituição Global
Para testar rapidamente este padrão em todo o seu site ou para implementações existentes de dataLayer.push() sem refatorar cada uma, você pode substituir globalmente a função dataLayer.push().
Importante: Coloque este script no topo do <head> do seu HTML, imediatamente após o script do contêiner GTM ser carregado. Isso garante que sua substituição esteja em vigor o mais cedo possível.
<script type="module">
// Ensure dataLayer exists (standard GTM snippet part)
window.dataLayer = window.dataLayer || [];
// --- INP Yield Pattern Helper ---
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);
});
});
}
// --- Applying the pattern to Google Tag Manager dataLayer.push globally ---
if (window.dataLayer && typeof window.dataLayer.push === 'function') {
// Preserve the original push function
const originalDataLayerPush = window.dataLayer.push.bind(window.dataLayer);
// Override dataLayer.push
window.dataLayer.push = function (...args) {
// Using an IIFE to use async/await syntax if preferred,
// or directly call awaitPaint.
(async () => {
await awaitPaint(() => {
// Call the original push with its arguments after yielding to paint
originalDataLayerPush(...args);
});
})();
// Return the value the original push would have, if any (though typically undefined)
// For GTM, the push method doesn't have a meaningful return value for the caller.
// The primary purpose is the side effect of adding to the queue.
};
console.log('dataLayer.push has been overridden to improve INP.');
}
</script>
Por que isso ajuda o Interaction to Next Paint
O INP mede a latência desde uma interação do usuário (por exemplo, clique, toque, pressionamento de tecla) até o navegador pintar a próxima atualização visual em resposta a essa interação. Se você executar sincronamente tarefas que consomem muitos recursos, como processamento de eventos GTM e disparo de tags, imediatamente após uma interação, você bloqueia a thread principal do navegador. Isso impede a renderização do feedback visual que o usuário espera. Ao adiar a execução de JavaScript não crítico como rastreamento GTM até após o navegador ter pintado as atualizações visuais, esse padrão garante que os usuários recebam feedback visual rápido, melhorando significativamente a pontuação de INP.
Por Que Não Usar Apenas um Atraso Fixo, idle Callback ou Scheduler?
setTimeout(delay)fixo: Usar um atraso hardcoded (por exemplo, setTimeout(..., 100)) é essencialmente adivinhar quando a renderização será concluída. Não é adaptativo; pode ser muito longo (atrasando o rastreamento desnecessariamente) ou muito curto (ainda bloqueando a pintura).requestIdleCallback: Esta API agenda trabalho quando o navegador está ocioso. Embora útil para tarefas em segundo plano, ela não garante a execução prontamente após a atualização visual de uma interação específica. O callback pode executar muito mais tarde ou, durante períodos de alta atividade, não executar antes que o usuário navegue para fora.- Schedulers Genéricos (
postTasketc.): Embora o schedulerpostTaskdo navegador ofereça priorização,requestAnimationFrameestá especificamente ligado ao ciclo de vida de renderização. O auxiliarawaitPaintaproveita isso usandorequestAnimationFramecomo um sinal de que o navegador está se preparando para pintar, e então adiciona um atraso mínimo para disparar após essa pintura provavelmente ter sido concluída.
Compensações
Toda otimização tem potenciais compensações.
- Micro-atraso na Coleta de Dados: Esta técnica introduz um micro-atraso previsível (aproximadamente 50-250ms, dependendo da carga do navegador e dos timeouts específicos usados no awaitPaint) antes que os dados do evento cheguem ao Google Tag Manager.
- Risco de Perda de Dados em Saída Rápida: Se um visitante aciona um evento e depois sai da página dentro desta janela de micro-atraso (antes que o dataLayer.push() adiado execute), os dados daquele evento específico podem não ser enviados. Para eventos críticos onde este risco é inaceitável (por exemplo, imediatamente antes de um redirecionamento ou descarregamento de página), mecanismos alternativos de rastreamento como navigator.sendBeacon podem ser considerados para esses eventos específicos, embora isso esteja fora do escopo da substituição do dataLayer.push.
Para a maioria do rastreamento padrão de analytics e marketing, este leve atraso é inconsequente e uma compensação que vale a pena pela melhoria significativa no desempenho percebido pelo usuário e nas pontuações de INP. No entanto, para cenários de latência ultra-baixa (por exemplo, alguns tipos de interações de leilão em tempo real diretamente ligadas a eventos GTM, ou dashboards financeiros altamente sensíveis onde precisão de milissegundos no registro de eventos é fundamental), esta abordagem pode não ser adequada.
De outra forma, o ganho em desempenho de INP e user experience tipicamente supera em muito a latência mínima na coleta de dados.De outra forma, o ganho em desempenho de INP e user experience tipicamente supera em muito a latência mínima na coleta de dados.
CrUX data is 28 days late.
Google provides data 28 days late. CoreDash provides data in real-time. Debug faster.
- Real-Time Insights
- Faster Debugging
- Instant Feedback

