// LOADING
// LOADING
// LOADING_ARTICLE
The initial JavaScript bundle is one of the most direct levers you have on page load performance. Every kilobyte a browser has to download, parse, and execute before anything is interactive is a kilobyte that delays your users. Next.js handles the coarsest splitting for you automatically, but getting the most out of it requires understanding what it does by default and where you need to intervene.
In the App Router, every segment in the route hierarchy maps to a separate chunk. When a user navigates to /notelogs/some-slug, the browser only loads the JavaScript for that route and its shared dependencies — not the code for /projects or /interview-prep. This is route-level code splitting, and it requires no configuration.
On top of that, Next.js extracts shared dependencies into common chunks. If five pages all import the same utility library, it ends up in one shared chunk that is cached across navigations rather than duplicated five times.
However, automatic splitting has limits. A single page that imports a large library — a rich text editor, a chart library, a date picker — pulls that code into the route chunk regardless of whether the user will actually interact with it on every visit.
next/dynamic is Next.js's wrapper around React's lazy and Suspense. It lets you split at the component boundary, deferring a component's code until it is actually rendered.
tsximport dynamic from 'next/dynamic'; const RichTextEditor = dynamic(() => import('./RichTextEditor'), { loading: () => <p>Loading editor...</p>, ssr: false, }); export default function CreatePost() { const [showEditor, setShowEditor] = useState(false); return ( <div> <button onClick={() => setShowEditor(true)}>Open Editor</button> {showEditor && <RichTextEditor />} </div> ); }
Here, the editor's chunk is not downloaded until the user clicks the button. The ssr: false option is important for components that depend on browser APIs — it tells Next.js not to attempt to render them on the server at all.
ssr: false is sometimes used as a workaround for hydration errors, but that is a misuse. The legitimate case is components that genuinely cannot run on the server: anything that directly reads window, document, localStorage, or WebGL contexts. Disabling SSR for those components is correct.
For components that could render on the server but are large and not above the fold, keep SSR enabled and use loading to show a skeleton. The server-rendered HTML appears immediately; the interactive version loads in progressively.
Before optimizing, measure. The @next/bundle-analyzer package produces an interactive treemap of your chunks:
bashnpm install @next/bundle-analyzer
ts// next.config.ts import withBundleAnalyzer from '@next/bundle-analyzer'; export default withBundleAnalyzer({ enabled: process.env.ANALYZE === 'true', })({ // your existing next config });
bashANALYZE=true npm run build
Look for large modules appearing in route chunks where they are not needed on initial render. Common culprits are Markdown parsers, syntax highlighters, form validation libraries, and chart components. Any of these that do not appear immediately on page load are candidates for dynamic().
For third-party scripts (analytics, chat widgets, ad tags), use Next.js's <Script> component with an appropriate strategy:
tsximport Script from 'next/script'; export default function Layout({ children }: { children: React.ReactNode }) { return ( <html> <body> {children} <Script src="https://analytics.example.com/script.js" strategy="lazyOnload" /> </body> </html> ); }
beforeInteractive — loads before the page becomes interactive; for critical scripts only.afterInteractive — loads after hydration; appropriate for analytics.lazyOnload — loads during browser idle time; for low-priority scripts like chat widgets.Third-party scripts are often overlooked during bundle analysis but can account for hundreds of kilobytes. Deferring them with lazyOnload has zero impact on your own bundle size metrics but real impact on LCP and TBT.
Lazy loading defers work, but it introduces a loading state when the component is first needed. For components that are likely to be interacted with quickly — a modal that opens on a common action — the loading delay can feel worse than just including the code upfront.
Next.js <Link> prefetches linked route chunks on hover (in production builds), which masks the latency for navigation. For dynamic() components, you can prefetch manually:
tsxconst RichTextEditor = dynamic(() => import('./RichTextEditor')); // Prefetch when the user hovers over the trigger button function CreatePostButton() { return ( <button onMouseEnter={() => import('./RichTextEditor')} onClick={() => setShowEditor(true)} > Create Post </button> ); }
The dynamic import in onMouseEnter triggers the chunk download on hover, so by the time the user clicks, the code is already cached.
Bundle size has a direct line to Core Web Vitals. A bloated initial bundle increases Time to Interactive and can delay LCP if the rendering of above-the-fold content depends on that JavaScript. Splitting off below-the-fold components keeps the critical path lean.
The flip side is Cumulative Layout Shift. If a lazy-loaded component takes up layout space when it loads, reserve that space with a skeleton or min-height on the container. An unexpected layout shift after load is just as damaging to CLS as one during initial render.
For a related performance topic, Image Optimization in Next.js covers the other major contributor to LCP — getting images right compounds the gains from good code splitting.
If you want to go deeper on the broader bundle optimization story, Reducing JavaScript Bundle Size in Next.js covers tree shaking and import analysis that complements the lazy loading techniques here.
Code splitting is not a one-time task — it is a habit. Start with the analyzer to find what is large and when it loads. Apply dynamic() to components that are not needed on initial render. Defer third-party scripts with the <Script> strategy. And measure the impact in Lighthouse or with field data rather than guessing. Small, targeted splits compound into meaningful improvements in real user experience.