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.
dynamic() wraps a dynamic import() and returns a component that loads its code when first rendered:
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:
const Chart = dynamic(() => import("./charts").then((mod) => mod.LineChart));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.An alternative for code that is not a component, such as a heavy formatting utility, is a plain dynamic import inside an event handler:
async function exportPdf() {
const { generatePdf } = await import("@/lib/pdf");
generatePdf(document.getElementById("report"));
}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 |
// 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.
The @next/third-parties package wraps popular scripts with tuned defaults:
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.
ssr: false in a Server Component, which is a build error; move the dynamic() call into a client file.<script> tag instead of next/script, losing deduplication and ordering.Where is `dynamic(() => import(...), { ssr: false })` allowed?
next/dynamic splits within a route on demand.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.