Delaying `Google Tag Manager` or `Google Analytics` for a Faster Page — Without Losing Analytics
A quick dive into how I improved my site’s performance by delaying Google Tag Manager (GTM) loading — without breaking analytics. This simple lazy-loading trick balances tracking needs with better Core Web Vitals.
As someone passionate about web performance, I'm always looking for ways to make websites load faster and smoother. I recently embarked on a mission to significantly improve my site's Google PageSpeed (Lighthouse) score, and one of the biggest culprits I identified was Google Tag Manager (GTM).
Initially, my website was scoring a respectable 70 on Mobile Lighthouse, which isn't terrible, but there was definitely room for improvement. Looking at the "Diagnose performance issues" section, GTM was a noticeable contributor to the main thread time and transfer size under "3rd parties".


GTM appeared in the 3rd party list, adding around 220ms of main thread time and contributing to slower initial rendering
The Problem
GTM was loading immediately on page start, even before any user interaction. That means the browser had to:
- Parse and execute
GTMscripts before rendering was complete. - Delay the
First Contentful Paint (FCP)andLargest Contentful Paint (LCP). - Consume CPU time for tracking setup, even if the user bounced right away.
Here’s what my initial setup looked like — the default snippet straight from Google Tag Manager:
<script>
(function(w,d,s,l,i){
w[l]=w[l]||[];
w[l].push({'gtm.start': new Date().getTime(), event:'gtm.js'});
var f=d.getElementsByTagName(s)[0],
j=d.createElement(s), dl=l!='dataLayer'?'&l='+l:'';
j.async=true;
j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;
f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXXX');
</script>Google Tag Manager default snippet
That’s fine for analytics-heavy sites — but for performance-focused projects, this was a problem.
The Optimization: Delay GTM Loading
Instead of loading GTM as soon as the page starts, I wanted it to load only when needed — either:
- when the user interacts (scrolls, clicks, types, etc.), or
- after a short delay (e.g., 3.5 seconds) as a fallback.
This ensures:
- Faster initial render
- Better
Core Web Vitals - No “third-party script” blocking issues in PageSpeed
Here’s the final version:
<script>
(function () {
const GTM_ID = "GTM-XXXXXXXX"; // ← Replace with your GTM ID
const LOAD_DELAY = 3500; // milliseconds
let gtmInitialized = false;
/**
* Initializes the Google Tag Manager script dynamically.
* Prevents multiple injections by checking `gtmInitialized`.
*/
function loadGTM() {
if (gtmInitialized) return;
gtmInitialized = true;
const script = document.createElement("script");
script.type = "text/javascript";
script.async = true;
script.src = `https://www.googletagmanager.com/gtm.js?id=${GTM_ID}`;
script.onload = () => {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: "gtm.js",
"gtm.start": new Date().getTime(),
"gtm.uniqueEventId": 0,
});
};
document.head.appendChild(script);
}
function onFirstInteraction(event) {
loadGTM();
event.currentTarget.removeEventListener(event.type, onFirstInteraction);
}
/**
* Initializes GTM after user interaction or fallback timeout.
* This ensures analytics load only when the visitor engages
* with the page, improving initial load and Lighthouse score.
*/
function setupDelayedGTM() {
// Fallback: load GTM after delay
document.addEventListener("DOMContentLoaded", () => {
setTimeout(loadGTM, LOAD_DELAY);
});
// Load GTM as soon as the user interacts
const triggers = ["scroll", "pointerdown", "keydown", "touchstart", "mousemove"];
triggers.forEach((eventName) => {
document.addEventListener(eventName, onFirstInteraction, { passive: true });
});
}
// Run setup
setupDelayedGTM();
})();
</script>Delaying Google Tag Manager snippet
Or use final uglified version:
<script>
(function(w,d,id,delay,l){
let a=false;
function g(){
if(a)return; a=true;
const s=d.createElement("script");
s.async=1;
s.src="https://www.googletagmanager.com/gtm.js?id="+id;
s.onload=()=>{w[l]=w[l]||[];w[l].push({event:"gtm.js","gtm.start":Date.now(),"gtm.uniqueEventId":0});};
d.head.appendChild(s);
}
function o(e){g();e.currentTarget.removeEventListener(e.type,o);}
d.addEventListener("DOMContentLoaded",()=>setTimeout(g,delay));
["scroll","pointerdown","keydown","touchstart","mousemove"].forEach(ev=>{
d.addEventListener(ev,o,{passive:!0});
});
})(window,document,"GTM-XXXXXXXX",3500,"dataLayer");
</script>Perfect — let’s break it down in detail. Below is a step-by-step deep dive explaining how the delayed Google Tag Manager (GTM) loading works, based on the exact script we used.
How It Works (Step-by-Step Breakdown)
1. Wait until the page’s DOM is ready
document.addEventListener("DOMContentLoaded", () => {
setTimeout(loadGTM, LOAD_DELAY);
});- The
DOMContentLoadedevent ensures this script doesn’t block rendering. - Once the HTML is parsed (not waiting for all images/resources), a timer starts.
- After 3.5 seconds, it calls
loadGTMto load GTM — only if the user hasn’t interacted yet.
We give the browser some breathing room to load critical CSS, JS, and above-the-fold content first. By deferring GTM by a few seconds, you reduce early JavaScript execution pressure — improving metrics like First Contentful Paint (FCP) and Largest Contentful Paint (LCP).
2. Add multiple user interaction listeners
const triggers = ["scroll", "pointerdown", "keydown", "touchstart", "mousemove"];
triggers.forEach((eventName) => {
document.addEventListener(eventName, onFirstInteraction, { passive: true });
});- We attach listeners to several common user actions:
scroll→ user starts exploringpointerdown→ mouse click / touch presskeydown→ keyboard interactiontouchstart→ mobile gesturemousemove→ desktop pointer movement
- The moment any of these events occur, we call
onFirstInteraction().
This means GTM will load immediately when a user engages, even before the 3.5s timer — because by that time, the user is active, and analytics are relevant.
3. Trigger GTM only once per session
function onFirstInteraction(event) {
loadGTM();
event.currentTarget.removeEventListener(event.type, onFirstInteraction);
}
loadGTM()ensures GTM is initialized when the event fires.- It also removes its own listener for that specific event to avoid duplicate triggers.
- We also protect GTM from multiple calls (see next section).
Without this cleanup, multiple events (like scroll + keydown) could trigger loadGTM() several times — creating multiple GTM <script> elements and redundant tracking.
4. Initialize GTM safely
let gtmInitialized = false;
function loadGTM() {
if (gtmInitialized) return;
gtmInitialized = true;
...
}
- We add a flag
gtmInitializedto prevent duplicate initialization. - This flag ensures GTM loads only once, whether by timeout or user action.
5. Create and inject GTM script tag
const script = document.createElement("script");
script.type = "text/javascript";
script.async = true;
script.src = `https://www.googletagmanager.com/gtm.js?id=${GTM_ID}`;
document.head.appendChild(script);
- This dynamically creates the GTM
<script>tag that normally lives in the<head>of your site. - Using
asyncmeans it won’t block rendering. - The browser will fetch the GTM JS file in parallel, without delaying other tasks.
This replicates the default GTM behavior but triggers it lazily — only when appropriate.
6. Handle GTM “load complete”
script.onload = () => {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: "gtm.js",
"gtm.start": new Date().getTime(),
"gtm.uniqueEventId": 0,
});
};
- Once the GTM script has finished downloading, we manually push a
gtm.jsevent into thedataLayer. - This event simulates the initial GTM startup, signaling that GTM is ready to process subsequent tags.
Normally, GTM’s inline snippet does this automatically. Since we’re injecting the script manually, this line ensures the same startup sequence occurs.
Here’s how it behaves in practice:
| Timeline | Trigger | What Happens |
|---|---|---|
| Page loads | DOM ready | Timer starts (3.5s) |
| Before 3.5s | User interacts | GTM loads immediately |
| After 3.5s | No interaction | GTM loads automatically |
| After GTM loads | script.onload | Sends gtm.js event to dataLayer |
The Result
After applying this optimization:
- My PageSpeed performance score improved from 70 → 94 🎉
- GTM disappeared from the 3rd-party impact list
- Initial page load and LCP improved significantly


| Metric | Before | After |
|---|---|---|
| Lighthouse Performance | 70 | 94 |
| LCP | 5.3s | 2.6s |
| Total Blocking Time | 170ms | 0ms |
| GTM Main Thread Impact | High | None before interaction |
This single change cleaned up my third-party script impact, leading to smoother initial loading and a much more responsive feel.

Takeaways
- GTM, while useful, is a render-blocking third-party script if loaded too early.
- Deferring it intelligently keeps analytics accurate without sacrificing performance.
- You can tweak the delay time or event list depending on your UX needs.
- Works great for static sites, blogs, and landing pages where instant tracking isn’t critical.
✨ What I Learned
Optimizing isn’t just about removing things — sometimes it’s about loading them smarter.
This small tweak taught me how timing can be just as powerful as content when it comes to web performance.
