Partytown vs Async Loading: Which Should Run Your Tag Manager?
This comparison belongs to Third-Party Script Performance within JavaScript Bundle Optimization & Code Splitting. A tag container costs 200–600ms of main-thread time on a mobile profile, and that time lands wherever the container happens to execute — frequently on top of a user interaction, where it becomes INP damage.
There are two credible answers. Move the work to another thread with a worker sandbox such as Partytown, or keep it on the main thread and control when it runs. They are not equivalent, and the wrong choice is expensive in different ways.
Rapid Diagnosis: What Does Your Container Actually Do?
- Does it write more than it reads? Analytics and conversion tags mostly send data out. Those are worker-friendly. Personalisation and A/B tools that read and mutate the DOM are not.
- When does it run today? Record an interaction, not a load. If container work appears inside your input handler's frame, the timing is your problem, not the total cost.
- How much does it cost? Under
100msof main-thread time, deferral is enough and worker isolation is over-engineering. - Can you verify events end to end? Worker isolation changes execution semantics. Without a way to compare event-for-event against a control, you cannot roll it out safely.
The Comparison
| Axis | Worker sandbox (Partytown) | Async / deferred loading |
|---|---|---|
| Main-thread cost | Near zero — proxy overhead only | Full cost, moved later |
| INP protection | Strong: tag work cannot block input | Partial: depends on when it runs |
| Setup complexity | High — service worker or proxy, CSP changes | Low — an attribute or a small loader |
| Vendor compatibility | Partial; some scripts detect and break | Complete |
| Debuggability | Harder — second execution context | Unchanged |
| Data fidelity risk | Real; needs event-for-event validation | Minimal |
| Reversibility | Config change, but validation must be redone | Immediate |
Root Cause Analysis: How Each Approach Fails
1. Worker isolation breaks synchronous DOM reads. Inside the sandbox, document.title or getBoundingClientRect() becomes an asynchronous proxy call. Scripts written assuming synchronous access either stall or silently take a fallback path — and the fallback is often "report nothing".
2. Some vendors detect the sandbox. A script that inspects window.top, checks for a service worker, or fingerprints its environment can decide it is being tampered with and disable itself.
3. Async loading leaves the work on the critical thread. async executes the container the moment it arrives, which can be during hydration or in the middle of a click. It reduces nothing; it merely randomises when the cost lands.
4. Interaction gating changes what you measure. Loading tags on first interaction means bounce visits are never recorded. That is a data-model change, not a performance detail, and it will be noticed in the weekly report if it was not agreed first.
Step-by-Step Resolution
1. Establish the baseline cost and the current timing. Record an interaction with the container enabled and disabled, and compare the input handler's frame. This tells you whether you have a cost problem or a timing problem.
2. If it is a timing problem, fix the timing. Deferred loading with an interaction trigger and an idle fallback solves most cases in a few lines.
<!-- Predictable: executes after parsing, in order, never mid-interaction. -->
<script defer src="https://tags.example.com/container.js"></script>
// Or gate it: no cost at all until the visitor engages.
const load = () => {
const s = document.createElement('script');
s.src = 'https://tags.example.com/container.js'; s.defer = true;
document.head.append(s);
};
['pointerdown', 'keydown', 'scroll'].forEach((e) =>
addEventListener(e, load, { once: true, passive: true }));
requestIdleCallback?.(load, { timeout: 6000 });
// trade-off: the idle fallback guarantees the container eventually runs, so
// bounce visits are still measured — at the price of the cost returning on
// pages where the user does nothing. Drop the fallback only if the data owner
// accepts losing those sessions.
Expected outcome: container work never overlaps an interaction; INP input delay attributable to the tag falls to near zero.
3. If it is a cost problem, sandbox it — with a validation plan. Worker isolation is worth the complexity when the container is genuinely expensive and mostly write-only.
<script type="text/partytown" src="https://tags.example.com/container.js"></script>
<script>
partytown = {
// Requests the worker cannot make directly are proxied through your origin.
resolveUrl: (url) => url.hostname === 'tags.example.com'
? new URL(`/proxy?u=${encodeURIComponent(url.href)}`, location.origin)
: url,
forward: ['dataLayer.push', 'gtag']
};
</script>
<!-- trade-off: `forward` is an allowlist. Any global the container expects to
call on the main thread and that is NOT listed silently does nothing —
which is exactly the failure mode that produces "we lost 8% of events". -->
Expected outcome: main-thread cost drops to proxy overhead, typically under 20ms, with the container's own work running off-thread.
4. Validate event fidelity before and after. Run both configurations against the same synthetic journey and diff the outbound requests.
# Capture the events each configuration emits for an identical scripted journey.
node scripts/journey.mjs --config=baseline > baseline.jsonl
node scripts/journey.mjs --config=partytown > worker.jsonl
diff <(jq -r '.event' baseline.jsonl | sort) <(jq -r '.event' worker.jsonl | sort)
# trade-off: a scripted journey covers the paths you thought of. Follow it with
# a percentage rollout and compare real event volumes for a week before going
# to 100% — silent loss is the failure mode that matters here.
Expected outcome: an explicit, evidenced answer to "did we lose events?", rather than a hope.
Verification
Confirm the main-thread effect first: record the same interaction after the change and check the bottom-up view for vendor script frames inside your handler. They should be absent (gated or deferred) or attributed to the worker (sandboxed).
Then confirm the metric moved in the field, segmenting INP by whether the session received the tag. A successful containment shows input delay dropping while processing duration is unchanged — because you removed contention, not your own handler's work.
Finally, keep watching the data side for a full reporting cycle. Performance work on tags is only successful if the measurement it exists to serve survives; a 300ms saving that costs 8% of conversion events will be reverted, correctly.
FAQ
Does Partytown work with every tag manager?
No. It works well with containers that mostly emit events and rarely read the DOM synchronously — most analytics and conversion tags fall in that group. Personalisation, A/B testing and session-replay tools that need synchronous layout access frequently misbehave or disable themselves. Test each tag individually rather than the container as a whole, because one incompatible tag inside a compatible container is enough to break the rollout.
Is async loading ever the right answer for a container?
Rarely. async gives you the download parallelism of defer without the execution predictability, so the container can run during hydration or in the middle of an interaction. defer is the better default in almost every case, and interaction gating is better still when the data owner accepts the trade. Reserve async for a tag that must fire as early as possible and genuinely has no ordering requirements.
How much main-thread time does the worker proxy itself cost?
Small but not zero — typically a few milliseconds of setup plus a fraction of a millisecond per proxied operation, since each one crosses a thread boundary. A container that performs thousands of DOM operations will therefore be slower overall in a sandbox, even though the main thread is less blocked. Profile the specific container; the technique suits chatty-but-shallow scripts, not DOM-heavy ones.
Can I combine the two approaches?
Yes, and it is often the best configuration: sandbox the measurement container, and interaction-gate the heavier optional tags that would not survive the worker. The two techniques address different tags rather than competing for the same one, and separating them also makes each rollout independently reversible.
Related
- Third-Party Script Performance — the wider containment strategy and per-origin cost accounting.
- Deferring non-critical analytics scripts safely — the mechanics of the deferred-loading option.
- Reducing input delay from third-party tags — measuring the INP damage this decision is meant to remove.