Scripts and Bundle Optimization
The idea in one sentence
Section titled “The idea in one sentence”Ship less JavaScript by loading third-party scripts at the right moment with next/script, code-splitting heavy client components with next/dynamic, and keeping the Server/Client boundary tight.
next/script and its strategies
Section titled “next/script and its strategies”A raw <script> tag blocks parsing and runs whenever the browser reaches it. next/script lets you choose the moment with a strategy. Pick the latest one your script can tolerate.
beforeInteractive— loads before any Next.js code hydrates; reserve it for critical scripts only.afterInteractive— the default; loads after hydration starts, right for analytics.lazyOnload— loads during idle time; right for low-priority widgets like chat.
// A chat widget is low priority — defer it to idle timeimport Script from 'next/script'
export default function Layout({ children }: { children: React.ReactNode }) { return ( <> {children} <Script src="https://widget.example.com/chat.js" strategy="lazyOnload" /> </> )}Code-split client components with next/dynamic
Section titled “Code-split client components with next/dynamic”next/dynamic (built on React.lazy) splits a heavy client component into its own chunk that loads only when rendered. For a component that touches browser-only APIs, pass ssr: false so it never runs on the server. Note that ssr: false is only allowed inside a Client Component.
'use client'import dynamic from 'next/dynamic'
// Heavy, browser-only — split into its own chunk, no SSRconst Chart = dynamic(() => import('./Chart'), { ssr: false, loading: () => <p>Loading chart…</p>,})
export default function Dashboard() { return <Chart />}optimizePackageImports and the bundle analyzer
Section titled “optimizePackageImports and the bundle analyzer”Some libraries with barrel files pull in far more than you use. optimizePackageImports tells Next.js to import only the modules you actually reference, shrinking the client bundle.
import type { NextConfig } from 'next'
const nextConfig: NextConfig = { experimental: { optimizePackageImports: ['lucide-react'], },}
export default nextConfigTo see what is actually in your bundle, wrap the config with @next/bundle-analyzer and run a build with the flag set.
// next.config.js — run: ANALYZE=true next buildconst withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true',})
module.exports = withBundleAnalyzer({})Keep the Server/Client boundary tight
Section titled “Keep the Server/Client boundary tight”The biggest bundle win is not shipping code at all. Everything imported past a "use client" boundary becomes client JavaScript, so push "use client" as far down the tree as you can — keep data-heavy work in Server Components and mark only the small interactive leaves as client.
flowchart TD Page["Server Component (0 KB client JS)"] --> Leaf["'use client' leaf"] Page --> Split["next/dynamic chunk"] Leaf --> JS[Client bundle] Split -->|loads on demand| JS Page --> Script["next/script (strategy-timed)"]