Third-party tags are the slow-creep performance killer. Marketing installs one analytics pixel, then a chat widget, then a heatmap tool, then an A/B testing script — each justified individually but together adding hundreds of KB and 1-2 seconds of execution. Median sites carry 20-30 third-party scripts. The fix is audit, defer, lazy-load, and set a budget that someone owns. This guide walks through each step plus the GTM-specific patterns that compound quickly.
<!-- Before — synchronous, blocks render --> <script src="https://cdn.heatmap.com/tracker.js"></script> <script src="https://cdn.chat.com/widget.js"></script> <!-- After — non-blocking --> <script async src="https://cdn.heatmap.com/tracker.js"></script> <script defer src="https://cdn.chat.com/widget.js"></script>
See the render-blocking JS guide for the rules of async vs defer.
Chat widgets and similar large scripts shouldn't load on initial page load — they should load when user shows intent.
// Idle imports — pre-fetches without blocking
function lazyLoadChat() {
const script = document.createElement('script');
script.src = 'https://widget.intercom.io/widget/APP_ID';
script.async = true;
document.body.appendChild(script);
// Remove the trigger so we don't load twice
removeTriggers();
}
function removeTriggers() {
['mousemove', 'touchstart', 'click', 'scroll'].forEach(evt => {
document.removeEventListener(evt, lazyLoadChat);
});
}
['mousemove', 'touchstart', 'click', 'scroll'].forEach(evt => {
document.addEventListener(evt, lazyLoadChat, { once: true, passive: true });
});
// Idle 5s after page load
window.addEventListener('load', () => {
setTimeout(lazyLoadChat, 5000);
});
// Visible support button with placeholder; loads widget on click
document.querySelector('.support-button').addEventListener('click', async () => {
await loadChat();
// Open the widget once loaded
if (window.Intercom) window.Intercom('show');
});
Google Tag Manager is the most common culprit — a single GTM container can hold dozens of tags, all firing on All Pages by default.
For sites where GTM has become a bottleneck:
Some third-party services let you self-host their scripts. Pros: cached with your domain, served with your headers, no DNS lookup. Cons: must update periodically.
# Build script: fetch latest GA4 script and serve from your domain curl -o public/js/gtag.js https://www.googletagmanager.com/gtag/js?id=G-XXXX # Then in HTML <script async src="/js/gtag.js"></script>
Some scripts (Stripe.js, recaptcha) require their CDN for security and version pinning. Don't self-host those.
# Lighthouse CI
{
"ci": {
"assert": {
"assertions": {
"third-party-summary": ["error", { "maxNumericValue": 300 }],
"resource-summary:third-party:size": ["error", { "maxNumericValue": 250000 }]
}
}
}
}
Budget: max 300ms total third-party blocking time, max 250 KB third-party transfer. Builds that exceed fail. The budget creates an enforced ceiling — adding a new tag means removing an old one or finding 50 KB elsewhere.