Lesson 80 of 81
Picking a next/script strategy is only step one. A chat widget, an analytics snippet, and a payment SDK each break in a different way if you get the strategy — or the surrounding integration code — wrong.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Knowing that next/script has four loading strategies is table stakes. The harder skill is mapping a specific third-party integration onto the right one, because "chat widget," "analytics snippet," and "payment SDK" aren't interchangeable — each has a different failure mode when it loads too early, too late, or gets remounted by React unexpectedly. This lesson works through three real integrations end to end: not just which strategy to pick, but the surrounding code you need so the integration actually behaves once it's loaded.
The common thread across all three: a third-party script usually attaches something to window (a global function, a constructor, an event queue), and your React code has to coordinate with that global safely — checking it exists before calling it, cleaning up after itself, and not assuming the script has loaded just because the <Script> tag is on the page.
A chat widget — lowest urgency, but it needs an onReady signal before you can call its API (e.g. to open the chat programmatically from your own "Contact us" button):
// components/chat-widget.tsx
"use client";
import Script from "next/script";
import { useState } from "react";
declare global {
interface Window {
ChatEmbed?: { open: () => void; init: (key: string) => void };
}
}
export function ChatWidget() {
const [ready, setReady] = useState(false);
return (
<>
<Script
src="https://cdn.chat-widget.example/embed.js"
strategy="lazyOnload"
onReady={() => {
window.ChatEmbed?.init(process.env.NEXT_PUBLIC_CHAT_KEY!);
setReady(true);
}}
/>
<button disabled={!ready} onClick={() => window.ChatEmbed?.open()}>
Chat with us
</button>
</>
);
}
onReady (distinct from onLoad, which only fires once per navigation) fires every time the component mounts, even on a script that's already cached and loaded — that's what makes ready state reliable across client-side navigations instead of only on first page load.
An analytics snippet — most vendors ship a "queue" pattern specifically so calls made before the real script loads aren't lost:
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Script id="analytics-init" strategy="beforeInteractive">
{`window.analyticsQueue = window.analyticsQueue || [];
window.track = function(...args) { window.analyticsQueue.push(args); };`}
</Script>
<Script src="https://cdn.analytics.example/tag.js" strategy="afterInteractive" />
</body>
</html>
);
}
Two separate <Script> tags, two separate strategies, on purpose: the tiny inline queue shim has to exist before anything else calls window.track(...), so it's beforeInteractive; the actual vendor bundle that drains the queue can arrive later without losing a single event, so it's afterInteractive. Calling track() anywhere in your app is now safe from the very first render, regardless of whether the real script has finished downloading yet.
A payment SDK — the highest-stakes case, because a checkout button that fires before the SDK is ready is a broken purchase, not a cosmetic delay:
// components/checkout-button.tsx
"use client";
import Script from "next/script";
import { useState } from "react";
export function CheckoutButton({ amount }: { amount: number }) {
const [sdkReady, setSdkReady] = useState(false);
return (
<>
<Script
src="https://js.payments.example/v3/sdk.js"
strategy="afterInteractive"
onReady={() => setSdkReady(true)}
onError={() => console.error("Payment SDK failed to load")}
/>
<button
disabled={!sdkReady}
onClick={() => window.PaymentSDK?.charge({ amount })}
>
{sdkReady ? "Pay now" : "Loading payment..."}
</button>
</>
);
}
afterInteractive, not lazyOnload: a payment button that's clickable before its SDK exists is worse than one that's disabled for an extra second, so this is the one third-party integration where erring toward "load sooner" is correct — but beforeInteractive still isn't warranted, since nothing about checkout needs to be ready before the page is even interactive. The disabled={!sdkReady} guard matters as much as the strategy choice: it's what actually prevents the broken-purchase failure mode, not the loading timing alone.
lazyOnload on a chat widget, then forget it also needs onReady if any of their own UI calls into it — the loading strategy and the readiness guard are two separate decisions.setTimeout. Most analytics and support-chat vendors already document a small inline shim like the one above; wiring your own polling loop to "wait until window.trackingLib exists" is redundant and flakier.onLoad only fires once per full page load; onReady fires on every mount, including client-side navigations. Using onLoad to drive component state that needs to survive route changes silently breaks the second time a user navigates back to that page without a full reload."We need to add a chat widget, an analytics tag, and a Stripe-style payment button to this page — walk me through how you'd load each one" tests something the general script-optimization question doesn't: whether a candidate connects the loading strategy to the actual failure mode of each specific integration, rather than reciting the four strategies and picking one by vibes. The strongest answers bring up readiness guards unprompted, before being asked "but what if someone clicks the button before the script loads?"