You're In The Right Place If…
- Lighthouse Best Practices flags "Uses deprecated APIs" pointing at snap.licdn.com (LinkedIn Insight Tag) — usually the Attribution Reporting deprecation
- A SECOND deprecation warning blames a "1st party" blob: URL on your own domain — a script you never wrote and cannot find in your source
- "Browser errors were logged to the console" from an ad beacon — bat.bing.com returning 400, LinkedIn px/li_sync returning 429, or similar
- You already deferred a tag with a setTimeout fallback and Lighthouse STILL catches it
- The warnings point at files like insight.old.min.js or zi-tag.js that you cannot edit
Whose Deprecated Code Is It? (How To Read These Warnings)
The audit names a source file, and that tells you the owner. snap.licdn.com/li.lms-analytics/insight.old.min.js is the LinkedIn Insight Tag — the Attribution Reporting deprecation ships inside it. A bat.bing.com 400 or a linkedin.com/px/li_sync 429 in the console is an ad beacon misbehaving server-side. And the sneakiest one: a warning blamed on blob:https://www.yoursite.com/{random-uuid} looks first-party — it is on YOUR domain — but a blob: URL is a script another script created in the browser. In our audit, that blob (and its deprecated unload listeners) was manufactured by ZoomInfo's zi-tag.js. Your own code is rarely the culprit; the tag stack is.
The wrong fixes: blocking the domains in a CSP (breaks the tags and logs new console errors), asking the vendor to hurry (good luck), or removing the tags entirely (marketing will find you). The right fix costs nothing: keep every tag, change WHEN it loads.
The Universal Interaction Gate (copy, Then Customize)
One self-contained block replaces every individually-pasted marketing tag. It waits for the first real human input, then loads everything at once. Below it carries LinkedIn, ZoomInfo, and Bing UET; the next section shows how to add anything else.
IMPORTANT — replace the placeholder values before pasting: YOUR_LI_PARTNER_ID (from your existing LinkedIn tag's _linkedin_partner_id line), YOUR_ZI_PROJECT_KEY (from your ZoomInfo snippet — if yours is the obfuscated "Generic Script 2023" version, the long hex string assigned at the end of the first line IS the key), and YOUR_UET_TAG_ID (the ti value in your Bing tag). Then DELETE the original standalone tags — the gate replaces them, it does not sit alongside them.
<!-- Deferred marketing tags: load on first user interaction only -->
<!-- REPLACE: YOUR_LI_PARTNER_ID, YOUR_ZI_PROJECT_KEY, YOUR_UET_TAG_ID -->
<script>
(function () {
var fired = false;
function loadDeferredTags() {
if (fired) return; fired = true;
// --- LinkedIn Insight Tag ---
window._linkedin_partner_id = "YOUR_LI_PARTNER_ID";
window._linkedin_data_partner_ids = window._linkedin_data_partner_ids || [];
window._linkedin_data_partner_ids.push(window._linkedin_partner_id);
if (!window.lintrk) { window.lintrk = function (a, b) { window.lintrk.q.push([a, b]); }; window.lintrk.q = []; }
var li = document.createElement('script');
li.async = true;
li.src = 'https://snap.licdn.com/li.lms-analytics/insight.min.js';
document.head.appendChild(li);
// --- ZoomInfo WebSights ---
window.ZIProjectKey = 'YOUR_ZI_PROJECT_KEY';
var zi = document.createElement('script');
zi.async = true;
zi.src = 'https://js.zi-scripts.com/zi-tag.js';
document.body.appendChild(zi);
// --- Microsoft Ads UET (Bing) ---
(function(w,d,t,r,u){var f,n,i;w[u]=w[u]||[],f=function(){var o={ti:"YOUR_UET_TAG_ID", enableAutoSpaTracking: true};o.q=w[u],w[u]=new UET(o),w[u].push("pageLoad")},n=d.createElement(t),n.src=r,n.async=1,n.onload=n.onreadystatechange=function(){var s=this.readyState;s&&s!=="loaded"&&s!=="complete"||(f(),n.onload=n.onreadystatechange=null)},i=d.getElementsByTagName(t)[0],i.parentNode.insertBefore(n,i)})(window,document,"script","https://bat.bing.com/bat.js","uetq");
}
['pointerdown', 'keydown', 'scroll', 'touchstart'].forEach(function (ev) {
window.addEventListener(ev, loadDeferredTags, { once: true, passive: true });
});
})();
</script> Adding ANY Other Troublesome Script To The Gate
The gate is extensible by design: any tag vendor gives you a snippet that (a) optionally sets some window config and (b) injects an external script. Move both parts inside loadDeferredTags(), above the closing brace of the function. The recipe for any vendor: keep their config lines (window.whatever = ...) as-is, then create the script element yourself instead of letting their snippet run at parse time.
Two rules keep it safe. First, keep vendor STUB functions intact — LinkedIn's lintrk stub above queues any conversion calls made before the real script arrives, and most vendors (Meta's fbq, TikTok's ttq, Twitter's twq) work the same way, so nothing errors even if other code calls them early. Second, one gate for everything — do not stack multiple gates with different triggers, or you will reintroduce ordering bugs. Candidates that belong in the gate: ad pixels (Meta, TikTok, Reddit, Twitter/X), intent trackers (ZoomInfo, 6sense, Clearbit, Leadfeeder), heatmaps and session recorders (Hotjar, Clarity, FullStory), and chat widgets. Things that do NOT belong in it: your core analytics (GA4, HubSpot analytics, Matomo) — deferring those genuinely costs you bounce and attribution data, and they rarely trigger Lighthouse warnings.
// Template for adding any vendor to the gate — inside loadDeferredTags():
// --- VendorName ---
window.VENDOR_CONFIG_LINES_GO_HERE = '...'; // keep their config as-is
var vx = document.createElement('script');
vx.async = true;
vx.src = 'https://their-cdn.example.com/their-tag.js';
document.head.appendChild(vx);
// Bonus: HubSpot chat can join the gate too. In your site HEADER add:
// <script>window.hsConversationsSettings = { loadImmediately: false };</script>
// ...then inside loadDeferredTags():
function loadChat() { window.HubSpotConversations.widget.load(); }
if (window.HubSpotConversations) { loadChat(); }
else { window.hsConversationsOnReady = (window.hsConversationsOnReady || []).concat(loadChat); } Trap #1: The SetTimeout Fallback That Leaks Into The Audit
The tempting design is "first interaction OR a 3-second timer, whichever comes first" — insurance against visitors who never interact. Do not do it. A throttled Lighthouse mobile run observes the page for 10–30 seconds, so any realistic timer fires MID-AUDIT and hands Lighthouse the tag anyway. We watched this exact leak survive a "fixed" deployment: the deferral was correct, the timer betrayed it.
Interaction-only is safe because Lighthouse simulates zero input — no mouse, no scroll, no keys. And skip mousemove as a trigger too: when you run Lighthouse from DevTools, the page reloads in your visible tab, and your own idle mouse over the viewport fires it mid-audit. pointerdown + keydown + scroll + touchstart cover every engaged human. The visitors you lose are the ones who load the page and touch nothing — statistically, bounces your ad platforms should not be optimizing toward anyway. And test on pagespeed.web.dev rather than DevTools: remote lab, guaranteed zero input, and a fresh IP (rerunning audits repeatedly from your own machine can even get you rate-limited — a 429 from linkedin.com/px/li_sync in your console is exactly that).
Trap #2: Your Platform Is Injecting A Second Copy Of The Tag
This is the one that nearly kept us off 100. The deferral gate was live and verified — ZoomInfo stayed out of the load window — but LinkedIn kept appearing with zero interaction. The page source was clean: one deferred copy, no eager copy. The culprit: the site platform's ads integration was injecting ANOTHER LinkedIn Insight Tag at runtime through its own tracking loader — invisible in the HTML, enabled months earlier when someone connected the LinkedIn Ads account. On HubSpot that lives under Settings → Marketing → Ads → tracking toggles; Shopify, Wix, and WordPress ad plugins have equivalents.
Symptoms that this is happening to you: the tag loads with no interaction even though your gate is correct, or the vendor's pixel helper extension reports two page loads per view. Beyond the Lighthouse noise, a duplicate tag double-fires conversions and quietly corrupts campaign optimization — so this fix pays for itself even without the score. The rule from our Facebook Pixel guide applies to every network: ONE tag per network, installed through EXACTLY ONE route.
How we caught the blob: script owner, for the curious: paste-run a small createObjectURL wrapper in the console before load and it logs which script manufactures each blob — that is how the "first-party" unload warning traced back to zi-tag.js. Ten lines of instrumentation beat an hour of guessing.
Verify Like A QA Engineer, Not By Rerunning Lighthouse
Before burning another audit run, prove the gate in DevTools: open the site in an incognito window, Network tab, and DO NOT touch the page. Filter for licdn, zi-scripts, and bat.bing — all three should show zero requests after ten seconds. Your analytics beacons (GA4 collect, HubSpot ptq.gif) should be present. Then scroll once: all three tags should appear within a second or two, followed by their tracking beacons. Two phases, both green = both Lighthouse and the marketing team stay happy. THEN run pagespeed.web.dev for the official number.
The Fix, Step By Step
- 1
Inventory every marketing tag and where it is installed
Incognito + DevTools Network tab on a fresh load with zero interaction. Note each ad/intent/heatmap domain that loads (licdn, zi-scripts, bat.bing, connect.facebook, etc.) and find where each is installed: header/footer HTML, tag manager, or a platform ads integration.
- 2
Build one interaction gate and move the tags into it
Use the gate above. Copy each vendor's config values (partner IDs, project keys, tag IDs) out of the existing snippets into the gate, then DELETE the original standalone snippets. Do not keep both.
- 3
Use interaction-only triggers — no timer, no mousemove
pointerdown, keydown, scroll, touchstart with { once: true, passive: true }. A timer fallback fires during the 10–30s audit window and defeats the whole exercise; mousemove fires from your own idle cursor during DevTools runs.
- 4
Hunt platform auto-injection (the duplicate-tag trap)
If a tag still loads with zero interaction after your gate is live, your platform is injecting a second copy: check HubSpot Settings → Marketing → Ads tracking toggles (or your platform's equivalent) and disable injection for networks you now load through the gate.
- 5
Leave core analytics alone
GA4, HubSpot analytics, and similar page-view measurement stay eager — deferring them costs real data and they rarely cause Lighthouse warnings. The gate is for ad pixels, intent trackers, heatmaps, and chat.
- 6
Verify in two phases, then audit remotely
Phase 1 (no touch): tag domains absent, analytics present. Phase 2 (one scroll): tags load and fire. Then run pagespeed.web.dev — remote, zero-input, fresh IP — for the official score.
From the trenches
How we hit this on a real production site
An enterprise B2B site we were tuning this week was stuck below 100 on Best Practices with exactly the errors above: LinkedIn's Attribution Reporting deprecation, a deprecated unload listener blamed on a mystery first-party blob: URL, and later a bat.bing.com 400 in the console. Three different audits, three different "sources" — all marketing tags.
The fix unfolded in layers. The blob traced back to ZoomInfo's tag. A first deferral attempt used a 3.5-second timer fallback — Lighthouse caught the tags anyway, because its throttled run outlasts any sane timer. Interaction-only triggers fixed ZoomInfo but LinkedIn STILL loaded with zero interaction, which exposed the real villain: the site platform's ads integration had been silently injecting a second LinkedIn tag for months. One toggle later — and with Bing UET moved into the same gate after it threw a 400 on the desktop run — the site hit a verified 100, with page-view analytics untouched and every ad tag still firing on first scroll.
Total code: one script block. Total cost to marketing: tags fire a few hundred milliseconds later, on engaged visitors only — and the accidental double-counting from the duplicate LinkedIn tag stopped. If your report is showing warnings from files you cannot edit, this pattern is the answer — and if you would rather hand the whole tag-stack forensics job to someone, that is literally what we do.
Frequently Asked Questions
Will deferring ad tags hurt my conversion tracking or retargeting audiences?
Marginally at most. Every visitor who scrolls, clicks, taps, or types still loads every tag — typically within a second or two of arriving. You lose only zero-interaction bounces, which are low-intent traffic ad platforms should not optimize toward anyway. Conversion calls made before a tag loads are queued by the vendor stubs (lintrk, fbq, uetq) and replayed when it arrives.
Why not just use a timer as a safety net?
Because Lighthouse's throttled audit observes the page for 10–30 seconds — longer than any timer you would realistically set. The tag loads mid-audit and every warning comes back. Interaction-only is the only version Lighthouse can never trigger, because it simulates zero user input.
What was the "first party" blob: URL in my deprecated-API warning?
A script that another script created at runtime via createObjectURL — it carries your domain, but the code inside belongs to whoever manufactured it (in our case ZoomInfo's zi-tag.js, which registers the deprecated unload listeners). Defer the parent tag and the blob and its warnings disappear with it.
My deferral is correct but the tag still loads with no interaction. How?
Almost certainly a second copy injected by your platform: HubSpot's Ads settings, Shopify's marketing integrations, or a WordPress plugin connected to the ad network. It injects at runtime through the platform's loader, so it is invisible in your page source. Disable the platform's injection and keep only the gated copy — you were probably double-firing conversions too.
Should I put Google Analytics in the gate as well?
No. GA4 and platform analytics (HubSpot, Matomo) measure every visit, including bounces — deferring them punches a real hole in your data. They also rarely log deprecated-API warnings or console errors. Gate the ad pixels, intent trackers, heatmaps, and chat widgets; leave measurement eager.
A tag beacon returns 429 (Too Many Requests) during my audits — is that my fault?
It usually means the vendor is rate-limiting YOUR IP because you re-ran audits many times in a row from the same machine. Test from pagespeed.web.dev instead — remote runner, fresh IPs — and the 429 disappears. With the gate in place the tag should not load during audits at all anyway.
Still stuck? Send us the exact error.
Paste the exact error message you're seeing and where it happens. We'll take a look — if it's quick we'll point you at the fix, and if it's deeper we'll tell you honestly what it takes.