July 15, 2025
Experienced Next.js developers know that performance is paramount. Next.js provides many features out‑of‑the‑box, but you still need to be proactive about bundle sizes, loading strategies, and JavaScript execution. In this post we’ll dive deep into several techniques to speed up a Next.js application - whether you’re still on the Pages Router or have migrated to the new App Router.
Before optimizing you need to know what you’re shipping. Next.js ships an official @next/bundle‑analyzer plugin that wraps webpack-bundle‑analyzer.
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({});
Then, run a production build with the analysis flag enabled:
ANALYZE=true npm run build
The plugin writes three HTML files (client.html, edge.html, nodejs.html) into .next/analyze/, covering the browser bundle, the edge‑runtime bundle, and the Node.js server bundle respectively.
Each rectangle in the treemap represents a module; bigger rectangles mean bigger files. Inspecting these regularly helps catch bloat early.
We’re also building a tool to make this easier and more comprehensible than the default @next/bundle‑analyzerplugin. If you’re interested in becoming an alpha tester of this product reach out to [email protected].
Manual checks are inconsistent. A better approach is to integrate a performance budget into your CI/CD pipeline. The size-limit library is a great tool for this.
npm i -D size-limit @size-limit/preset-app
Add a budget in package.json:
"size-limit": [
{ "path": ".next/static/chunks/**", "limit": "1024 KB" }
]
Note: The path above is a great starting point that targets the main client-side JavaScript chunks. For a more thorough budget, you can adjust this path or add more entries to monitor other assets like CSS files or specific page bundles.
Create a small GitHub Action:
# .github/workflows/size-budget.yml
name: Bundle Size Check
on: [pull_request]
jobs:
size:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- uses: andresz1/size-limit-action@v2
size-limit-action will comment on the PR with a diff and fail the build if your bundle crosses the threshold. Other tools like bundlesize and bundlewatch offer similar functionality.
Some libraries include optional or debugging code that isn’t needed in production, and they provide flags to remove it. A prime example is Sentry. The Sentry SDK includes debug and tracing code that can bloat your bundle unnecessarily in production. Sentry’s documentation recommends using Webpack’s DefinePlugin in your Next config to set certain flags to false, which causes that code to be omitted during bundling . For instance, you can add:
// next.config.js inside the webpack() function
config.plugins.push(new webpack.DefinePlugin({
__SENTRY_DEBUG__: false,
__SENTRY_TRACING__: false,
// ...other flags
}));
Download our service guide to understand how we can help you optimise your site speed
Setting __SENTRY_DEBUG__ and __SENTRY_TRACING__ to false will tree-shake away Sentry's debug logging and tracing features that you aren't using. Always consult the documentation of your third party libraries - many provide tips to minimize their footprint (another example: setting moment to ignore locale files, or using the lightweight build of a library if one exists).
Even after pruning unused code, you may have chunks of JavaScript that are truly needed but not needed upfront. Large UI components (a complex chart, a rich text editor, a video player, etc.) or hefty libraries (like chart.js) can dramatically slow down your initial load if they’re included in the main bundle. The solution is code splitting via dynamic import - in other words, lazy loading those parts of your app only when they are actually required.
Use next/dynamic for client-side components: Instead of statically importing a heavy component at the top of your file, you can dynamically import it. For example, suppose you have a <DataVisualization> component that is 500KB (with a big charting library) and it's only shown when the user opens a statistics panel. You could do:
// Instead of
import dynamic from 'next/dynamic';
// Dynamically import with no SSR (client-only)
'use client'; // needed in App Router when using ssr:false
const DataVisualization = dynamic(() => import('../components/DataVisualization'), { ssr: false });
Now <DataVisualization> will be code-split into a separate chunk and will only be loaded when it's actually rendered on the client.
Use the option for components that are purely interactive and depend on browser-only APIs (like window or document), or for large, non-critical components where showing a loading fallback is preferable to increasing the server-render time.
Next 15 caveat: When you pass , the dynamic() call must live in a file that starts with the use client directive. Calling it from a Server Component now triggers a build-time error, because Server Components no longer allow opting-out of SSR on the fly.
React.lazy and Suspense: Alternatively, you can use React.lazy() and <Suspense> to lazy-load components. Next.js supports this as well, but next/dynamic is generally more ergonomic and offers the ssr:false switch. Under the hood, next/dynamic is basically a wrapper around React.lazy with enhancements. Either way, the outcome is the same: code splitting and lazy loading yield smaller initial bundles and faster Time to Interactive.
Images often comprise a large portion of a webpage’s weight and can significantly slow down your loads if not optimized. A single huge, unoptimized image can wreck your performance by delaying rendering, causing layout shifts, and blocking the main thread during decoding. Next.js addresses this with its built-in <Image> component, which provides automatic Image Optimization. If you’re currently using plain <img> tags for anything beyond trivial icons, switching to <Image> is one of the quickest wins for performance.
Key benefits of Next.js Image Optimizationinclude:
Using the <Image> component is straightforward:
// Instead of
<img src="/hero.jpg" width="1200" height="800" />
// You would do
import Image from 'next/image;
<Image
src="/hero.jpg"
alt="Hero banner"
width={1200}
height={800}
priority // if this image is critical (e.g., above the fold or LCP image)
placeholder="blur" // optional blur-up placeholder
/>
Tips:
Performance is a moving target. Visualize your bundles, enforce budgets in CI, strip unused code, lazy-load the heavy stuff, and <Image> to ship fewer bytes. Keep an eye on server render times and cache aggressively. Follow these practices and your users (and Core Web Vitals) will thank you.
Download our service guide to understand how
we can help you optimise your site speed