cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

Scripts and third-party code

Load analytics, widgets, maps and videos in Next.js with next/script strategies and @next/third-parties, and see when each script downloads and runs.

By the end, you can
  • 01
    Choose a strategyLoad a script with beforeInteractive, afterInteractive or lazyOnload, and know why worker isn't one to use in the App Router yet.
  • 02
    Run your own code with a scriptGive inline scripts an id, and use onLoad, onReady and onError from a Client Component.
  • 03
    Add Google's tools, and check their costUse @next/third-parties for Google Analytics, Tag Manager, YouTube and Google Maps, and read which requests each one makes.

The tea shop's analytics script

Third-party code is code another company writes that your site loads: analytics that count visitors, a chat widget, a map, a video player. It runs in the visitor's browser, next to your own code, and each piece has to be downloaded and run. Next.js loads these scripts with <Script> from next/script: you say which script, and when it should load, and Next.js takes care of the rest.

Here the tea shop loads an analytics script on every page. The script, public/analytics.js, is a stand-in: three lines that only write down that they ran. A real one would come from an analytics company; a stand-in keeps the recording from depending on another company's servers. The project was built with real Next.js 16.4.3 and run with next start, then real Chrome took three steps: open /, click Teas, and click Show what ran.

An analytics script on every page of the tea shopNext.js
app/teas/public/
app/layout.tsx
import Link from "next/link";import Script from "next/script";import RunLog from "./run-log"; export const metadata = { title: "Tea shop" }; export default function RootLayout({ children }: LayoutProps<"/">) {  return (    <html lang="en">      <body>        <nav>          <Link href="/">Home</Link> <Link href="/teas">Teas</Link>        </nav>        {children}        <RunLog />        <Script src="/analytics.js" />      </body>    </html>  );}
Recorded from Next.js 16.4.3

Each step was taken in real Chrome against next start. Pick one to see the page after it.

localhost:3000/Tea shop
Home Teas
Tea shop

Fresh tea, sent to your door.

Show what ran
    Focus
    nothing: the page itself
    Scrolled
    0 px down

    Requests the page made during this step (11), files and other sites included

    1. GET/200a whole page
    2. GET/_next/static/chunks/1kexj4s7oxhs2.js200application/javascript
    3. GET/_next/static/chunks/06bs-nf19b2js.js200application/javascript
    4. GET/analytics.js200application/javascript
    5. GET/_next/static/chunks/3gjfgdn2oppok.js200application/javascript
    6. GET/_next/static/chunks/3l_qmp-61z4yc.js200application/javascript
    7. GET/_next/static/chunks/turbopack-3-ry682tkyiqk.js200application/javascript
    8. GET/_next/static/chunks/45834dh3i6j9o.js200application/javascript
    9. GET/200text/x-component
      rsc
      1
      next-router-prefetch
      1
      next-router-segment-prefetch
      /_tree
      next-url
      /
    10. GET/teas200text/x-component
      rsc
      1
      next-router-prefetch
      1
      next-router-segment-prefetch
      /_tree
      next-url
      /
    11. GET/teas200text/x-component
      rsc
      1
      next-router-prefetch
      1
      next-router-segment-prefetch
      /teas/__PAGE__
      next-url
      /

    Line 2 of app/layout.tsx imports Script. Line 16 adds <Script src="/analytics.js" /> to the root layout, so every page has it. src is the script's address; files in public are served from the site's root, so /analytics.js is public/analytics.js. app/run-log.tsx is a Client Component that writes React hydrated the page in an Effect on lines 14 to 17, and shows everything written down when its button is clicked. Hydration is when React takes over the HTML already on screen: it matches its Client Components to that HTML and attaches their event handlers, so buttons start to work (Server rendering and hydration goes further).

    Now the Steps tab, one step at a time:

    1. Open the shop. The page asks for /analytics.js among its first files. The Network tab shows why: the HTML has <link rel="preload" href="/analytics.js" as="script">, which tells the browser to start downloading it at once.
    2. Click Teas. No request at all. The teas page was prefetched when its link appeared, and the analytics script isn't downloaded again: Next.js loads a script once, even when the visitor moves between pages.
    3. Click Show what ran. The log says React hydrated the page, then analytics.js ran: in this run, once, and after the log's Effect.
    Real-life analogyOpening the tea shop in the morning

    Before the doors open, the lock has to work. As the doors open, the till goes on. The radio can wait until the morning rush is over. Some boxes arrive at dawn, but stay closed until there's time. Each job has its moment, and the shop runs best when nothing is done earlier than it has to be.

    In real life: The lock that must be on before the doors open
    In Next.js: beforeInteractive: runs before the page works
    In real life: The till, switched on as the doors open
    In Next.js: afterInteractive: runs once the page has begun to hydrate
    In real life: The radio, turned on once the morning rush is over
    In Next.js: lazyOnload: loads when the browser is idle
    In real life: Things delivered early, but unpacked later
    In Next.js: A preloaded script, run later
    The main idea

    next/script loads a script once, at the moment its strategy says. beforeInteractive is fetched before any of the page's own code, afterInteractive (the default) loads after some (or all) of the page has hydrated, and lazyOnload once the page has loaded and the browser is idle. Handlers like onLoad need a Client Component, inline scripts need an id, and @next/third-parties wraps Google's tools in ready-made components.

    Four strategies: when a script loads and runs

    The strategy prop decides when a script loads. The shop gets a chat widget, public/chat.js, another stand-in, which also writes down how far the page had loaded when it ran (document.readyState: loading, interactive once the HTML is read, or complete once the page and its files have loaded). Each button is one strategy. Before you click, guess: in which version does the chat script write its line before React hydrated the page, and which version's is the last file Chrome asks for?

    Playground: one chat widget, four strategies
    The tea shop, with chat.js loaded this wayNext.js
    app/public/
    app/layout.tsx
    import Script from "next/script";import RunLog from "./run-log"; export const metadata = { title: "Tea shop" }; export default function RootLayout({ children }: LayoutProps<"/">) {  return (    <html lang="en">      <body>        {children}        <RunLog />        <Script src="/chat.js" strategy="beforeInteractive" />      </body>    </html>  );}
    Recorded from Next.js 16.4.3

    Each step was taken in real Chrome against next start. Pick one to see the page after it.

    localhost:3000/Tea shop
    Tea shop

    Questions? Our chat is open from 9 to 5.

    Show what ran
      Focus
      nothing: the page itself
      Scrolled
      0 px down

      Requests the page made during this step (7), files and other sites included

      1. GET/200a whole page
      2. GET/_next/static/chunks/2n2x6ko-morcw.js200application/javascript
      3. GET/_next/static/chunks/1hv73wm-mg223.js200application/javascript
      4. GET/chat.js200application/javascript
      5. GET/_next/static/chunks/3gjfgdn2oppok.js200application/javascript
      6. GET/_next/static/chunks/turbopack-3-ry682tkyiqk.js200application/javascript
      7. GET/_next/static/chunks/3-d-p3p3456r4.js200application/javascript
      Try it yourself
      beforeInteractive

      Line 12 of the root layout. The HTML preloads /chat.js, so Chrome asks for it among the first files. In this recording, step 2's log says chat.js ran (page: interactive) before React hydrated the page. (The docs say its execution doesn't block hydration.)

      Each version was built with next build from Next.js 16.4.3 and opened in real Chrome. Step 1 lists every file the page asked for, in order; step 2 clicks Show what ran. Reset goes back to the first version.

      Step 1 shows when Chrome downloaded /chat.js, and step 2's log shows the order things ran in, in this recording. Next to each, what Next.js's docs promise:

      • beforeInteractive is for scripts the whole site needs before it works, like a cookie consent manager or a bot detector. It must be in the root layout. The docs say these scripts are preloaded and fetched before any first-party code, "but their execution does not block page hydration from occurring." In this recording, the log says chat.js ran (page: interactive), then React hydrated the page: in Next.js 16.4.3, the App Router's startup code runs these scripts one after another and then starts hydrating (app-bootstrap.ts, lines 58 to 80). It runs once per full page load; a click to another page doesn't run it again.
      • afterInteractive, the default, can go in any page or layout. The docs say it loads "after some (or all) hydration occurs on the page". Its file is preloaded as early, but Next.js adds the script from an Effect in <Script>; in this recording, React hydrated the page came first. Next.js's docs name tag managers and analytics for it.
      • lazyOnload isn't preloaded. Next.js waits for the page's load event, when every file the page needs has arrived, then for a moment when the browser is idle, and only then adds the script. In step 1's list, /chat.js is the last file. It suits chat widgets and social media buttons.
      • worker would run the script in a web worker, a background thread, with a library called Partytown. It's experimental, needs nextScriptWorkers in next.config.ts, and the docs say it's not yet stable and doesn't work in the App Router yet. In this recording, the page never asked for /chat.js, and the log has no line from it.

      Each row is one strategy: where it can go, and what it did in these recordings.

      When each strategy loads and runs
      StrategyWhere it can goWhen Chrome downloaded itWhen it ran
      beforeInteractiveOnly in the root layout.Preloaded by the HTML, among the first files.Before the log's hydration line, in this 16.4.3 recording. The docs say it doesn't block hydration.
      afterInteractive (the default)Any page or layout.Preloaded by the HTML, among the first files.After the log's hydration line. The docs say: after some (or all) hydration.
      lazyOnloadAny page or layout.Last: after the page has loaded, when the browser is idle.After the page had loaded.
      workerExperimental; the docs say it doesn't yet work with the App Router.Not at all, in this App Router recording.Not at all, in this App Router recording.
      A timeline from left to right, as recorded: the HTML arrives, React hydrates the page, the page has loaded, the browser is idle. In the recordings, beforeInteractive ran between the HTML and hydration, afterInteractive after hydration, and lazyOnload when the browser was idle. worker, dashed and crossed out, didn't load in the App Router recording.HTML arrivesReact hydratespage loadedbrowser idlebeforeInteractiveafterInteractivelazyOnloadworkernot loaded, in the recording
      One page load, left to right, and when each strategy's chat.js ran in these 16.4.3 recordings. Both beforeInteractive and afterInteractive scripts were preloaded by the HTML; what differed is when they ran. lazyOnload waited for the page to load and the browser to be idle. worker didn't load.

      A plain <script> tag has its own ways to wait, async and defer (From HTML to running script explains them). <Script> adds what a React app needs on top: it ties the moment to the page's startup and to its load, and loads each script once across pages.

      Read it in Next.js's own code

      <Script> is a Client Component in packages/next/src/client/script.tsx. For afterInteractive, it asks React to preload the file during rendering, at lines 361 to 376, and adds the script in an Effect, at lines 259 to 271. lazyOnload waits for the load event and an idle moment, at lines 169 to 177. For beforeInteractive, the App Router's startup code in 16.4.3 loads the scripts one after another, then calls hydrate, which is why the recorded log has the chat script first: app-bootstrap.ts, lines 58 to 80.

      Inline scripts need an id

      An inline script has its code between the tags instead of in a file, like a few lines that set up an analytics tool. Write the code as a string inside <Script>, or pass it with dangerouslySetInnerHTML. Next.js's docs say an inline script must have an id, "in order for Next.js to track and optimize the script". Here a small component with an inline script is on two pages, / and /teas:

      Playground: an inline script on two pages
      The tea shop, with an inline script in a component both pages useNext.js
      app/teas/
      app/visit-note.tsx
      import Script from "next/script"; export default function VisitNote() {  return (    <Script id="visit-note">      {`window.shopLog = window.shopLog || [];window.shopLog.push("the inline script ran");`}    </Script>  );}
      Recorded from Next.js 16.4.3

      Each step was taken in real Chrome against next start. Pick one to see the page after it.

      localhost:3000/teasTea shop
      Home Teas
      Our teas

      Green tea, mint tea and oolong.

      Show what ran
      1. React hydrated the page
      2. the inline script ran
      Focus
      the button “Show what ran”
      Scrolled
      0 px down

      No requests during this step: the page already had what it needed

      Try it yourself
      With an id

      Line 5 gives the inline script the id the docs require. Both pages render it, and in this recording, step 3's log has the inline script ran once.

      Both versions were built with next build from Next.js 16.4.3. Real Chrome opened /, clicked Teas, then clicked Show what ran. Reset goes back to the first version.

      Line 5 of app/visit-note.tsx opens the <Script>, and lines 6 and 7 are its code. Real Chrome opened /, clicked Teas, and showed the log. In this recording, with id="visit-note", the inline script ran once; without an id, it ran twice, once for each page. That's how 16.4.3's <Script> works inside: it keeps track of the scripts it has run by their id, or by their src for a file (script.tsx, lines 75 to 80), and an inline script has no src. The guarantee to rely on is the docs' rule: give every inline script an id.

      onLoad, onReady and onError

      Some scripts give you something to use once they've loaded, like a map library that draws a map into an element. <Script> takes three handlers for that. They're functions, and a Server Component can't send a function to the browser, so they only work in a Client Component. What the docs promise for each:

      • onLoad runs code after the script has finished loading. It can't be used with beforeInteractive.
      • onReady runs after the script has finished loading, and then every time the component is mounted again once the script is loaded, after a navigation for example. It's the place to set up a widget the script provides.
      • onError runs if the script fails to load. It can't be used with beforeInteractive either.

      The shop's map, on its visit page and its contact page:

      A map script, with onLoad, onReady and onErrorNext.js
      app/contact/visit/public/
      app/visit/shop-map.tsx
      "use client"; import Script from "next/script";import { useRef, useState } from "react"; declare global {  interface Window {    TeaMap?: { draw: (element: HTMLElement) => void };  }} export default function ShopMap() {  const mapRef = useRef<HTMLDivElement>(null);  const [status, setStatus] = useState("Waiting for the map script");   return (    <section>      <div ref={mapRef} />      <p>{status}</p>      <Script        src="/tea-map.js"        onLoad={() => setStatus("onLoad ran: the map script has loaded")}        onReady={() => window.TeaMap?.draw(mapRef.current!)}        onError={() => setStatus("onError ran: the map couldn't load")}      />    </section>  );}
      Recorded from Next.js 16.4.3

      Each step was taken in real Chrome against next start. Pick one to see the page after it.

      localhost:3000/contactTea shop
      Home Visit us Contact
      Contact

      Write to hello@teashop.example, or come by.

      Map: 12 Leaf Street. Times drawn: 2

      Waiting for the map script

      Show what ran
        Focus
        the link “Contact”
        Scrolled
        0 px down

        Requests the page made during this step (2), files and other sites included

        1. GET/200text/x-component
          rsc
          1
          next-router-prefetch
          1
          next-router-segment-prefetch
          /__PAGE__
          next-url
          /visit
        2. GET/contact200text/x-component
          rsc
          1
          next-router-prefetch
          1
          next-router-segment-prefetch
          /contact/__PAGE__
          next-url
          /visit

        Line 1 of app/visit/shop-map.tsx makes it a Client Component. Lines 20 to 25 load /tea-map.js, a stand-in map library whose TeaMap.draw writes a map's address into an element, and counts how many times it has drawn. Line 22, onLoad, changes the text under the map. Line 23, onReady, draws the map into the <div> from line 18, through a ref. Line 24, onError, says the map couldn't load. Both pages render ShopMap. Click through what happened in this recording:

        1. Open Visit us. Chrome asks for /tea-map.js. Both handlers run: Times drawn: 1, and onLoad ran: the map script has loaded.
        2. Click Contact. A new ShopMap mounts. The only requests are the router's fetches for the pages' RSC payloads (the data that describes a page); /tea-map.js isn't asked for again. The map says Times drawn: 2: onReady ran for the new component, since the script was loaded. The text says Waiting for the map script: the new component's onLoad didn't run, because the script had finished loading before it mounted.
        3. Click Visit us. The page is back exactly as it was, Times drawn: 1. Nothing mounted: with Cache Components on, Next.js keeps a page you just left, hidden, and shows it again on the way back.

        Each row is one handler: what the docs say, and what it did in this experiment.

        Three handlers
        HandlerWhen it runsGood to know
        onLoadAfter the script has finished loading. In the recording, the contact page's ShopMap, mounted after that, didn't get it.Not for beforeInteractive.
        onReadyAfter the script has finished loading, and every time the component mounts again once it's loaded. In the recording, it ran on both pages.Set up a widget the script provides.
        onErrorWhen the script fails to load. In the recording, after a 404.Not for beforeInteractive.
        Two browser windows, as recorded. The first, /visit, loads tea-map.js: onLoad ran and onReady ran. An arrow labeled click Contact goes to the second window, /contact, whose ShopMap mounted after the script had loaded: onLoad, dashed and crossed out, didn't run; onReady ran./visitonLoad ranonReady ranclick Contact/contactonLoadonReady ran
        In the recording: on /visit, tea-map.js loaded, and onLoad and onReady both ran. On /contact, a new ShopMap mounted after the script had loaded: only its onReady ran, and nothing was downloaded again.

        Now break it, two ways:

        Playground: two ways a script's handlers go wrong
        The visit page's map, broken this wayNext.js
        app/contact/visit/public/
        app/visit/shop-map.tsx
        "use client"; import Script from "next/script";import { useRef, useState } from "react"; declare global {  interface Window {    TeaMap?: { draw: (element: HTMLElement) => void };  }} export default function ShopMap() {  const mapRef = useRef<HTMLDivElement>(null);  const [status, setStatus] = useState("Waiting for the map script");   return (    <section>      <div ref={mapRef} />      <p>{status}</p>      <Script        src="/maps/tea-map.js"        onLoad={() => setStatus("onLoad ran: the map script has loaded")}        onReady={() => window.TeaMap?.draw(mapRef.current!)}        onError={() => setStatus("onError ran: the map couldn't load")}      />    </section>  );}
        Recorded from Next.js 16.4.3

        Each step was taken in real Chrome against next start. Pick one to see the page after it.

        localhost:3000/visitTea shop
        Home Visit us Contact
        Visit us

        onError ran: the map couldn't load

        Show what ran
          Focus
          nothing: the page itself
          Scrolled
          0 px down

          Errors in Chrome's console during this step (1)

          1. Failed to load resource: the server responded with a status of 404 (Not Found)

          Requests the page made during this step (12), files and other sites included

          1. GET/visit200a whole page
          2. GET/_next/static/chunks/1kexj4s7oxhs2.js200application/javascript
          3. GET/_next/static/chunks/2qh-avimhn1oz.js200application/javascript
          4. GET/maps/tea-map.js404text/plain
          5. GET/_next/static/chunks/3gjfgdn2oppok.js200application/javascript
          6. GET/_next/static/chunks/29k0lwsa-cdae.js200application/javascript
          7. GET/_next/static/chunks/turbopack-3-ry682tkyiqk.js200application/javascript
          8. GET/_next/static/chunks/3xfzf07sl2z2b.js200application/javascript
          9. GET/_next/static/chunks/0k9wmronebsvk.js200application/javascript
          10. GET/no answer yetfetch
            rsc
            1
            next-router-prefetch
            1
            next-router-segment-prefetch
            /_tree
            next-url
            /visit
          11. GET/visitno answer yetfetch
            rsc
            1
            next-router-prefetch
            1
            next-router-segment-prefetch
            /_tree
            next-url
            /visit
          12. GET/contactno answer yetfetch
            rsc
            1
            next-router-prefetch
            1
            next-router-segment-prefetch
            /_tree
            next-url
            /visit
          Try it yourself
          A wrong address

          Line 21 points at a file that doesn't exist. The server answers 404, Chrome logs the failed load, and onError on line 24 shows onError ran: the map couldn't load.

          Both versions were built with next build from Next.js 16.4.3. The first was opened in real Chrome; the second never got that far, so open its Terminal tab. Reset goes back to the first version.

          A script at a wrong address answers 404, and onError runs. Chrome also logs the failed load in its console, which the Steps tab lists. And handlers in a Server Component don't reach the browser at all: the build stops while it prerenders /visit. A <Script> without handlers is fine in a Server Component, like the analytics script in the shop's root layout.

          Google Analytics, Tag Manager, maps and videos

          @next/third-parties is a package from the Next.js team with ready-made components for popular third-party tools. GoogleAnalytics, GoogleTagManager and YouTubeEmbed load their scripts with <Script>; GoogleMapsEmbed loads no script, only a frame. The docs call the package experimental. Install it with npm install @next/third-parties@latest; the latest on October 30, 2026 is 16.4.3, released with Next.js. Google's tools are in @next/third-parties/google. Here are Google Analytics and Google Tag Manager, with made-up IDs:

          Playground: Google Analytics or Tag Manager
          The tea shop's root layout, with this Google toolNext.js
          app/
          app/layout.tsx
          import { GoogleAnalytics } from "@next/third-parties/google"; export const metadata = { title: "Tea shop" }; export default function RootLayout({ children }: LayoutProps<"/">) {  return (    <html lang="en">      <body>{children}</body>      <GoogleAnalytics gaId="G-TEASHOP1" />    </html>  );}
          Recorded from Next.js 16.4.3

          Each step was taken in real Chrome against next start. Pick one to see the page after it.

          localhost:3000/Tea shop
          Tea shop

          Fresh tea, sent to your door.

          Focus
          nothing: the page itself
          Scrolled
          0 px down

          Requests the page made during this step (7), files and other sites included

          1. GET/200a whole page
          2. GET/_next/static/chunks/2n2x6ko-morcw.js200application/javascript
          3. GET/_next/static/chunks/1hv73wm-mg223.js200application/javascript
          4. GEThttps://www.googletagmanager.com/gtag/js?id=G-TEASHOP1not sent: another sitescript
          5. GET/_next/static/chunks/3gjfgdn2oppok.js200application/javascript
          6. GET/_next/static/chunks/turbopack-3-ry682tkyiqk.js200application/javascript
          7. GET/_next/static/chunks/07w89l33g7bi7.js200application/javascript
          Try it yourself
          GoogleAnalytics

          Line 9 adds Google Analytics. The HTML preloads gtag/js from www.googletagmanager.com, and the page asks for it: the one request to another site, which the recorder stopped.

          Both versions were built with next build from Next.js 16.4.3 and @next/third-parties 16.4.3, and opened in real Chrome. The recorder stops every request to another site before it leaves, and lists it as not sent. Reset goes back to the first version.

          GoogleAnalytics takes your measurement ID as gaId, and GoogleTagManager your container ID as gtmId. In the root layout, they load on every page. Each one writes a small inline script that sets up Google's dataLayer, and loads Google's script with <Script>'s default strategy, afterInteractive: the Network tab shows the HTML preloading it from www.googletagmanager.com, and step 1 lists that request, not sent, since the recorder stops every request to another site. To record an event, like an Add to cart click, the package has sendGAEvent and sendGTMEvent. The docs add that Google Analytics counts a page view each time the URL changes, so client-side navigations are counted too. If the site already has Tag Manager, the docs suggest setting up Analytics through it instead of adding both.

          The visit page gets a video and a map. The map is far down the page, below 3000 px of text:

          A YouTube video and a Google map on the visit pageNext.js
          app/visit/
          app/visit/page.tsx
          import { GoogleMapsEmbed, YouTubeEmbed } from "@next/third-parties/google"; export default function VisitPage() {  return (    <main>      <h1>Visit us</h1>      <YouTubeEmbed videoid="ogfYd705cRs" height={400} playlabel="Play: how we pack our tea" />      <div style={{ height: 3000 }}>Opening hours, prices and the story of the shop.</div>      <GoogleMapsEmbed apiKey="your-maps-api-key" mode="place" q="Tea shop, 12 Leaf Street" height={300} width="100%" />    </main>  );}
          Recorded from Next.js 16.4.3

          Each step was taken in real Chrome against next start. Pick one to see the page after it.

          localhost:3000/visitTea shop
          Visit us
          Opening hours, prices and the story of the shop.
          Focus
          nothing: the page itself
          Scrolled
          0 px down

          Requests the page made during this step (9), files and other sites included

          1. GET/visit200a whole page
          2. GEThttps://cdn.jsdelivr.net/gh/paulirish/lite-youtube-embed@master/src/lite-yt-embed.cssnot sent: another sitestylesheet
          3. GET/_next/static/chunks/2n2x6ko-morcw.js200application/javascript
          4. GET/_next/static/chunks/1hv73wm-mg223.js200application/javascript
          5. GET/_next/static/chunks/3gjfgdn2oppok.js200application/javascript
          6. GET/_next/static/chunks/turbopack-3-ry682tkyiqk.js200application/javascript
          7. GET/_next/static/chunks/1tq6ppe__m8m-.js200application/javascript
          8. GET/_next/static/chunks/3xoej_kx-l3ao.js200application/javascript
          9. GEThttps://cdn.jsdelivr.net/gh/paulirish/lite-youtube-embed@master/src/lite-yt-embed.jsnot sent: another sitescript

          Line 7 adds the video with YouTubeEmbed, by its videoid; it loads lite-youtube-embed's script with <Script> and lazyOnload. Line 9 adds the map with GoogleMapsEmbed: a Maps Embed API key from Google, here a placeholder, the mode, and q, what to show. It renders a plain <iframe>, which the browser loads, not a script. Open the Network tab: the video is a <lite-youtube> element, and the map an <iframe loading="lazy"> whose address holds the key, for anyone to read. Then the Steps tab:

          • Open Visit us. The page asks cdn.jsdelivr.net for lite-youtube-embed's stylesheet and script, a small library that draws a thumbnail and a play button, and loads YouTube's own player only when the visitor clicks. Nothing from youtube.com, and no map.
          • Scroll down to the map. Now the page asks www.google.com for the map's frame. loading="lazy" made the browser wait until the map was near the screen.
          On the left, the Visit us page with a YouTubeEmbed and a GoogleMapsEmbed far below it. An arrow from the video goes to cdn.jsdelivr.net, asked for when the page opened: lite-youtube-embed's script and stylesheet. youtube.com is dashed and crossed out: not asked for, until a click. An arrow from the map goes to www.google.com, asked for only after the visitor scrolled near it.Visit usYouTubeEmbedGoogleMapsEmbed3000 px of textcdn.jsdelivr.netwhen the page openedyoutube.comnot until a clickwww.google.comafter the scroll
          The recorded requests to other sites. When the page opened, the video asked cdn.jsdelivr.net for lite-youtube-embed's script and stylesheet, and nothing from youtube.com. The map's frame was asked for only after the scroll.

          Measuring what a script costs

          Every script costs the visitor something: the bytes to download, often a connection to another site, and the time to run it in the browser, on the same thread that responds to clicks. The recorded request lists show the first part: which files each version asked for, from which site, and in which step. That's the question to ask of every third-party script: what does it make the page download, and when?

          To measure the rest in your own app, use real Chrome on the real site:

          • The Network panel in Chrome's DevTools lists every request with its size and its site. Reload the page and filter by a third party's domain to see everything it brings.
          • The Performance panel records a page load and shows which scripts kept the browser busy, and for how long.
          • Lighthouse, also in DevTools, has an audit called "Reduce the impact of third-party code", which lists each third party's size and main-thread time.
          • Build it twice, with the script and without, or with two strategies, and compare, the way each playground's versions were built.
          The cheapest script

          A script you don't load costs nothing. Put a script in the page or layout that needs it, not the root layout, as the docs recommend; pick the latest strategy that still works; and prefer components like YouTubeEmbed that wait for the visitor.

          Which strategy fits?

          Sort these scripts by the strategy that suits each one.

          Which strategy fits each script?
          • A cookie consent manager that must decide before anything else runs
          • A bot detector that checks every visitor
          • Google Analytics
          • A tag manager
          • A chat widget in the corner
          • Social media share buttons at the bottom of a post
          Try it yourself
          0 of 6 correct

          Sort each script: does it need beforeInteractive, afterInteractive, or can it wait for lazyOnload?

          Choose a category for every card. You can change an answer at any time; Reset clears them all.

          Common mix-ups

          • "afterInteractive means the file downloads after hydration." No. The HTML preloads it, so it downloads early. It runs later, after some (or all) of the page has hydrated.
          • "beforeInteractive works in any page." It belongs in the root layout, for scripts the whole site needs first.
          • "worker is a fourth option for the App Router." No. The docs say it's not yet stable and doesn't yet work with the App Router; in the recorded App Router page, the script never loaded.
          • "A script in the root layout downloads again on every page." No. Next.js loads it once; a click to another page didn't ask for it again.
          • "An inline script doesn't need an id." The docs require one, so Next.js can track the script. In the recording, the one without an id ran twice.
          • "onLoad and onReady are the same." No. onLoad runs when the script finishes loading. onReady runs then too, and again whenever the component mounts once the script is loaded.
          • "A YouTube embed loads YouTube's player with the page." Not YouTubeEmbed: the recorded page asked youtube.com for nothing.

          Practice exercises

          A Next.js project can't run inside this page, so answer first, then use the hints.

          Exercise 1 · Warm-upPick a strategy for the chat

          The chat widget isn't needed until a visitor has a question. Which strategy goes in place of the question mark?

          Starter codeTSX
          import Script from "next/script";
          
          export default function HomePage() {
            return (
              <main>
                <h1>Tea shop</h1>
                <p>Questions? Our chat is open from 9 to 5.</p>
                <Script src="/chat.js" strategy="?" />
              </main>
            );
          }
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 2 · Warm-upFix the inline script

            This component is on two pages, and its inline script runs on both. What's missing?

            Starter codeTSX
            import Script from "next/script";
            
            export default function VisitNote() {
              return (
                <Script>
                  {`window.shopLog = window.shopLog || [];
            window.shopLog.push("the inline script ran");`}
                </Script>
              );
            }
            

            Answer, then press Check. Spacing and letter case don’t matter.

              Exercise 3 · PracticeRead the recorded log

              chat.js writes chat.js ran into the log, and RunLog writes React hydrated the page from its Effect. In the recording of this layout, which line came first?

              Starter codeTSX
              import Script from "next/script";
              import RunLog from "./run-log";
              
              export const metadata = { title: "Tea shop" };
              
              export default function RootLayout({ children }: LayoutProps<"/">) {
                return (
                  <html lang="en">
                    <body>
                      {children}
                      <RunLog />
                      <Script src="/chat.js" strategy="beforeInteractive" />
                    </body>
                  </html>
                );
              }
              

              Answer, then press Check. Spacing and letter case don’t matter.

                Exercise 4 · PracticeWhy the build stops

                The build stops while it prerenders this page. What does the file need, so that onLoad can work?

                Starter codeTSX
                import Script from "next/script";
                
                export default function VisitPage() {
                  return (
                    <main>
                      <h1>Visit us</h1>
                      <Script src="/tea-map.js" onLoad={() => console.log("The map script has loaded")} />
                    </main>
                  );
                }
                

                Answer, then press Check. Spacing and letter case don’t matter.

                  Exercise 5 · ChallengeWhich handler ran on the second page?

                  A visitor opens /visit, then clicks Contact, which also renders ShopMap. In the recording, which of the contact page's ShopMap handlers ran?

                  Starter codeTSX
                  "use client";
                  
                  import Script from "next/script";
                  import { useRef, useState } from "react";
                  
                  declare global {
                    interface Window {
                      TeaMap?: { draw: (element: HTMLElement) => void };
                    }
                  }
                  
                  export default function ShopMap() {
                    const mapRef = useRef<HTMLDivElement>(null);
                    const [status, setStatus] = useState("Waiting for the map script");
                  
                    return (
                      <section>
                        <div ref={mapRef} />
                        <p>{status}</p>
                        <Script
                          src="/tea-map.js"
                          onLoad={() => setStatus("onLoad ran: the map script has loaded")}
                          onReady={() => window.TeaMap?.draw(mapRef.current!)}
                          onError={() => setStatus("onError ran: the map couldn't load")}
                        />
                      </section>
                    );
                  }
                  

                  Answer, then press Check. Spacing and letter case don’t matter.

                    Exercise 6 · ChallengeWhen the map asks Google

                    The map is below 3000 px of text. When does the visitor's browser ask Google for it?

                    Starter codeTSX
                    import { GoogleMapsEmbed, YouTubeEmbed } from "@next/third-parties/google";
                    
                    export default function VisitPage() {
                      return (
                        <main>
                          <h1>Visit us</h1>
                          <YouTubeEmbed videoid="ogfYd705cRs" height={400} playlabel="Play: how we pack our tea" />
                          <div style={{ height: 3000 }}>Opening hours, prices and the story of the shop.</div>
                          <GoogleMapsEmbed apiKey="your-maps-api-key" mode="place" q="Tea shop, 12 Leaf Street" height={300} width="100%" />
                        </main>
                      );
                    }
                    

                    Answer, then press Check. Spacing and letter case don’t matter.

                      Check your understanding

                      For each question, ask when the script is downloaded, when it runs, and which component is allowed to hold its handlers.

                      The Scripts and third-party code quiz · 7 questionsScore: first tries count
                      1. Question 1 of 7Which strategy does <Script src="/analytics.js" /> use?

                        Choose an answer to see the explanation.

                      2. Question 2 of 7Where does a beforeInteractive script like this one belong?

                        Read the code, then predictTSX
                        import Script from "next/script";
                        import RunLog from "./run-log";
                        
                        export const metadata = { title: "Tea shop" };
                        
                        export default function RootLayout({ children }: LayoutProps<"/">) {
                          return (
                            <html lang="en">
                              <body>
                                {children}
                                <RunLog />
                                <Script src="/chat.js" strategy="beforeInteractive" />
                              </body>
                            </html>
                          );
                        }
                        

                        Choose an answer to see the explanation.

                      3. Question 3 of 7In the recording, which /chat.js was the last file Chrome asked for?

                        Choose an answer to see the explanation.

                      4. Question 4 of 7What do Next.js's docs say about strategy="worker" in the App Router?

                        Choose an answer to see the explanation.

                      5. Question 5 of 7What does this inline script need, according to Next.js's docs?

                        Read the code, then predictTSX
                        import Script from "next/script";
                        
                        export default function VisitNote() {
                          return (
                            <Script>
                              {`window.shopLog = window.shopLog || [];
                        window.shopLog.push("the inline script ran");`}
                            </Script>
                          );
                        }
                        

                        Choose an answer to see the explanation.

                      6. Question 6 of 7Why does this page stop the build?

                        Read the code, then predictTSX
                        import Script from "next/script";
                        
                        export default function VisitPage() {
                          return (
                            <main>
                              <h1>Visit us</h1>
                              <Script src="/tea-map.js" onLoad={() => console.log("The map script has loaded")} />
                            </main>
                          );
                        }
                        

                        Choose an answer to see the explanation.

                      7. Question 7 of 7A page has <YouTubeEmbed> from @next/third-parties. Before anyone clicks play, what does the page ask for?

                        Choose an answer to see the explanation.

                      Key takeaways

                      • <Script> from next/script loads a script once, even across pages, at the moment its strategy says.
                      • beforeInteractive, in the root layout, is fetched before the page's own code. afterInteractive, the default, is preloaded but runs after some (or all) hydration. lazyOnload loads after the page, when the browser is idle. worker is experimental and doesn't yet work with the App Router.
                      • An inline script needs an id, so Next.js can track it; the recorded one without an id ran twice.
                      • onLoad, onReady and onError are functions, so they need a Client Component. onLoad runs when the script finishes loading; onReady runs then, and on every later mount once it's loaded.
                      • @next/third-parties/google has GoogleAnalytics, GoogleTagManager, YouTubeEmbed, which load their scripts with <Script>, and GoogleMapsEmbed, a lazy frame.
                      • A script's cost is what it makes the page download and run: list its requests, then measure in DevTools.

                      Remember the one-liner.
                      Load third-party code with next/script, as late as it can be, once, and from the component that needs it.

                      Coming next: Static files and the public folder. It shows what goes in public, like the stand-in scripts here, how it's served and cached, and when to import a file instead.

                      CompleteFrontend Clear concepts. Working examples.