Reduce unused JavaScript in Next.js, React and Google Tag Manager
Lighthouse flags any script with over 20 KiB of unused code. The fix for each source: tag manager tags, below-the-fold components and whole libraries.
LLaunchScaler·Published ·9 min read
To reduce unused JavaScript, find which files Lighthouse lists under "Reduce unused JavaScript", then either stop shipping that code (remove it or tree-shake it) or ship it later (a dynamic import, or a tag that fires after load, interaction or consent). The usual sources are third-party tags loaded through a tag manager, components below the fold, and icon or utility libraries imported whole.
The fix depends on where the unused code comes from, so this guide starts with finding the source and then gives the fix per source, for Next.js, plain React and Google Tag Manager.
What does "Reduce unused JavaScript" mean?
It is a Lighthouse audit, shown in PageSpeed Insights and Chrome DevTools, that lists scripts whose code downloaded during page load but never ran. Lighthouse flags every JavaScript file with more than 20 KiB of unused code. For a source-mapped bundle it also breaks the file down by module and lists modules with more than 512 bytes unused.
A few details change how you read it:
"Unused" means unused during that page load. A form validator or a checkout modal is unused until someone clicks, so it will always show up. That is a sign to load it later, not to delete it.
The audit survived Lighthouse 13's move to insight audits in October 2025. unused-javascript is not among the audits Chrome replaced or removed, and Chrome says the rest are unaffected, while the old third-party summary became an insight.
Unused bytes still have to be downloaded before the page can use the rest of the file. Long-running scripts raise Total Blocking Time, which carries 30% of the Lighthouse performance score, the largest single weight.
How do you find which scripts are unused?
Start from the Lighthouse table, then confirm with the Coverage panel so you know whether the waste is first-party code, a library or a third-party tag. Group each file by where it comes from, because each group has a different fix.
Run the page through PageSpeed Insights or the Lighthouse panel in DevTools and open "Reduce unused JavaScript". Note each URL and its estimated savings.
In Chrome DevTools, open the command menu (Cmd+Shift+P on Mac, Ctrl+Shift+P on Windows and Linux), type "coverage" and choose Show Coverage.
Questions, answered
What people ask about this
01
What does 'Reduce unused JavaScript' mean in PageSpeed Insights?
It lists JavaScript files that downloaded during page load but whose code did not run. Lighthouse flags every file with more than 20 KiB of unused code and shows the estimated savings per file.
Pick "Per function" or "Per block", click the reload button, and use the page the way a visitor would: scroll, open the menu, submit a form.
Click "Stop instrumenting coverage and show results". Sort by Unused Bytes. In the Usage Visualization bar, gray is unused and green is used.
Click a file to see its unused lines highlighted in the Sources panel. For your own bundles, run a bundle analyzer (below) to see which package each chunk contains.
Sort the list into the groups in the table, then work from the biggest group down.
Where the unused code comes from
How it shows up
The fix
Tag manager and its tags
gtm.js, gtag/js and the scripts its tags inject
Remove dead tags, fire the rest after load or consent
Chat, video and social widgets
Large third-party files that run only on click
Load on interaction or with lazyOnload
Components below the fold
Your own chunks for modals, carousels, editors, charts
Dynamic import with next/dynamic or React.lazy
Whole-library imports
Icon sets, lodash, date libraries in a shared chunk
Import only what you use, use ESM builds
Code that only transforms data
Markdown parsers, syntax highlighters in the client bundle
Run it on the server and send HTML
How do you reduce unused JavaScript in Next.js?
In the App Router, Server Components are code split automatically, so the unused code is usually in Client Components, the libraries they import and third-party scripts. Use next/dynamic for client components that aren't needed on first view, move pure rendering work to the server, and measure with a bundle analyzer.
Lazy load client components that appear below the fold or behind a click. next/dynamic wraps React.lazy() and Suspense:
The calculator's code now downloads only when someone opens it. Add { ssr: false } only for a component that cannot render on the server; that option is allowed in Client Components only, and the Next.js docs warn that a Server Component dynamically importing a Client Component does not get automatic code splitting. Libraries can load on demand too: const Fuse = (await import('fuse.js')).default inside an event handler pulls a search library in on the first keystroke.
Move work that only turns data into markup to the server. The Next.js bundling guide uses a syntax highlighter as the example: in a Client Component the whole library ships to the browser even though the output is static HTML, while in a Server Component the browser receives only the markup.
Measure before and after. On Next.js 16.1 and later with Turbopack, run npx next experimental-analyze for an interactive treemap with import chains, or add --output to write the analysis to .next/diagnostics/analyze so you can diff it. On webpack builds, install @next/bundle-analyzer, wrap your config with withBundleAnalyzer({ enabled: process.env.ANALYZE === 'true' }) and run ANALYZE=true npm run build.
Load third-party scripts with the right next/script strategy. afterInteractive (the default) loads after some hydration and suits tag managers and analytics. lazyOnload injects the script during browser idle time after every other resource has been fetched, and the docs name chat support plugins and social media widgets as candidates. The worker strategy is experimental and does not work with the App Router.
How do you reduce unused JavaScript from Google Tag Manager?
Remove or pause every tag you no longer use, fire the rest after the page has loaded, and hold advertising and analytics tags until the visitor answers your consent banner. Google's own guidance says a paused or removed tag takes its code out of the container, while a tag blocked by a trigger exception still ships.
Work through the container in this order:
Export the container and list every tag with its last-fired date and owner. Pause or delete the ones nobody can vouch for. Blocking a tag with a trigger exception is not enough, because its code stays in gtm.js.
Keep one container per page. Google's tag guidance says multiple containers add overhead and script execution.
Check the container size. GTM caps a container at 300 KB and warns at 70% of that; web.dev puts the median container at about 50 KB.
Change the trigger on non-essential tags from Page View or DOM Ready to Window Loaded. web.dev's rule is that the earlier a tag fires, the more it affects performance.
Replace Custom HTML tags with Custom Templates where you can. Custom HTML can insert elements into the page, which costs time and can shift layout; templates run sandboxed.
Use consent mode. In basic consent mode, Google tags are blocked until the visitor interacts with the consent banner, so no tag code runs for visitors who never accept.
Test in Preview mode before you publish. web.dev also recommends a two-step process where an administrator approves container changes, so a new tag can't slip into production unreviewed.
In Next.js, the GoogleTagManager component from @next/third-parties/google fetches the container script after hydration, so it does not compete with your own code during the first render. The package is still marked experimental.
How do you reduce unused JavaScript in a React app?
Split by route and by interaction with React.lazy and Suspense, and make sure your bundler can tree-shake what you import. lazy() takes a dynamic import(), and React defers loading that component's code until it is rendered for the first time, so a route or a dialog costs nothing until someone opens it.
Declare every lazy() call at the top level of the module. React's docs warn that declaring it inside a component resets state on every re-render. Split at the router first (one lazy component per route), then at heavy widgets inside a route.
How do you tree-shake icon and utility libraries?
Import the exact functions and icons you use, from packages that publish ES modules, and let the bundler drop the rest. Tree shaking depends on the static structure of import and export, so a CommonJS package or a Babel config that converts modules to CommonJS defeats it.
The webpack guide lists what tree shaking needs: ES2015 module syntax, no compiler step that turns modules into CommonJS, a "sideEffects" field in package.json, and production mode. Mark CSS as a side effect ("sideEffects": ["**/*.css"]) or your styles will be dropped.
In practice that means:
import { debounce } from 'lodash-es' rather than import _ from 'lodash'.
Named icon imports (import { Search } from 'lucide-react') rather than a namespace import of the whole set.
In Next.js, add packages that export hundreds of modules to experimental.optimizePackageImports. Next.js already applies it by default to a list that includes lucide-react, date-fns, lodash-es, react-icons/*, @heroicons/react, @mui/icons-material and @tabler/icons-react, so check the list before adding one.
How much third-party JavaScript is too much?
Keep third-party main-thread time well under 250 ms. Lighthouse flags a page whose third-party code blocks the main thread for 250 ms or longer, and in Lighthouse 13 that check moved from the old third-party summary into the third parties insight. Treat 250 ms as the ceiling, not the target.
The third parties table groups time by vendor, so you can see which one costs the most. For each, decide whether it is needed on this page at all, whether it can load with defer (web.dev suggests defer for less critical resources, such as a video player below the fold), and whether it can wait for a click. A chat widget or an embedded video can show a static placeholder that loads the real script only when someone clicks it, and web.dev notes that lazy-loading resources below the fold helps load performance.
Main-thread time is also what hurts responsiveness for real visitors, which is why cutting scripts is usually the first step in the guide to improving Interaction to Next Paint. If Search Console already reports a failing group, work through why a Core Web Vitals assessment fails as well, since Google's search systems look at field data from real users rather than a Lighthouse score.
How do you check the fix worked?
Re-run Lighthouse five times and compare the medians, because lab results vary between runs; Google's Lighthouse docs say the median of 5 runs is twice as stable as a single run. Confirm the flagged files shrank or left the initial load, that Total Blocking Time fell, and that nothing you deferred broke: forms still submit and consent still gates the tags.
Lab numbers move the day you deploy, while field data takes weeks to catch up. The comparison of Core Web Vitals checker tools explains which tool reads which data, and why their numbers disagree.
To check the whole page in one pass, run the free scan on LaunchScaler. It needs only your URL and no account, and its speed checks read the lab audits for unused JavaScript, third-party script impact, main-thread work and render-blocking resources alongside field Interaction to Next Paint at the 75th percentile, as part of 156 checks across 6 of its 7 categories. Fix what it flags, deploy, and scan again to see which items cleared.
Open Chrome DevTools, press Cmd+Shift+P (Ctrl+Shift+P on Windows), run Show Coverage, then click the reload button. The Unused Bytes column and the gray part of each bar show code that never ran while you were recording.
03
Why does Google Tag Manager show up as unused JavaScript?
The container script carries the code for every tag in it, including tags that never fire on the page you tested. Pausing or removing dead tags takes their code out of the container, which a blocking trigger does not.
04
Can I get unused JavaScript to zero?
No, and you should not try. Code for a modal, a form handler or a chat widget runs only when someone uses it, so it counts as unused on load. The goal is to load that code when it is needed instead of on every page view.
05
Does unused JavaScript affect SEO?
Not as a score. Google's search systems look at Core Web Vitals from real users, and heavy JavaScript tends to hurt Interaction to Next Paint and Largest Contentful Paint, which are two of them.
The six security headers every site should send, the value for each, the Mozilla Observatory penalty for a missing one, and how to set them on any host.