One of the most frustrating experiences in modern web development is seeing a ‘Reduce unused JavaScript’ warning in Google Lighthouse. If you’re wondering how to reduce unused JavaScript in Next.js, you’re not alone. In my experience building production apps, the culprit is rarely one massive file, but rather a ‘death by a thousand cuts’—small libraries and components being imported where they aren’t needed.
Shipping unused JS doesn’t just affect your load time; it kills your Total Blocking Time (TBT) and Interaction to Next Paint (INP), especially on mobile devices. To fix this, we need to move from a ‘load everything’ mentality to a ‘load on demand’ strategy.
Prerequisites
- A functioning Next.js project (App Router or Pages Router).
- Basic familiarity with npm or yarn.
- Chrome DevTools installed for performance auditing.
Step 1: Audit and Visualize Your Bundle
You can’t fix what you can’t see. Before guessing which library is too heavy, I always use the @next/bundle-analyzer. This tool creates a visual map of exactly what is taking up space in your JS chunks.
First, install the package:
npm install @next/bundle-analyzer
Then, update your next.config.js:
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({
// Your existing next config
})
Run the analysis with: ANALYZE=true npm run build. As shown in the image below, this will open a browser window showing exactly which dependencies are bloating your bundles.
Step 2: Implement Dynamic Imports (Code Splitting)
The most effective way to reduce unused JavaScript is to stop loading components until they are actually needed. In Next.js, we do this using next/dynamic.
For example, if you have a heavy ‘UserDashboard’ component that only appears after a user clicks a button, don’t import it at the top of your file. Instead, do this:
import dynamic from 'next/dynamic'
// This component will be loaded in a separate chunk and only when rendered
const HeavyChart = dynamic(() => import('../components/HeavyChart'), {
loading: () => Loading Chart...,
ssr: false, // Set to false if the component uses browser-only APIs
})
I’ve found that applying this to modals, complex forms, and third-party chart libraries can reduce the initial JS payload by 30-50%. If you’re also optimizing your visuals, don’t forget to check out my next.js image component optimization tutorial to ensure your assets aren’t slowing you down as well.
Step 3: Optimize Third-Party Libraries
Many developers accidentally import entire libraries when they only need one function. This is where tree-shaking comes in. If you’re using lodash, for instance, never do import { cloneDeep } from 'lodash', as this often pulls in the whole library.
Instead, use the specific package: import cloneDeep from 'lodash/cloneDeep'.
Additionally, for scripts like Google Analytics or Facebook Pixel, use the next/script component with the strategy="lazyOnload" attribute to ensure they don’t block the main thread during initial page load.
Step 4: Leverage React Server Components (RSC)
If you are using the Next.js App Router, you have a massive advantage: Server Components. By default, components in the app/ directory are Server Components and send zero JavaScript to the client.
The key to reducing unused JS here is to move as much logic as possible to the server and only use 'use client' for the smallest possible leaf components. In my last project, moving a complex filtering logic from a client component to a server component reduced the client-side bundle by 12KB. For those interested in the cutting edge of rendering, I highly recommend reading about react 19 concurrent rendering performance to see how the underlying engine is evolving.
Pro Tips for Advanced Optimization
- Analyze ‘Coverage’ in Chrome: Open DevTools → Cmd+Shift+P → type ‘Coverage’ → hit record. It will highlight exactly which lines of CSS and JS were executed during the page load.
- Barrel File Avoidance: Be careful with
index.tsfiles that export everything from a folder. They often trick the bundler into including modules you aren’t actually using. - Package Alternatives: Swap heavy libraries for lighter ones (e.g.,
date-fnsinstead ofmoment.js, orlucide-reactinstead of a massive custom SVG set).
Troubleshooting
Issue: My dynamic import is still showing up in the main bundle.
Solution: Check if you are importing the component somewhere else in the file using a standard import statement. This overrides the dynamic import and forces the code into the main chunk.
Issue: The page feels ‘jumpy’ when dynamic components load.
Solution: Always provide a loading skeleton that matches the final dimensions of the component to prevent Layout Shift (CLS).
What’s Next?
Now that you’ve trimmed your JavaScript, it’s time to look at the rest of your performance budget. Focus on reducing your Largest Contentful Paint (LCP) and optimizing your API response times. Remember, performance is a continuous process, not a one-time task.