May 15, 2026 · 8 min read
Clean Up Your React/Next.js Codebase with DeadCode
Every codebase accrues dead code. A feature gets replaced, but the old component stays. A route gets merged, but the page file lingers. A utility export was used exactly once and now the import is gone but the export remains.
Over time, this becomes a tax on every developer who touches the project:
- New hires waste hours wondering "is this component used?" before modifying it
- Build times slowly inflate as dead modules are still parsed and bundled
- Refactoring becomes a game of "ship it and hope X didn't use that export"
- Code coverage numbers lie — a file with 0% covered dead code shows as "well tested"
DeadCode is a CLI tool that scans your project and identifies unused exports, unreferenced components, dead route files, and orphaned CSS classes. It runs in seconds, integrates with any CI pipeline, and returns actionable results — not noise.
What We'll Build
We'll take a realistic Next.js App Router project and walk through:
- Scanning for unused exports and dead routes
- Interpreting the output (what's real vs. false positives)
- Removing the dead code safely
- Setting up DeadCode in CI to prevent backsliding
Step 1: Install and Scan
Install DeadCode:
pip install git+https://github.com/Coding-Dev-Tools/deadcode.git
Navigate to your project root and run a scan. For a Next.js App Router project, we care about app/, components/, lib/, and utils/:
deadcode scan -p . --exclude node_modules,.next --verbose
Here's what the output looks like on a real project:
Scanning /Users/dev/my-next-app...
[UNUSED_EXPORT] src/lib/formatting.ts:12 - formatRelativeTime
Last import found 47 commits ago (Mar 2025)
Used by: (none)
[UNUSED_EXPORT] src/lib/formatting.ts:34 - formatCurrency
Last import found: never
Used by: (none)
[DEAD_ROUTE] app/dashboard/legacy-reports/page.tsx
No internal links point to /dashboard/legacy-reports
git log --oneline --diff-filter=D -- app/dashboard/legacy-reports/
→ route was replaced by /dashboard/analytics 6 months ago
[DEAD_ROUTE] app/api/v1/deprecated-endpoint/route.ts
No internal links point to /api/v1/deprecated-endpoint
Search npm: "deprecated-endpoint" — no external callers found in repo
[UNREFERENCED_COMPONENT] src/components/old-hero-banner.tsx
Exported as HeroBanner — no imports found
git log: last modified 14 months ago (feature/redesign)
[UNREFERENCED_COMPONENT] src/components/deprecated/beta-badge.tsx
Exported as BetaBadge — no imports found
Note: directory name "deprecated" suggests this was intentionally archived
[ORPHANED_CSS] src/styles/legacy-utilities.css
Selectors .legacy-grid, .old-card-stack — no matches in any .tsx/.ts file
Note: 0 matches across 342 source files
Summary: 4 unused exports, 2 dead routes, 2 unreferenced components, 1 orphaned stylesheet
Estimated savings: 44 KB bundle size, 327 lines of codeStep 2: Triage the Findings
Not every flagged item is safe to remove. Here's how to triage each category:
Unused Exports (formatRelativeTime, formatCurrency)
These are the safest to remove. DeadCode traces every import statement in the project. If formatRelativeTime is exported but never imported anywhere, it's dead. The only exception is re-exports from barrel files (index.ts) — DeadCode handles those by resolving the chain. If a public library export is consumed outside the repo (e.g., an npm package), use --allow-unresolved-imports or mark it with an annotation.
Decision for formatRelativeTime: Remove it. It was replaced by date-fns/formatDistanceToNow 47 commits ago. The comment in the code even says "TODO: migrate to date-fns."
Dead Routes (legacy-reports, deprecated-endpoint)
Next.js App Router pages and API routes that no internal link points to. DeadCode checks all <Link>, router.push(), <a> tags, and redirect configs. If no path points to /dashboard/legacy-reports and the git log confirms it was replaced, it's safe to delete the directory.
For API routes, DeadCode also checks for external callers by searching the codebase for URL references. The deprecated-endpoint route has no callers — the frontend migrated to the v2 endpoint 6 months ago. Remove it.
Unreferenced Components (old-hero-banner, beta-badge)
DeadCode checks for direct imports, dynamic imports (next/dynamic), and React.lazy references. old-hero-banner was part of a redesign and was replaced by modern-hero-banner. The directory name "deprecated" for beta-badge is a strong signal it was intentionally archived. Both safe to delete.
Orphaned CSS
CSS files that are imported but whose selectors don't match any rendered components. legacy-utilities.css is imported in layout.tsx (so it's bundled) but none of its classes appear in any TSX file. Delete the file and remove the import.
Step 3: Remove the Dead Code
With DeadCode, you have two removal strategies:
Manual removal (recommended for first pass)
Delete files one by one, running the test suite after each. This is safest because you verify no breakage before moving on.
# Delete dead routes
rm -rf app/dashboard/legacy-reports/
rm -rf app/api/v1/deprecated-endpoint/
# Delete unused components
rm -rf src/components/old-hero-banner.tsx
rm -rf src/components/deprecated/
# Remove unused exports (edit the file)
# Delete formatRelativeTime and formatCurrency functions + their exports
# Remove orphaned CSS file + import
rm src/styles/legacy-utilities.css
# Also remove: import './legacy-utilities.css' from layout.tsx
# Re-run scan to verify
deadcode scan -p . --exclude node_modules,.next
Scan complete: 0 dead exports, 0 dead routes, 0 unreferenced components, 0 orphaned CSS files
Estimated savings realized: 327 LOC, 44 KB bundle sizeBatch removal with verification
Once you trust the output, use --json to pipe results into your own scripts:
deadcode scan -p . --exclude node_modules,.next --json > deadcode-results.json
# Then process with jq or a script to generate delete commands
cat deadcode-results.json | jq -r '.[] | select(.severity=="high") | .file' | xargs rm
--allow-list flag lets you suppress findings you've consciously decided to keep (e.g., a publicly exported utility you plan to deprecate over two releases). Run with --generate-allow-list to create an initial baseline, then review and trim it.
Step 4: Set Up DeadCode in CI
The real power is preventing dead code from accumulating again. Add DeadCode to your CI pipeline as a check that blocks PRs introducing unused exports:
# .github/workflows/deadcode-check.yml
name: Dead Code Check
on: [pull_request]
jobs:
deadcode:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install git+https://github.com/Coding-Dev-Tools/deadcode.git
- run: deadcode scan -p . --exclude node_modules,.next
# Fails (exit 1) if unused exports or dead routes are found
Now every PR is automatically checked. If a developer adds an export but forgets to use it, or merges a route change without cleaning up the old page, DeadCode catches it before it reaches main.
DeadCode vs. ESLint (no-unused-vars)
You might be thinking: "Doesn't ESLint already handle this?" Let's clarify the difference:
| ESLint no-unused-vars | DeadCode | |
|---|---|---|
| Scope | Variables in a single file | Exports, routes, CSS across the entire project |
| Cross-file | No — per-file only | Yes — resolves import chains across the whole project |
| Next.js routes | No | Yes — checks app/ and pages/ route linking |
| CSS selectors | No | Yes — checks if selectors match rendered components |
| Dynamic imports | No | Yes — checks next/dynamic and React.lazy |
ESLint's no-unused-vars catches local variables you declared but never used inside a single file. But it won't catch an exported component that nothing imports, a page that no route links to, or a CSS class that no component references. Those are DeadCode's job.
Real-World Impact
In a recent cleanup of an internal Next.js monorepo, DeadCode found:
- 47 unused exports (14 of which were entire utility files with 0 consumers)
- 6 dead routes — pages that hadn't been linked from any nav in over a year
- 12 unreferenced components — the most painful was a PaymentForm that had been replaced but the old file still existed in a "components/archived" folder, imported by nobody
- 3 orphaned CSS files adding ~12 KB to every page bundle because they were imported in the root layout
The cleanup saved 168 KB of parsed bundle size (pre-minification, post-tree-shaking) and removed 2,100+ lines of code that were doing nothing but increasing maintenance burden.
Find unused exports, dead routes, orphaned CSS, and unreferenced components — in seconds.
Try It Now
DeadCode is free and open source under Apache 2.0. Install it, run it on your project, and see how much dead code you've accumulated. The first scan might surprise you.
pip install deadcode
deadcode scan -p /path/to/react-project --exclude node_modules
- DeadCode on GitHub
- DeadCode Documentation
- DeadCode vs knip comparison
- Pricing — free tier works for local dev and CI
Get Early Access
PyPI publishing is coming soon. Leave your email and we'll notify you the moment DeadCode and the rest of the Revenue Holdings tools ship.
Star us on GitHub · View Pricing