TidyURLs
Go to Tools
BLOG
The TidyURLsJournal

Practical guides, tutorials, and insights about URLs, SEO, web development, and online marketing. Learn, troubleshoot, and get the most of the web.

Back to the Journal
TidyURLs Journal

How to Find Unused JavaScript and Reduce Your Bundle Size

Practical insights, clear explanations, and useful ideas for understanding the web.

October 26, 20269 min read6 topics
How to Find Unused JavaScript and Reduce Your Bundle Size
Explore the details
#Next.js#JavaScript#Web Performance#Bundle Analysis#Chrome DevTools#Frontend
October 26, 20269 min read

A large JavaScript bundle can include code a page never needs, but “unused” depends on the route and the user’s actions. This guide shows how to inspect production bundles in Next.js, use Chrome DevTools Coverage to find code that did not run, and decide whether dynamic imports, tree shaking, dependency changes, or narrower Client Component boundaries are appropriate. Measure before and after: a smaller bundle is measurable, but a speed improvement must be verified separately.

Key Takeaways

  • Use a production build to inspect shipped chunks; development output is not a reliable bundle-size baseline.
  • Bundle analysis shows what is included. DevTools Coverage shows what did not execute during a particular recording. Use both, and test representative routes and interactions.
  • Dynamic imports help defer code only when the imported feature is not needed immediately.
  • In Next.js, keep Client Component boundaries narrow: a file marked with "use client" brings its imports into the client-side dependency graph.
  • Compare the same routes, build settings, and interactions before and after a change. Report bundle measurements separately from page-load or interaction performance.

Start with a production baseline

Choose a representative route and record a baseline before editing dependencies or component boundaries. Keep the commit, lockfile, build command, and environment consistent so the comparison is useful.

For a Next.js app, build using the command your project uses for production, commonly:

npm run build

Note the route’s generated JavaScript and inspect any build output that reports route or chunk sizes. The exact output varies by Next.js version and build system. Save the output or analyzer report, and record which route you are investigating.

Also open the deployed page or a production-equivalent build in Chrome. In DevTools, open the Command Menu, search for Show Coverage, start recording, and reload the page. Exercise the route as a real user would: open menus, switch tabs, and trigger relevant interactions. Coverage reports which JavaScript bytes were not executed during that recording.

Coverage is not proof that code is unnecessary. A module can be unused on the initial view but required after a click, on another route, or in a different browser state. Repeat the recording with the interactions that matter.

Find what is in the bundle

Analyze Next.js bundles

Next.js documents package-bundling analysis, including the @next/bundle-analyzer approach for builds that use webpack. If your project uses a different Next.js version or build system, check the current Next.js documentation for the compatible analyzer and invocation; do not assume a webpack-specific plugin analyzes every build mode.

For a webpack-based project, install the analyzer as a development dependency:

npm install --save-dev @next/bundle-analyzer

A CommonJS next.config.js can enable it conditionally:

const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({});

Run the analyzer-enabled production build on macOS or Linux:

ANALYZE=true npm run build

On PowerShell, set the environment variable for the command instead:

$env:ANALYZE = "true"
npm run build

Use the report to identify large modules in client-side chunks, then trace them to the component or package that imports them. A dependency appearing in the report does not automatically mean it is wasteful: it may be required for a feature or shared across routes.

Inspect dependencies

Start with the dependency tree rather than removing packages based only on package.json:

npm ls --depth=0
npm explain package-name

npm explain can help show why a package is installed. For pnpm or Yarn, use the equivalent pnpm why package-name or yarn why package-name command. Then search the codebase for imports and check whether the package is used by a route, a Client Component, or server-only code.

A package can be present in the project without contributing its full contents to a particular page’s client bundle. Conversely, a small-looking import can pull in transitive modules. Confirm the effect in the analyzer after changing an import or dependency.

Use Coverage to find code that did not run

Coverage complements bundle analysis rather than replacing it:

ToolWhat it helps answerImportant limitation
Next.js bundle analyzerWhich modules contribute to analyzed chunks?A large module may be necessary, and report details depend on build mode.
Chrome DevTools CoverageWhich recorded bytes were not executed during this session?It only reflects the routes and interactions you recorded.
Dependency inspectionWhy is a package installed, and where might it be imported?Installation alone does not establish that code reaches a browser bundle.

In Coverage, inspect JavaScript entries associated with the page and open relevant files to locate unexecuted ranges. Investigate before removing code: conditional features and rarely used interactions may not run during a simple reload. Repeat with important user flows and other routes.

If you remove an import, component, or package, rebuild and repeat both checks. The analyzer can show whether a chunk changed; Coverage can show whether the new page behavior still exercises the intended feature.

Reduce code shipped to the browser

Keep Client Component boundaries narrow

In the Next.js App Router, Server Components are the default. A Client Component is needed for browser-only APIs, state, event handlers, and other interactive behavior. The "use client" directive marks a client entry point; modules it imports become part of that client-side dependency graph.

Put the directive on the smallest component that needs client behavior, rather than a high-level layout or page that contains mostly static content. Pass data or rendered content across the boundary where appropriate, and avoid importing server-only modules into client code. This can keep unrelated components and dependencies out of the browser bundle, but verify the actual chunks after restructuring.

Defer large features with dynamic imports

A charting library, editor, or other substantial feature may not be needed until a user opens a panel. In a Client Component, Next.js supports next/dynamic for loading a component on demand. For example, if HeavyChart is a default-exported Client Component:

"use client";

import { useState } from "react";
import dynamic from "next/dynamic";

const HeavyChart = dynamic(() => import("./HeavyChart"), {
  loading: () => <p>Loading chart…</p>,
});

export default function ChartPanel() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <section>
      <button onClick={() => setIsOpen((open) => !open)}>
        {isOpen ? "Hide chart" : "Show chart"}
      </button>
      {isOpen ? <HeavyChart /> : null}
    </section>
  );
}

This makes the chart eligible to load separately instead of being required for the initial render. Confirm the behavior in the production build and browser: rendering conditions, framework behavior, and preloading can affect when a chunk is requested. A loading state is important when the feature takes time to arrive.

Dynamic imports are not automatically a win. If the feature appears immediately on every visit, splitting it may add a request or delay its availability without avoiding much initial work. Next.js also documents constraints around ssr: false: it is for Client Components, not Server Components. Use it only when disabling server rendering is appropriate for that component.

Check tree shaking and import patterns

Tree shaking lets a bundler remove exports it can determine are unused. It works best when the package and build pipeline expose analyzable module structure, commonly ES modules. It is not a guarantee that any unused-looking code will disappear: side effects, CommonJS modules, package configuration, and import patterns can affect what can be removed.

When a package supports documented subpath imports, importing only the needed module can make intent clearer and may reduce included code. Do not guess an internal package path or replace a public import without checking that package’s documentation and compatibility guarantees. Compare the resulting production report.

Avoid treating every barrel file as a problem. Modern bundlers can optimize some barrel exports, while other package structures may retain more modules than expected. Use the analyzer to identify a specific contribution before changing imports or package configuration.

Measure before and after

Use this repeatable comparison:

  1. Establish a baseline. Build the same commit with the same Next.js version, lockfile, build command, and environment. Save route and chunk information from the build or analyzer.
  2. Record a representative browser session. In Chrome Coverage, reload the target route and perform the interactions needed to exercise its features. Note which files and ranges were not used.
  3. Choose one change. For example, defer a rarely opened editor, narrow a Client Component boundary, or replace a confirmed unnecessary dependency. One change at a time makes attribution easier.
  4. Rebuild and compare. Repeat the same production build and inspect the same route and chunks. Check both the size change and whether expected functionality remains.
  5. Repeat browser checks. Use the same device conditions, cache state, route, and interactions when comparing runtime measurements. If you measure load or interaction performance, report the metric and test setup; do not infer a speed gain from a smaller bundle alone.

Keep the analyzer report and test notes with the change when bundle size is an important project constraint. If a code split reduces the initial chunk but increases later loading for an interaction, document that trade-off rather than presenting only one number.

Common Mistakes

  • Using development output as the baseline. Development mode and production builds have different bundling behavior. Compare production builds.
  • Calling all uncovered code dead code. Coverage only describes a recorded session. Test relevant interactions and routes before removing code.
  • Assuming a dependency is fully shipped to every page. Trace imports and inspect the route’s chunks instead of relying on the dependency list alone.
  • Adding dynamic imports everywhere. Splitting frequently used code can add loading boundaries without reducing the work users need for the initial view.
  • Marking a whole page or layout as a Client Component. Broad client boundaries can pull more dependencies into the browser-side graph than necessary.
  • Reporting a performance win from bundle bytes alone. Bundle size and runtime performance are related but distinct measurements. Verify the user-facing metric independently.

Best Practices

  • Analyze production builds and keep the build environment consistent across comparisons.
  • Combine bundle analysis with Coverage and dependency inspection; no single tool answers every question.
  • Test realistic interactions before deciding code is unused.
  • Keep browser-only behavior and "use client" boundaries as local as practical.
  • Defer code only when the feature can genuinely wait, and provide a useful loading state.
  • Recheck functionality, accessibility, and runtime measurements after changing loading behavior.

Conclusion

Finding unused JavaScript is an evidence-gathering task: identify what is shipped, observe what runs in representative sessions, and trace both findings back to code and dependencies. Make a focused change, rebuild, and compare the same routes and interactions. That process can establish whether bundle contents changed; only separate performance measurements can establish whether users experienced a speed improvement.

References