Isolating Analytics and Tag Manager Scripts From App Chunks
A vendor-chunk audit turns up something that should not be there:
vendor-app.9c31de.js 182.4 kB
├─ react-dom 41.2 kB
├─ @analytics-vendor/sdk 38.7 kB ← third-party
├─ @session-replay/core 31.4 kB ← third-party
├─ router 12.8 kB
└─ …
Seventy kilobytes of third-party instrumentation, bundled into the same chunk as the framework, evaluated on the critical path, and re-downloaded by every user whenever a tracking configuration changes.
This is a specific failure of the boundaries described in vendor chunk isolation and third-party management. Vendor isolation groups dependencies by change frequency; analytics SDKs installed from npm slip through because, to the bundler, they are indistinguishable from any other dependency.
Root cause: an npm-installed tag is application code to the bundler
Analytics vendors ship two delivery paths for the same product: a hosted script you reference by URL, and an npm package you import. The package is more convenient — real types, no global sniffing, a testable interface — and it puts the vendor’s code inside your bundle, where it inherits every property of your own code.
That inheritance is the problem:
- Cache lifetime. Vendor code now shares a content hash with application code. A tracking change invalidates chunks containing the framework, as described in stabilizing chunk hashes to maximize cache hits.
- Scheduling. Code inside an application chunk evaluates when that chunk evaluates — during hydration, competing with the work that makes the page interactive.
- Failure coupling. An exception thrown at module scope by a vendor SDK is an exception inside your entry chunk, which can prevent the application from starting at all.
Fix 1: force the tags into their own chunk
Both bundlers can carve third-party instrumentation into a dedicated chunk by package pattern.
// webpack.config.js — Webpack 5
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
// Higher priority than the general vendor group, so instrumentation
// never lands in the same chunk as the framework.
tags: {
test: /[\\/]node_modules[\\/](@analytics-vendor|@session-replay|@tag-manager)[\\/]/,
name: 'tags',
chunks: 'async', // reachable only through the deferred loader
priority: 30,
enforce: true, // create the chunk even below minSize
},
},
},
},
};// vite.config.js — Vite 5+
export default {
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (/node_modules\/(@analytics-vendor|@session-replay|@tag-manager)\//.test(id)) {
return 'tags';
}
},
},
},
},
};Note chunks: 'async' in the Webpack group: it only takes effect for code reached through a dynamic import. If anything still imports the SDK statically from the entry, the group cannot help, because a statically-reachable module must be in a chunk the entry loads.
Fix 2: load after interactive, with a buffer
Isolating the chunk enables scheduling; the loader decides when. The right moment is after the application is interactive, during idle time.
// tags.js — one loader for every third-party tag
const queue = [];
let ready = false;
// Public API the application calls from the first millisecond.
export function track(event, props) {
if (ready) return window.__analytics.track(event, props);
// Buffer: events fired before the vendor script loads are the most
// valuable ones in the session, and would otherwise be dropped silently.
queue.push([event, props]);
}
const idle = window.requestIdleCallback || ((fn) => setTimeout(fn, 1500));
export function initTags() {
idle(async () => {
try {
const { createAnalytics } = await import(/* webpackChunkName: "tags" */ '@analytics-vendor/sdk');
window.__analytics = createAnalytics({ token: window.__ANALYTICS_TOKEN__ });
ready = true;
for (const [event, props] of queue.splice(0)) window.__analytics.track(event, props);
} catch (error) {
// A vendor outage must never surface to the user or break the app.
queue.length = 0;
}
}, { timeout: 5000 });
}Three properties make this safe. The application never imports the SDK directly, so no static edge can pull it back in. Events are buffered rather than dropped, so deferring costs no data. And the catch is total: a failed vendor fetch clears the queue and the application continues, which is the lazy chunk failure handling pattern applied to code nobody should notice.
Fix 3: prefer the hosted script where the SDK is large
For a session-replay or heat-mapping SDK measured in tens of kilobytes, the vendor’s hosted script is often the better delivery path despite the worse developer experience: the bytes never enter your bundle, the vendor’s own CDN serves them, and updates do not require a deploy. Wrap it in a thin typed façade so application code still has one interface.
The trade is a third-party origin in the critical connection path and less control over versioning. Decide per SDK by size: below roughly 10 KB, the npm package in an isolated chunk is simpler; above that, hosted delivery usually wins.
Step-by-step verification
-
Search the app chunks for vendor code. Grep the built application chunks for a distinctive vendor string. Any hit means the isolation rule is not matching.
-
Confirm the tag chunk loads late. In the network panel, the tag chunk must be requested after the route chunk has evaluated, not alongside it.
-
Block the vendor origin. With the origin blocked, the application must render and function completely, with no console errors surfacing to the user.
-
Confirm the buffer flushes. Fire a tracked event immediately on load, then confirm it reaches the vendor after the tag chunk resolves.
-
Confirm hash independence. Change a tag configuration value, rebuild, and verify only the tag chunk’s hash changed.
-
Re-measure interactivity. Compare Interaction to Next Paint before and after. Removing tag evaluation from hydration is usually worth 30–80 ms on mid-tier mobile hardware.
Edge cases and gotchas
Consent management must load early. A consent banner is not an analytics tag: it gates them, so it has to be available before anything it governs. Keep consent logic in the application chunk and defer only what it permits.
Tag managers loading further tags. A tag manager is a loader for other vendors, so isolating it does not bound what it subsequently pulls in. Audit what it injects at runtime, not just what it costs at build time.
Server-side rendering. A tag SDK that touches browser globals at module scope throws during a server render if it is statically imported. Deferred loading fixes this incidentally, but only if no static import remains.
Duplicate loading. A vendor delivered both by npm and by a hosted script tag runs twice, usually double-counting events. Check for both paths when adopting the npm package.
FAQ
Why is an analytics SDK in my application chunk at all?
Because it was installed as an npm dependency and imported like any other module, so the bundler treated it like application code. The vendor’s hosted script tag and their npm package are two different delivery paths for the same product, and only the first keeps the code out of your bundle. Importing the package is more convenient and gives better typing, at the cost of putting third-party bytes inside chunks you cache and version.
Does isolating tags into their own chunk actually help?
It helps in two distinct ways. Cache stability improves because a tag configuration change no longer invalidates chunks containing application code. And scheduling becomes possible: an isolated chunk can be loaded during idle time after the application is interactive, whereas code merged into an application chunk is fetched and evaluated whenever that chunk is, which is usually on the critical path.
What happens to events fired before the analytics script loads?
They are lost unless you buffer them. A small in-application queue that records calls and flushes them once the vendor script is ready costs a few hundred bytes and preserves the events that happen during the first seconds of a session — which are frequently the most valuable ones. Without it, deferring the tag silently trades data quality for performance, which is rarely the trade anyone intended.
Related
- Vendor Chunk Isolation and Third-Party Management — the parent guide on grouping dependencies by change frequency
- Splitting a Vendor Chunk That Grew Past the Budget — what to do when the remaining vendor chunk is still too large
- Configuring Vite manualChunks for Vendor Isolation — the Rollup-side assignment mechanics
- Handling Lazy Chunk Load Failures and Fallbacks — why a failed tag fetch must stay invisible