Lazy Loading, Scripts and next/dynamic

Intermediate
10 min

Lazy Loading, Scripts and next/dynamic

Next.js already splits JavaScript by route, so visiting /about never downloads the code for /dashboard. Within a route, though, everything a page imports ships together, including a 300 KB charting library that only appears after a click. Lazy loading defers such code until it is needed, and next/script does the same for third-party scripts. After this lesson you will be able to load components on demand with next/dynamic, skip server rendering for browser-only libraries, and load external scripts with the right strategy.

Lazy Loading Components With next/dynamic

dynamic() wraps a dynamic import() and returns a component that loads its code when first rendered:

tsx
import dynamic from "next/dynamic"; const Markdown = dynamic(() => import("@/components/Markdown"), { loading: () => <p>Loading editor...</p>, });

The loading option renders while the chunk downloads. Under the hood this is React.lazy plus Suspense; Next.js still pre-renders the component on the server by default.

For a named export, resolve it in the import callback:

tsx
const Chart = dynamic(() => import("./charts").then((mod) => mod.LineChart));

Skipping Server Rendering

Some libraries read window or document at import time and crash on the server. Pass ssr: false to load them only in the browser, as in the sample at the top of this lesson. Two rules apply:

  • ssr: false is only allowed inside a Client Component; a Server Component cannot opt a child out of server rendering.
  • The component's HTML is absent from the server response, so never use it for content that search engines or the first paint should include.

An alternative for code that is not a component, such as a heavy formatting utility, is a plain dynamic import inside an event handler:

tsx
async function exportPdf() { const { generatePdf } = await import("@/lib/pdf"); generatePdf(document.getElementById("report")); }

Loading Third-Party Scripts With next/script

Analytics, chat widgets and embeds should never block rendering. next/script controls when a script loads:

| Strategy | When it runs | Use for | |---|---|---| | beforeInteractive | Before hydration, injected in the root layout only | Consent managers, bot detection | | afterInteractive (default) | Right after hydration | Analytics, tag managers | | lazyOnload | During browser idle time | Chat widgets, social embeds |

tsx
// app/layout.tsx import Script from "next/script"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> {children} <Script src="https://widget.example.com/chat.js" strategy="lazyOnload" /> <Script id="theme-init" strategy="beforeInteractive"> {`document.documentElement.dataset.theme = localStorage.getItem("theme") ?? "light";`} </Script> </body> </html> ); }

Inline scripts need an id so Next.js can track them. A script placed in a layout loads once per session; in a page it loads when the page is visited. The onLoad and onReady callbacks (Client Components only) let you initialise a library after it arrives.

Official Wrappers for Common Services

The @next/third-parties package wraps popular scripts with tuned defaults:

tsx
import { GoogleAnalytics } from "@next/third-parties/google"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body>{children}</body> <GoogleAnalytics gaId="G-XXXXXXX" /> </html> ); }

GoogleTagManager, GoogleMapsEmbed and YouTubeEmbed are available from the same package and avoid the usual performance penalties of these embeds.

Common mistakes

  • Using ssr: false in a Server Component, which is a build error; move the dynamic() call into a client file.
  • Lazy loading tiny components; the extra request costs more than the bytes saved.
  • Adding a raw <script> tag instead of next/script, losing deduplication and ordering.
Quick Quiz
Question 1 of 3

Where is `dynamic(() => import(...), { ssr: false })` allowed?

Key Takeaways

  • Routes are code-split automatically; next/dynamic splits within a route on demand.
  • Use loading for a placeholder and then((m) => m.Named) for named exports.
  • ssr: false works only in Client Components and removes the component from server HTML.
  • next/script strategies (beforeInteractive, afterInteractive, lazyOnload) control when third-party code runs.
  • @next/third-parties provides tuned wrappers for Google Analytics, Tag Manager, Maps and YouTube.

Next lesson: Metadata and SEO in Next.js — set titles, descriptions and social tags with the Metadata API.

Lazy Loading, Scripts and next/dynamic - Next.js | CodeYourCraft | CodeYourCraft