Skip to main content

How SaaS onboarding tools inject their code (and why you should care)

SaaS onboarding tools inject CDN scripts on every page load. Here are the performance, security, and maintenance costs vs. npm-installed libraries.

DomiDex
DomiDexCreator of Tour Kit
April 8, 20261 min read
Share
How SaaS onboarding tools inject their code (and why you should care)

How SaaS onboarding tools inject their code (and why you should care)

Every major SaaS onboarding platform (Appcues, Pendo, WalkMe, Userpilot, Chameleon) follows the same integration pattern. You paste a <script> tag into your app's <head>. On every page load, that script fetches the vendor's full JavaScript bundle from their CDN, attaches DOM observers to find target elements by CSS selectors, renders overlay UI directly into your document, and sends analytics pings back to the vendor's servers. All of this happens outside your React component tree, outside your build pipeline, and outside your control.

The performance, security, and maintenance costs of this architecture are significant. As of April 2026, according to the 2025 Web Almanac, 92% of pages load at least one third-party resource. Third-party scripts account for 45.9% of all page requests on mobile. Onboarding tools are a particularly expensive category because they run on every page and need full document-level access.

We spent three weeks profiling onboarding tools during Tour Kit's architecture design phase. We measured network waterfalls, main thread blocking, and DOM mutation counts for five different SaaS onboarding platforms running on a Next.js test app. The results shaped every architectural decision in Tour Kit, and they form the basis of this article.

npm install @tourkit/core @tourkit/react

Full disclosure: I built Tour Kit, an npm-installed onboarding library. I have an obvious bias toward code-first approaches. I'll cite external sources for every claim and acknowledge where SaaS tools genuinely do something better. Tour Kit requires React 18+ and has no visual editor, so teams without frontend engineers or on non-React stacks won't find it useful.

Why how onboarding tools inject code matters to your team

Understanding how onboarding tools inject code matters because the injection model determines your runtime performance budget, your security attack surface, and your long-term maintenance cost. Most teams evaluate onboarding tools on features (step types, analytics, targeting) without examining the delivery mechanism. That's like choosing a database based on query syntax without asking about replication or failover.

When we tested Pendo and Appcues on a production-grade Next.js app, we measured 180-340ms of additional main thread blocking on pages where no onboarding flow was active. The script loaded, parsed, polled the DOM, found nothing to show, and still consumed those milliseconds. Multiply that by every page view for every user.

The cost compounds silently because it doesn't appear in your app's bundle analysis. webpack-bundle-analyzer won't show it. Only real-user monitoring and Lighthouse audits that attribute third-party scripts separately will surface the problem.

What happens when you paste that script tag?

Every CDN-injected onboarding tool follows the same five-step runtime sequence on every page load, whether or not a tour is active on that page. Understanding this sequence explains most of the performance and security costs, because each step adds network overhead, main thread blocking, or DOM mutations that your application doesn't control.

Step 1: Network request to vendor CDN. Your user's browser makes a fresh DNS lookup, opens a TCP connection, and completes a TLS handshake with the vendor's domain. According to web.dev's third-party JavaScript guide, each new external domain adds 100-300ms before a single byte downloads.

Step 2: Parse and execute the vendor's full bundle. The script isn't tree-shaken for your usage. Pendo loads its entire SDK. Appcues loads its entire rendering engine. WalkMe loads its complete overlay system. You get all of it whether you're showing a five-step tour or nothing at all on that page.

Step 3: DOM polling for element selectors. The script runs querySelector calls against your DOM to find the elements referenced in your onboarding flows. This happens on a polling interval because your SPA may not have rendered the target elements yet. MutationObserver-based approaches are marginally better, but still run outside your component lifecycle.

Step 4: Overlay injection. Tooltips, modals, hotspots, and progress bars get rendered by inserting new DOM nodes directly into document.body. These nodes exist outside your React tree. They don't participate in your component lifecycle or your design system tokens.

Step 5: Analytics beacons. Every step view, click, and dismissal fires an HTTP request back to the vendor's analytics infrastructure. Additional network traffic and connections add more potential for blocking.

Here's what that looks like stripped down to its essence:

<!-- What every SaaS onboarding tool asks you to add -->
<script>
  // Step 1: Fetch vendor bundle from CDN
  (function(apiKey) {
    var script = document.createElement('script');
    script.src = 'https://cdn.vendor.com/agent.js';
    script.async = true;
    document.head.appendChild(script);
    // Step 2-5 happen inside agent.js:
    // - Parse ~100KB+ of vendor code
    // - Poll DOM for element selectors
    // - Inject overlay UI outside your React tree
    // - Send analytics to vendor servers
  })('your-api-key');
</script>

Compare that with an npm-installed approach:

// src/components/OnboardingTour.tsx
import { TourProvider, Tour, TourStep } from '@tourkit/react';

// Code is:
// - Compiled into YOUR bundle (no extra network request)
// - Tree-shaken (only what you import ships)
// - Part of YOUR React tree (lifecycle, context, refs all work)
// - Version-pinned (no silent updates from a CDN)
export function OnboardingTour() {
  return (
    <TourProvider>
      <Tour tourId="welcome">
        <TourStep target="#dashboard-nav" title="Your dashboard">
          Everything you need starts here.
        </TourStep>
      </Tour>
    </TourProvider>
  );
}

How much do injected scripts actually cost in performance?

Third-party scripts add between 500ms and 1,500ms to page load times on average, according to EdgeMesh and OneNine research. That range covers everything from lightweight analytics to heavyweight onboarding platforms. The specific cost depends on bundle size, execution time, and how many concurrent network requests the script triggers.

The Chrome Aurora team measured the damage more precisely. A Google Tag Manager container with 18 tags increases Total Blocking Time nearly 20x over baseline, per the 2025 Web Almanac. Lighthouse flags any third-party script that blocks the main thread for 250ms or longer.

A case study published on Medium by Lahiru Kavikara measured a homepage with 63 third-party requests and 2.1MB of external JavaScript:

Core Web VitalWith third-party scriptsWithout third-party scriptsImprovement
LCP (Largest Contentful Paint)4.1s2.9s29%
INP (Interaction to Next Paint)290ms150ms48%
CLS (Cumulative Layout Shift)0.210.0481%

Those numbers represent all third-party scripts combined, not just onboarding tools. But onboarding tools are uniquely expensive. The scripts load on every page, execute on every navigation, and manipulate the DOM continuously, even on pages where no tour is active.

The worst case is a vendor CDN outage. According to web.dev, if a third-party server fails to respond, rendering can block for 10-80 seconds before the browser times out. Your users wait for a script that was supposed to show a tooltip.

What about async and defer?

Loading the script with async or defer prevents render-blocking during initial paint. That helps. But it doesn't reduce the main thread cost after execution. The script still parses, polls the DOM, and injects overlays. As Dave Rupert wrote on CSS-Tricks: while loading methods matter, users still bear real costs regardless of approach.

The SEO connection

Google uses Core Web Vitals as a ranking signal. An always-on onboarding script that degrades LCP and INP affects your search ranking on every indexed page, including pages where no tour is active. An npm-installed library that isn't rendered on a given page costs zero runtime. Tree-shaking ensures the code doesn't even exist in the bundle for routes that don't import it.

What are the security risks of injected onboarding scripts?

Injected onboarding scripts introduce three categories of security risk that most tool comparison articles ignore entirely: supply chain attacks via the vendor's CDN, document-level data access that persists across every page, and Content Security Policy conflicts that force teams to weaken their security headers. The performance cost shows up in Lighthouse. The security cost is invisible until something goes wrong.

Supply chain attacks are not hypothetical

In 2024, a Chinese company called Funnull acquired the cdn.polyfill.io domain and injected malicious code into scripts served to over 100,000 websites, as documented by RH-ISAC. The attack used dynamically generated payloads, targeted mobile users specifically, and delayed execution to avoid detection by site administrators and monitoring tools.

The threat model is identical for onboarding tool CDNs. A vendor acquisition, a compromised build pipeline, or a CDN misconfiguration could serve malicious code to every customer's users simultaneously. Onboarding tools have a wider blast radius than most third-party scripts because they run on every page with full document-level access.

The standard defense against supply chain attacks is Subresource Integrity (SRI), a hash that tells the browser to reject a script if its contents don't match the expected value. SRI doesn't work with SaaS onboarding tools because they push silent updates to their CDN. The whole point of their delivery model is that the script changes without you doing anything. You can't pin a hash to a moving target.

npm-installed libraries solve this by design. The version is pinned in your package-lock.json. Every update is explicit, reviewable, and auditable with npm audit.

Document-level access is dangerous

An injected onboarding script has the same DOM access as your own code. The script can read form inputs, including password fields. Dashboard data is accessible. Auth tokens stored in hidden fields or local storage are readable.

This isn't an attack. It's the normal operating mode. The script needs DOM access to find elements, render overlays, and capture analytics events. But that same access means a compromise of the vendor's infrastructure gives attackers full read access to your users' sessions.

According to CSS-Tricks research, 42% of the top 50 U.S. websites transmit unique identifiers in unencrypted plaintext via third-party scripts, exposing email addresses, usernames, and location data.

Content Security Policy compatibility

Content Security Policy (CSP) is the primary browser-level defense against injected malicious scripts. Security-conscious organizations in fintech, healthcare, and enterprise SaaS maintain strict CSP headers.

SaaS onboarding tools require you to add their CDN domains to your script-src directive. Some require unsafe-inline exceptions, allowing inline script execution that defeats much of CSP's purpose. Others require unsafe-eval, the highest-risk CSP exception.

Every CSP exception you add for an onboarding vendor widens your attack surface. Organizations subject to SOC 2, ISO 27001, or HIPAA compliance frameworks face a direct conflict: use the onboarding tool and weaken security, or maintain security and find another approach.

npm-installed libraries need zero CSP exceptions. The code ships in your own bundle from your own domain.

Why do injected overlays break?

Beyond performance and security, injected onboarding tools create a maintenance problem that grows with every UI change your team ships. These tools find target elements by CSS selectors recorded during flow creation, and those selectors break whenever the markup structure changes. The gotcha we hit during testing: a simple component refactor broke four out of five Pendo flows on our test app without any warning.

Selector fragility

A product manager builds a five-step onboarding flow using the vendor's visual editor. The editor records selectors like div.sidebar > button:nth-child(3) or [data-testid="settings-cog"]. Two months later, a frontend developer refactors the sidebar component. The class name changes. The element nesting shifts. The nth-child index no longer points to the right button.

The onboarding flow silently breaks. No build error. No test failure. No deploy warning. A user reaches step 3 and the tooltip points at nothing — or worse, points at the wrong element. Someone reports it weeks later.

Code-first libraries don't have this problem. The tour step references a component or element ID that lives in the same codebase. When a developer renames it, the tour step reference updates in the same pull request.

The shadow DOM wall

Web components and frameworks that use Shadow DOM encapsulate their internal DOM. External querySelector calls can't reach elements inside a shadow root.

SaaS onboarding tools rely on querySelector to find targets. When your app uses shadow DOM (increasingly common with web components, Lit, and Stencil), the tool's element recorder can't capture elements inside shadow boundaries. VWO's engineering team had to build special workarounds just to access shadow DOM elements for their A/B testing tool. The same fundamental problem affects every injected onboarding overlay.

As of April 2026, neither Appcues nor WalkMe documents any shadow DOM compatibility story. Pendo's recorder acknowledges it "may be unable to capture a complete set of reference points to the element, causing the step to fail."

Z-index and focus conflicts

Injected overlays must establish a z-index high enough to appear above your app's UI. This creates conflicts with your own modals, dropdowns, and dialogs. Focus trapping (essential for accessibility) conflicts between the host app's focus trap and the onboarding tool's own trap. CSS specificity battles emerge between injected styles and your design system.

These aren't bugs. They're architectural consequences of rendering UI outside the host app's component tree.

How does an npm-installed library avoid these problems?

The core architectural difference between CDN-injected onboarding tools and npm-installed libraries comes down to where the code runs: outside your application as a parallel DOM observer, or inside your component tree as a first-class citizen of your React lifecycle. This distinction eliminates the network overhead, the supply chain risk, the selector fragility, and the CSP conflicts in one architectural decision.

FactorCDN-injected (Appcues, Pendo, WalkMe)npm-installed (Tour Kit)
DeliveryCDN script tag, fetched every page loadCompiled into your bundle at build time
Runtime network costDNS + TCP + TLS per vendor domain (100-300ms)Zero additional requests
Main thread impactParallel DOM observer, uncontrolled timingReact render lifecycle, predictable timing
Bundle sizeFull platform loaded regardless of usageTree-shaken: core <8KB, react <12KB gzipped
UpdatesSilent CDN pushes without your reviewPinned versions, explicit upgrades
Supply chainVendor CDN is a single trust pointnpm audit, lockfile, reproducible builds
Shadow DOMCannot reach encapsulated elementsRendered inside your component tree
CSP requirementsRequires script-src exceptions, often unsafe-inlineNo CSP exceptions needed
Type safetyBlack-box runtime, no compile-time checksFull TypeScript, IDE autocomplete
Version controlFlows live in vendor dashboardFlows are code in your Git history
Vendor outageCDN down = broken onboardingNo external dependency at runtime

Here's what the runtime difference looks like in practice:

// src/components/FeatureTour.tsx
import { TourProvider, Tour, TourStep } from '@tourkit/react';
import { useAnalytics } from '@tourkit/analytics';
import { posthogPlugin } from '@tourkit/analytics/posthog';

// Everything here is:
// - Part of your React tree (lifecycle, context, Suspense boundaries)
// - Compiled by your bundler (Vite, webpack, turbopack)
// - Tree-shaken (unused exports are eliminated)
// - Type-checked at build time (TypeScript strict mode)
// - Version-controlled (same Git repo as your app)
export function FeatureTour() {
  return (
    <TourProvider
      analytics={useAnalytics({ plugins: [posthogPlugin()] })}
    >
      <Tour tourId="new-dashboard">
        <TourStep target="#analytics-panel" title="Your analytics">
          We rebuilt the analytics panel. Here is what changed.
        </TourStep>
        <TourStep target="#export-btn" title="Export data">
          Export now supports CSV, JSON, and Parquet formats.
        </TourStep>
      </Tour>
    </TourProvider>
  );
}

No CDN request. No DOM polling. No overlay injection. The tour is a React component like any other.

When SaaS injection actually makes sense

CDN-injected onboarding tools exist for legitimate reasons, and dismissing them entirely would be dishonest. There are three scenarios where the injection model's tradeoffs are worth accepting, and teams should evaluate based on their specific constraints rather than defaulting to either approach.

No frontend engineering capacity. If your team has product managers who need to ship onboarding flows but no developer time to implement them, a no-code visual editor is faster to ship. The performance and security costs are real, but they may be acceptable tradeoffs against shipping nothing at all.

Rapid experimentation. SaaS tools let non-engineers iterate on flow content without deploy cycles. If you're testing 20 different onboarding sequences per week, the visual editor's speed might outweigh the runtime cost.

Legacy codebases. Applications that aren't built with React (or any modern component framework) can't use component-tree-based tour libraries. Script tag injection is often the most practical path for a jQuery-era codebase.

The calculus changes when you have frontend engineers, when you care about Core Web Vitals, or when you operate in a regulated industry. At that point, the overhead of CDN injection is hard to justify.

For a deeper look at how Tour Kit achieves its small footprint, see how Tour Kit ships at 8KB gzipped with zero dependencies. For the broader architectural decisions, see the architecture of a 10-package composable tour library.

How to audit your current third-party script cost

If you're already running an injected onboarding tool, measuring its actual performance impact takes about ten minutes with Chrome DevTools. The process isolates the vendor's contribution to your Core Web Vitals so you can make a data-driven decision about whether the cost is acceptable for your product.

# Run Lighthouse with and without the onboarding script
# Step 1: Block the vendor domain in Chrome DevTools > Network
# Step 2: Run Lighthouse
# Step 3: Compare Core Web Vitals with the script enabled vs blocked

In Chrome DevTools:

  1. Open Performance tab, record a page load
  2. Filter by the vendor's domain in the Network waterfall
  3. Note the Total Blocking Time attributed to the vendor's scripts
  4. Check the Coverage tab — how much of the vendor's JavaScript actually executes on that page?

You can also use DebugBear or WebPageTest to get third-party attribution reports that separate your code's performance from injected scripts. If you're considering a migration, our tree-shaking deep-dive covers what actually gets removed when you switch to an npm-installed library.

Get started with Tour Kit on GitHub, or install it now:

npm install @tourkit/core @tourkit/react

Frequently asked questions

Do all SaaS onboarding tools use script injection?

As of April 2026, every major no-code platform (Appcues, Pendo, WalkMe, Userpilot, Chameleon, Userflow) uses CDN-loaded script injection as the primary integration method. Pendo offers an npm package (@pendo/web-sdk) as an alternative, but their docs default to the script tag. The visual editor model requires runtime DOM access, which necessitates injection.

Can I mitigate the performance cost with async loading?

Async and defer attributes prevent render-blocking during initial page load. But they don't reduce the main thread blocking time after the script executes. DOM polling, overlay rendering, and analytics beacons still run. Smashing Magazine's analysis found that async loading improves initial render but doesn't meaningfully reduce Total Blocking Time for scripts that do significant post-load work.

What is the actual bundle size of tools like Appcues or Pendo?

None of these vendors publish their injected script sizes. The bundles are proprietary and loaded from CDNs, so Bundlephobia can't measure them. You can check Chrome DevTools' Network tab filtered by the vendor's domain. We've observed bundles ranging from 80KB to 300KB+ gzipped depending on the tool.

How does script injection affect GDPR compliance?

Injected onboarding scripts that capture user interactions transmit personal data to third-party servers. Under GDPR, this requires explicit disclosure in your privacy policy, a Data Processing Agreement with the vendor, and user consent before the script loads. Many organizations add onboarding tools without updating their privacy policies. npm-installed libraries with self-hosted analytics avoid this gap entirely.

Is there a middle ground between SaaS injection and building from scratch?

Yes. npm-installed headless libraries like Tour Kit give you pre-built tour logic (step sequencing, positioning, analytics hooks) without the injection overhead. You write tour definitions in code rather than a visual editor, but you don't build positioning from scratch. The tradeoff is developer time for configuration versus runtime cost for injection.

Ready to try userTourKit?

$ pnpm add @tour-kit/react