Installing airgap.js: Synchronous vs Asynchronous
This guide gives you the exact setup for both ways to load airgap.js, with complete, paste-ready markup.
airgap.js regulates network requests, cookies, and DOM insertions like a client-side firewall. It can only regulate what happens after it initializes.
Nothing may track before
airgap.jsis ready.
There are exactly two correct ways to honor that rule:
- Synchronous: load
airgap.jsas the very first script, so it is already regulating by the time anything else runs. You then add trackers normally. - Asynchronous: load
airgap.jsoff the critical path and gate every tracker, so none start untilairgap.jsis ready.
Use synchronous loading. It is the simplest setup that is correct by default: airgap regulates everything after it, so you don't have to touch your tracker code. The one
<script>in<head>blocks briefly while the bundle loads.Choose asynchronous only if you have a measured need to keep
airgap.jsoff the critical rendering path (e.g. strict LCP budgets). The tradeoff: you become responsible for gating every tracker, and any that slips through fires unregulated.
| Synchronous (recommended) | Asynchronous | |
|---|---|---|
| How airgap loads | Blocking <script>, first in <head> | Injected async after an inline stub |
| Who guarantees "nothing tracks first" | airgap (it's first) | You gate every tracker |
| Tracker code changes | None; add trackers normally | Every tracker moved behind a ready gate |
| Best for | Almost everyone | Strict performance budgets, SPAs |
How it works: because airgap.js is the first script on the page, it is already intercepting requests before any tracker runs. You add analytics and marketing scripts exactly as their vendors document them, with no gating or changes on your side, and airgap regulates them for you.
Copy it from Developer Settings → Installation. Your bundle URL looks like https://transcend-cdn.com/cm/<your-bundle-id>/airgap.js.
It must come before any script, pixel, or resource hint that could make a tracking request.
Place every tracker below the airgap tag; they load normally.
Everything below the airgap tag is regulated automatically. The airgap tag is first in <head>, and the trackers (Amplitude and a marketing pixel) come after it, unmodified.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<!-- 1) Load airgap.js first, before all other scripts/trackers.
data-cfasync="false" stops Cloudflare Rocket Loader (and similar
optimizers) from deferring or reordering it. -->
<script
data-cfasync="false"
src="https://transcend-cdn.com/cm/<your-bundle-id>/airgap.js"
></script>
<!-- Everything below is regulated by airgap.js -->
<!-- 2) Amplitude, load its library, then initialize. -->
<script src="https://cdn.amplitude.com/libs/analytics-browser-2-min.js"></script>
<script>
window.amplitude.init("AMPLITUDE_API_KEY", { autocapture: true });
</script>
<!-- 3) Any other (non-Google) marketing pixel / tag. -->
<script src="https://example-pixel.com/pixel.js"></script>
<!-- Google tags (gtag.js / GTM)? See the Google Consent Mode section below. -->
</head>
<body>
<!-- your page -->
</body>
</html>- Keep
data-cfasync="false". Cloudflare Rocket Loader and similar tools will otherwise defer/reorder the tag and break the "first script" guarantee. - Put airgap above resource hints that fetch, such as
<link rel="preload">/preconnectfor tracker hosts. - Using a tag manager (e.g. GTM)? Put the
airgap.jstag above the container snippet so the non-Google tags it injects are regulated at the network level. Don't rely on the tag manager itself to enforce consent; it's leaky. For Google tags, use Consent Mode (see the Google Consent Mode section below). - Server-rendered React, Next.js, or Hydrogen? Render this exact tag as the first element of
<head>on both server and client. If the tag only exists in server HTML, hydration can remove it and break initialization. If that's hard, use async loading (Part 2) or see the React Snippets guide.
How it works: you load airgap.js off the critical path. Until it's ready, nothing is regulated, so you must hold back every tracker and start each one only once airgap signals ready.
Recommended pattern: a tiny inline pre-init stub (synchronous, so the ready queue exists immediately) that exposes a memoized getAirgap() promise. Every tracker initializes inside await getAirgap(), so airgap loads once and trackers start only when it's regulating.
Same as above, from Developer Settings → Installation.
This inline block is synchronous and tiny. It defines the ready queue and injects airgap.js asynchronously. Put it first so the queue exists before anything else runs.
Load each tracker's library and start it inside the ready gate, using a small loadScript helper, so even the library fetch waits until airgap.js is regulating. Nothing that touches the network may run outside the gate.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<!-- 1) First: pre-init stub + getAirgap() helper (inline, synchronous, tiny).
This defines the ready queue and injects airgap.js asynchronously. -->
<script>
// Get your bundle ID at
// https://app.transcend.io/consent-manager/developer-settings/installation
var BUNDLE_ID = '<your-bundle-id>';
var airgapAPI;
window.getAirgap = function () {
return (airgapAPI =
airgapAPI ||
new Promise(function (resolve, reject) {
// Stub the ready queue before airgap.js exists.
if (!(self.airgap && self.airgap.ready)) {
self.airgap = Object.assign(
{
readyQueue: [],
ready: function (cb) { this.readyQueue.push(cb); },
},
self.airgap
);
}
// Resolve once airgap.js has loaded and is ready.
self.airgap.ready(function (airgap) { resolve(airgap); });
// Inject airgap.js asynchronously.
var s = document.createElement('script');
s.addEventListener('error', reject);
s.async = s.defer = true;
// Optional load options, e.g.:
// s.dataset.lazyLoadUi = 'on';
s.src = 'https://transcend-cdn.com/cm/' + BUNDLE_ID + '/airgap.js';
document.documentElement.appendChild(s);
}));
};
</script>
<!-- 2) Gate: after airgap.js is ready, load each tracker's script,
then initialize it. Even the library fetch happens after airgap is
regulating, so nothing touches the network too early. -->
<script>
// Inject a <script> element and resolve once it has loaded.
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.async = true;
script.src = src;
script.onload = () => resolve();
script.onerror = reject;
document.head.appendChild(script);
});
}
(async () => {
// 1. Wait until airgap.js is loaded and regulating.
await getAirgap();
// 2. Amplitude: load its library, then initialize.
await loadScript("https://cdn.amplitude.com/libs/analytics-browser-2-min.js");
window.amplitude.init("AMPLITUDE_API_KEY", { autocapture: true });
// 3. Any other (non-Google) marketing pixel / tag.
await loadScript("https://example-pixel.com/pixel.js");
// Google tags (gtag.js / GTM)? See the Google Consent Mode section below.
})();
</script>
</head>
<body>
<!-- your page -->
</body>
</html>Beware: you are now importing
airgap.jsasynchronously. Since it can only regulate traffic after it loads, you are responsible for ensuring no trackers load beforeairgap.js. The caveat applies only to the window before init; once airgap is ready, quarantine and replay handle later consent changes on the same page.
- Load tracker libraries inside the gate, too. Don't add ungated tracker
<script>tags to<head>; use theloadScripthelper so even the library fetch happens afterairgap.jsis regulating. - Extend, never overwrite
self.airgap. The stub above usesObject.assign({…}, self.airgap)for exactly this reason: clobbering it destroys any queue another script already registered. - React, Next.js, or Hydrogen: load airgap once at app startup (a provider), gate trackers with a hook, and remember the async React pattern does not load airgap first. Use the React Snippets for airgap guide.
This is the same whether you load airgap synchronously or asynchronously. Google tags (gtag.js / GTM) are a special case: don't gate them behind airgap. Google Consent Mode is a signal integration: airgap passes consent state to Google and Google's own tags self-regulate (in Advanced mode they load early and send cookieless pings before consent), so gating would suppress that.
Instead, set safe denied consent defaults synchronously, before airgap.js loads. Then load airgap (the sync tag from Part 1, or the async stub from Part 2) and let your Google tags run normally; airgap pushes the regime default and consent updates as they resolve. Your non-Google trackers still follow the mode you chose: after airgap in sync, inside the gate in async.
<!-- FIRST in <head>: denied Consent Mode defaults (synchronous, before airgap.js) -->
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){ dataLayer.push(arguments); }
gtag('set', 'developer_id.dODQ2Mj', true); // Transcend Consent Mode developer ID
gtag('consent', 'default', {
analytics_storage: 'denied',
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
functionality_storage: 'denied',
personalization_storage: 'denied',
security_storage: 'denied',
});
</script>
<!-- then load airgap.js, and your Google tags load normally -->The snippet above is for sites that load Google tags directly with gtag.js. With Google Tag Manager, keep your standard container snippet; the one thing to change is placement. Google tells you to paste the GTM container "as high in <head> as possible," but with Transcend the denied defaults and airgap.js go above the GTM container, so airgap regulates everything GTM injects. The GTM <body> <noscript> block is unchanged.
<!doctype html>
<html>
<head>
<!-- 1) Denied Consent Mode defaults (synchronous, first) -->
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){ dataLayer.push(arguments); }
gtag('set', 'developer_id.dODQ2Mj', true); // Transcend Consent Mode developer ID
gtag('consent', 'default', {
analytics_storage: 'denied',
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
functionality_storage: 'denied',
personalization_storage: 'denied',
security_storage: 'denied',
});
</script>
<!-- 2) airgap.js, above the GTM container so it regulates everything GTM
injects. (Loading airgap async? Use the Part 2 stub here instead.) -->
<script
data-cfasync="false"
src="https://transcend-cdn.com/cm/<your-bundle-id>/airgap.js"
></script>
<!-- 3) Your standard GTM container snippet, unchanged -->
<!-- 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-XXXXXXX');</script>
<!-- End Google Tag Manager -->
</head>
<body>
<!-- Google Tag Manager (noscript), unchanged -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- your page -->
</body>
</html>Prefer not to touch page code? Set the defaults inside GTM on the built-in Consent Initialization – All Pages trigger, or load airgap from within GTM using Transcend's GTM template. Basic vs Advanced mode, the purpose mapping, and the full GTM template are covered in the Google Consent Mode guide.
- Network tab: hard-reload and confirm no tracker request fires before
airgap.js. In sync mode,airgap.jsis the first request; in async mode, no tracker should hit the network until after it. airgap.export(): run it in the console before and after granting consent on the same page. Blocked requests should appear while unconsented and drain after consent. (If they appear but never drain, you have an initialization/hydration problem.)- Google Tag Assistant (if using GCM): confirm you do not see "A tag read consent state before a default was set."
See Consent Management Debugging and Testing for more.