Planowanie zdarzeń dataLayer w celu optymalizacji INP
Odraczanie zdarzeń GTM do momentu stabilizacji layoutu w celu poprawy wartości INP

TL;DR: Poprawa INP poprzez optymalizację Google Tag Manager
Problem: Standardowe wywołania Google Tag Manager (dataLayer.push()), szczególnie gdy są uruchamiane bezpośrednio przez interakcje użytkownika (takie jak kliknięcia lub dotknięcia), mogą opóźniać zdolność przeglądarki do wyświetlania aktualizacji wizualnych. Negatywnie wpływa to na wynik Interaction to Next Paint (INP), ponieważ przeglądarka jest zmuszana do przetwarzania zadań GTM przed wyrenderowaniem wizualnej odpowiedzi na tę interakcję.
Rozwiązanie: Możemy odroczyć wywołania dataLayer.push() do momentu po wyrenderowaniu następnej klatki przez przeglądarkę. Priorytetem staje się natychmiastowa wizualna odpowiedź dla użytkownika. Poprawka polega na niewielkim fragmencie JavaScript, który modyfikuje domyślne zachowanie dataLayer.push(), wprowadzając to odroczenie.
Korzyść: Podejście to zazwyczaj skutkuje redukcją INP o 20ms do 100ms u naszych klientów, często przekształcając niezdane wyniki Core Web Vitals w zdane. Użytkownicy doświadczają zauważalnie szybszego interfejsu. Chociaż zbieranie danych dla GTM jest nieznacznie opóźnione (zwykle 50-250ms), jest to akceptowalny kompromis dla większości celów analitycznych i marketingowych.
Rozwiązywanie problemów z INP spowodowanych wykonywaniem Google Tag Manager
U jednego z naszych klientów zaobserwowaliśmy redukcję metryki Interaction to Next Paint (INP) o 100ms poprzez proste przeplanowanie momentu wykonania funkcji dataLayer.push() po interakcji użytkownika. Poprawę tę osiągnięto za pomocą łatwego do zastosowania i przetestowania „drop-in” zamiennika JavaScript, który priorytetyzuje renderowanie.
Table of Contents!
- TL;DR: Poprawa INP poprzez optymalizację Google Tag Manager
- Problem z INP przy dataLayer.push()
- Rozwiązanie: najpierw renderowanie, potem push do dataLayer!
- Zastosowanie kodu
- Łatwe testowanie: nadpisanie globalne
- Dlaczego to pomaga Interaction to Next Paint
- Dlaczego nie użyć stałego opóźnienia, idle Callback lub Schedulera?
- Kompromisy
Problem z INP przy dataLayer.push()
Jeśli pracowałeś z Google Tag Manager (GTM), znasz dobrze dataLayer.push(). To standardowa metoda wysyłania danych lub zdarzeń do Data Layer, umożliwiająca uruchamianie tagów. Jest szeroko stosowana, głęboko zintegrowana z wieloma funkcjonalnościami stron, a jej wpływ na wydajność rzadko bywa kwestionowany. Jednak zakładanie, że zawsze wykonuje się w optymalnym momencie dla user experience, może być problematyczne.
Gdy dataLayer.push() jest wywoływany bezpośrednio w obsłudze zdarzenia interakcji użytkownika (np. kliknięcie przycisku), zazwyczaj wykonuje się synchronicznie. Oznacza to, że wszelkie tagi GTM skonfigurowane do uruchomienia na podstawie tego zdarzenia również próbują wykonać się natychmiast i blokują główny wątek przed aktualizacją layoutu. To blokowanie uniemożliwia przeglądarce szybkie wyrenderowanie zmian wizualnych oczekiwanych po interakcji użytkownika (np. otwarcie menu, wyświetlenie spinnera ładowania), co prowadzi do słabego wyniku INP.
Przyjrzyjmy się, co się dzieje. Poniższy ślad wydajności, z dużego serwisu informacyjnego, pokazuje aktywność związaną z GTM po interakcji użytkownika. W tym przypadku zadania GTM zajęły około 90ms, a przy ogólnej wartości INP wynoszącej 263ms ta interakcja nie przechodzi tego Core Web Vitals!

Rozwiązanie: najpierw renderowanie, potem push do dataLayer!
Rozwiązanie jest zarówno proste, jak i eleganckie, zgodne z najlepszymi praktykami optymalizacji INP: priorytetem jest percepcja szybkości przez użytkownika. Zamiast wykonywać cały kod (obsługę interakcji, aktualizacje wizualne i śledzenie GTM) synchronicznie, powinniśmy:
- Natychmiast wykonać krytyczny kod odpowiedzialny za aktualizację wizualną.
- Pozwolić przeglądarce wyrenderować te zmiany wizualne.
- Następnie wykonać mniej krytyczny kod, taki jak wysyłanie zdarzeń do dataLayer.
Podejście to jest często nazywane „yielding to the main thread”. Zobaczmy, jaki jest wpływ zastosowania tego wzorca yielding do wywołań dataLayer.push() na tej samej stronie i dla dokładnie tej samej interakcji co wcześniej. Jedyną różnicą jest to, że zaplanowaliśmy dataLayer.push() do wykonania po tym, jak przeglądarka miała szansę wyrenderować następną klatkę za pomocą requestAnimationFrame.

Jak widać, ta sama interakcja, która wcześniej nie przechodziła, teraz komfortowo spełnia wymagania Core Web Vitals. Wszystkie niezbędne dane nadal są wysyłane do dataLayer. Kluczowa różnica polega na tym, że skrypty związane z GTM wykonują się teraz po tym, jak przeglądarka zaktualizowała layout w odpowiedzi na działanie użytkownika. Oznacza to, że odwiedzający otrzymuje natychmiastową wizualną odpowiedź, poprawiając jego doświadczenie, zamiast czekać na przetworzenie skryptów śledzących.
Zastosowanie kodu
Kod działa poprzez nadpisanie domyślnej funkcji dataLayer.push() zmodyfikowaną funkcją, która wysyła dane do dataLayer po wykonaniu aktualizacji layoutu.
Funkcja pomocnicza Await Paint
Ta funkcja pomocnicza wykorzystuje requestAnimationFrame do zaplanowania wywołania zwrotnego, które uruchomi się po wyrenderowaniu następnej klatki przez przeglądarkę.
// --- 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();
}
}Przykład implementacji
Oto przykład funkcji narzędziowej React, która automatycznie planuje push do 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>
// );
// }
Łatwe testowanie: nadpisanie globalne
Aby szybko przetestować ten wzorzec na całej stronie lub dla istniejących implementacji dataLayer.push() bez refaktoryzacji każdej z nich, można globalnie nadpisać funkcję dataLayer.push().
Ważne: Umieść ten skrypt wysoko w <head> swojego HTML, bezpośrednio po załadowaniu skryptu kontenera GTM. Zapewni to, że nadpisanie będzie aktywne tak szybko, jak to możliwe.
<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>
Dlaczego to pomaga Interaction to Next Paint
INP mierzy opóźnienie od interakcji użytkownika (np. kliknięcie, dotknięcie, naciśnięcie klawisza) do momentu, gdy przeglądarka wyrenderuje następną aktualizację wizualną w odpowiedzi na tę interakcję. Jeśli synchronicznie wykonujesz zasobożerne zadania, takie jak przetwarzanie zdarzeń GTM i uruchamianie tagów, bezpośrednio po interakcji, blokujesz główny wątek przeglądarki. Uniemożliwia to renderowanie wizualnej odpowiedzi, której użytkownik oczekuje. Odraczając wykonanie niekrytycznego JavaScript, takiego jak śledzenie GTM, do momentu po wyrenderowaniu aktualizacji wizualnych przez przeglądarkę, ten wzorzec zapewnia użytkownikom szybką wizualną odpowiedź, znacząco poprawiając wynik INP.
Dlaczego nie użyć stałego opóźnienia, idle Callback lub Schedulera?
- Stałe
setTimeout(delay): Użycie zakodowanego na sztywno opóźnienia (np. setTimeout(..., 100)) to w zasadzie zgadywanie, kiedy renderowanie się zakończy. Nie jest adaptacyjne; może być za długie (niepotrzebnie opóźniając śledzenie) lub za krótkie (nadal blokując renderowanie). requestIdleCallback: To API planuje pracę, gdy przeglądarka jest bezczynna. Choć przydatne dla zadań w tle, nie gwarantuje wykonania natychmiast po wizualnej aktualizacji konkretnej interakcji. Wywołanie zwrotne może uruchomić się znacznie później lub, w okresach dużego obciążenia, w ogóle nie uruchomić się przed opuszczeniem strony przez użytkownika.- Generyczne Schedulery (
postTaskitp.): Chociaż schedulerpostTaskprzeglądarki oferuje priorytetyzację,requestAnimationFramejest specyficznie powiązany z cyklem życia renderowania. Funkcja pomocniczaawaitPaintwykorzystuje to, używającrequestAnimationFramejako sygnału, że przeglądarka przygotowuje się do renderowania, a następnie dodaje minimalne opóźnienie, aby uruchomić się po prawdopodobnym zakończeniu tego renderowania.
Kompromisy
Każda optymalizacja wiąże się z potencjalnymi kompromisami.
- Mikroopóźnienie w zbieraniu danych: Ta technika wprowadza przewidywalne mikroopóźnienie (około 50-250ms, w zależności od obciążenia przeglądarki i konkretnych timeoutów użytych w awaitPaint) przed dotarciem danych zdarzenia do Google Tag Manager.
- Ryzyko utraty danych przy szybkim opuszczeniu strony: Jeśli odwiedzający wywoła zdarzenie, a następnie opuści stronę w oknie tego mikroopóźnienia (przed wykonaniem odroczonego dataLayer.push), dane tego konkretnego zdarzenia mogą nie zostać wysłane. Dla krytycznych zdarzeń, gdzie to ryzyko jest nieakceptowalne (np. bezpośrednio przed przekierowaniem lub zamknięciem strony), alternatywne mechanizmy śledzenia, takie jak navigator.sendBeacon, mogą być rozważone dla tych konkretnych zdarzeń, choć wykracza to poza zakres nadpisania dataLayer.push.
Dla większości standardowego śledzenia analitycznego i marketingowego to niewielkie opóźnienie jest nieistotne i stanowi opłacalny kompromis za znaczną poprawę postrzeganej przez użytkownika wydajności i wyników INP. Jednak w scenariuszach wymagających ultrakrótkiego czasu odpowiedzi (np. niektóre rodzaje interakcji z licytacjami w czasie rzeczywistym bezpośrednio powiązanymi ze zdarzeniami GTM lub wysoce wrażliwe panele finansowe, gdzie milisekundowa precyzja rejestrowania zdarzeń jest kluczowa), to podejście może nie być odpowiednie.
W pozostałych przypadkach zysk w wydajności INP i user experience zazwyczaj znacznie przewyższa minimalne opóźnienie w zbieraniu danych.W pozostałych przypadkach zysk w wydajności INP i user experience zazwyczaj znacznie przewyższa minimalne opóźnienie w zbieraniu danych.
Performance is a Feature.
Treating speed as an afterthought fails. Build a performance culture with a dedicated 2-sprint optimization overhaul.
- 2-Sprint Overhaul
- Culture Building
- Sustainable Speed

