# Third Party Scripts

> Audit and optimize third-party scripts — analytics, tag managers, chat widgets, embeds — with the right loading strategy, performance budget, facades, and CSP/consent controls. Use when adding a script, when TBT/INP regress, when a GDPR/CCPA consent requirement arises, or before shipping. Not for first-party bundle size (use bundle-optimization) or broad Core Web Vitals diagnosis (use rendering-performance).

- Skill: `jaykim88/third-party-scripts` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jaykim88/third-party-scripts`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jaykim88/third-party-scripts/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: JayKim88 (https://skillmd.com/u/jaykim88)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/jaykim88/third-party-scripts

---


# Third-Party Scripts

## Purpose
Keep third-party scripts (analytics, tag managers, chat, A/B tools, embeds) from silently destroying performance and privacy. Each script earns its place: right loading strategy, measured cost, CSP-controlled, consent-gated where required.

**Universal** — loading-strategy classification, performance budgeting, facade patterns, and CSP/consent controls apply to any site; only the script-injection helper differs by framework.

## Procedure

1. **Inventory every third-party script**
   - List all external scripts (analytics, tag managers, ads, chat, embeds, hosted fonts)
   - For each: purpose, owner, business-critical? — scripts with no owner or unclear purpose are removal candidates

2. **Assign a loading strategy per script**
   - **Critical & needed early** (consent manager, core analytics) → load high priority, but still async
   - **Important, not render-blocking** (most analytics) → load after hydration / when the main thread is idle
   - **On-interaction only** (chat widget, video embed) → lazy-load on click / scroll via a **facade**
   - Never inject a render-blocking synchronous `<script>` in `<head>`

3. **Use a facade for heavy embeds**
   - Chat widgets, YouTube / maps embeds, social widgets often cost 500KB+ each
   - Render a static placeholder; load the real script only on user intent (click)
   - This is the single biggest win for INP / TBT on widget-heavy pages

4. **Prefer server-side / first-party where possible**
   - Tag managers and analytics increasingly support server-side containers — moves work off the client
   - Self-host stable scripts (fonts, small libs) to cut a third-party origin connection

5. **Set a third-party performance budget**
   - Cap total third-party JS (e.g., ≤ 100KB transfer, ≤ 200ms main-thread on mid-tier mobile)
   - Each new script must fit the budget or displace an existing one — measure with the WebPageTest "3rd party" breakdown or Lighthouse

6. **Connection hints for unavoidable origins**
   - `preconnect` / `dns-prefetch` for the script origin (saves TCP/TLS handshake) — coordinate with `api-caching-optimization`
   - Only preconnect the few origins that matter; each one costs a connection

7. **Security + privacy controls**
   - Allowlist each script origin in CSP `script-src` (coordinate with `security-audit`) — no blanket `'unsafe-inline'`
   - Add Subresource Integrity (SRI) hash for scripts served from a fixed URL
   - Consent-gate tracking scripts — don't load before opt-in where GDPR / CCPA applies. Consent must gate *loading*, not just configuration; in the EEA, Google Consent Mode v2 is required for ads/analytics tags, and the consent manager is the one script allowed to load first
   - **Tag managers are a runtime backdoor**: GTM lets non-engineers inject arbitrary scripts that bypass your review, budget, and CSP — govern who can publish, audit the container, and accept that its CSP story is painful (it pushes toward `'unsafe-inline'` or nonce gymnastics)

8. **Measure impact (validation loop)**
   - Record a trace before/after each script; if main-thread blocking or INP regresses past budget, downgrade its strategy (idle → on-interaction → facade) and re-measure
   - Loop until total third-party cost fits the budget

## Anti-patterns

| ❌ Anti-pattern | ✅ Correct |
|---|---|
| Synchronous `<script>` in `<head>` for analytics | Async, loaded after hydration / when idle |
| Chat widget loaded eagerly on every page | Facade placeholder → load on click |
| Blanket `script-src 'unsafe-inline'` for vendors | Allowlist each origin + nonce / SRI |
| Loading tracking before consent (GDPR) | Consent-gate loading (Consent Mode v2 in EEA); load only after opt-in |
| Tag manager any teammate can publish to | Governed publish access + container audit + CSP review |
| Adding scripts with no budget check | Each script must fit the third-party budget or displace one |

## Severity tiers

| Tier | Examples | Action SLA |
|---|---|---|
| **Critical** | Render-blocking sync third-party script in `<head>`; tracking loaded pre-consent in a GDPR region | Block release; fix immediately |
| **Major** | Eagerly-loaded heavy widget (chat / embed) inflating INP / TBT; third-party JS over budget; missing CSP allowlist; ungoverned tag manager (anyone can inject runtime scripts) | Fix this sprint |
| **Minor** | Missing `preconnect` for a used origin; unowned script of unclear value | Schedule within 2 sprints |

## Completion Criteria
- [ ] Every third-party script inventoried with purpose + owner
- [ ] Each script has an explicit loading strategy (none render-blocking)
- [ ] Heavy widgets / embeds use a facade (load on interaction)
- [ ] Total third-party JS within budget (default ≤ 100KB transfer)
- [ ] Each script origin allowlisted in CSP; SRI where applicable
- [ ] Tracking scripts consent-gated where required (Consent Mode v2 in EEA); tag-manager publish access governed
- [ ] All Critical findings fixed; all Major findings scheduled

## Output
- **Script inventory**: `docs/third-party-scripts.md` — script / purpose / owner / strategy / size / budget impact
- **Loading config**: per-script strategy (async / idle / on-interaction) + facades for heavy embeds
- **CSP update**: `script-src` allowlist + SRI hashes (coordinate with `security-audit`)
- **Budget report** (paste into PR): total third-party transfer + main-thread time before/after
- **Commit format**: `perf(3p): defer <script>` / `feat(3p): facade for <widget>`

## Implementation

### React + Next.js (default)
- Loading: `next/script` with `strategy` — `beforeInteractive` (rare, critical only), `afterInteractive` (default), `lazyOnload` (idle), `worker` (Partytown, experimental — runs off the main thread; test each script, those needing direct DOM/cookies often break in a worker)
- Facades: render a placeholder component, mount the real embed on click (e.g., `react-lite-youtube-embed`, lazy chat triggers)
- Connection hints: `<link rel="preconnect">` in the root layout
- CSP: `script-src` allowlist in `next.config.ts` `headers()` (see `security-audit`)
- Server-side: GTM server-side container; reverse-proxy analytics (PostHog, Vercel) to a first-party path

### Other stacks
- **Vue / Nuxt**: `useHead({ script: [{ src, defer }] })`, or `@nuxt/scripts` (purpose-built: facades, consent, perf strategies)
- **SvelteKit**: `<svelte:head>` for script tags; manual facade components; Partytown integration for worker offloading
- **Angular**: a script-loader service (or `Renderer2`) with an explicit per-script strategy; manual facades for embeds
- **Universal**: `<script async/defer>`, `preconnect` / `dns-prefetch`, CSP `script-src`, SRI, and the facade pattern are web-platform standards; Partytown (web-worker offloading) is framework-agnostic

## Related skills
- `rendering-performance` — third-party JS is a top INP / TBT regression source
- `bundle-optimization` — first-party bundle vs. third-party loaded scripts are different levers
- `security-audit` — external scripts need CSP allowlisting + SRI

## Reference
- **Key insight encoded**: Third-party scripts are the most common cause of poor INP / TBT because they run on the main thread outside your control. Default everything to `afterInteractive` / `lazyOnload`, use facades for heavy widgets (load on interaction), and hold every script to a measured budget — a script with no owner and no measured value is a removal candidate. Two governance traps: a tag manager is a runtime backdoor that bypasses your CSP/budget/review (govern publish access), and consent must gate script *loading* (Consent Mode v2 in the EEA), not just post-load configuration.

