DeadCode Under the Hood: How Static Analysis Finds Dead React/Next.js Code
AST parsing, tree-shaking-aware export resolution, cross-file reference tracking, and why this catches things linters miss.
In a previous tutorial, we showed you how to use DeadCode to clean up a React/Next.js project. But how does it actually work under the hood?
DeadCode isn't just grep with patterns. It builds a full abstract syntax tree (AST) of every file, resolves imports and exports across files, tracks references bidirectionally, and distinguishes between "exported but unused" and "exported and used by a barrel file." Here's how.
Architecture Overview
DeadCode's analysis pipeline has four stages:
- Discovery — Find all source files by extension (.ts, .tsx, .js, .jsx, .css)
- Parsing — Build ASTs for every file, extracting exports, imports, and references
- Resolution — Resolve import/export relationships across files (including barrel files, re-exports, and path aliases)
- Analysis — Classify each export and reference as used, unused, or uncertain
Let's walk through each stage.
Stage 1: Discovery
DeadCode scans the project directory tree, respecting .gitignore and a built-in exclusion list (node_modules, .next, dist, build, .cache). It collects files by extension, categorizing them as:
- TypeScript/React — .ts, .tsx, .js, .jsx (AST-parsed for exports/imports)
- CSS — .css, .scss, .less (parsed for class names and selectors)
- Routes — Next.js App Router
page.tsx,route.ts,layout.tsx(special handling for file-system routing)
This stage also detects the project framework (Next.js Pages Router vs App Router vs plain React) to adjust analysis rules.
Stage 2: AST Parsing
Every source file is parsed into an AST using Python's tree-sitter bindings with the TypeScript and JavaScript grammars. Here's what DeadCode extracts from each file:
Exports
DeadCode classifies every export in the file:
export function X()— Named function exportexport const X = ...— Named variable exportexport default class X— Default class exportexport { X, Y }— Named re-exportexport * from './module'— Wildcard re-export (barrel)export type X— Type-only export
Imports
Each import is resolved to its source module and the specific symbols imported:
import { X } from './module'— Named import from relative pathimport X from './module'— Default importimport * as X from './module'— Namespace import (marks everything as used)import './styles.css'— Side-effect import (CSS, images)import type { X }— Type-only import
References
DeadCode tracks every identifier reference in the file:
- Function calls:
MyComponent() - JSX tags:
<MyComponent /> - Variable references:
myFunctionin expressions - Property access:
obj.method - CSS class usage:
className="my-class",styles.myClass
Stage 3: Cross-File Resolution
This is where DeadCode separates itself from a simple linter. It builds a bidirectional reference graph across all files.
Direct Resolution
For each import statement, DeadCode resolves the relative path to an actual file, accounting for:
- Extension resolution:
./component→component.tsx,component.ts,component/index.tsx - Path aliases:
@/components/Button→ resolved viatsconfig.jsonpaths orjsconfig.json - Node_modules: Package imports are noted but not analyzed (they're assumed to be used)
Barrel File Unwrapping
This is the trickiest case. A barrel file like components/index.ts often does:
export { Button } from './Button';
export { Card } from './Card';
export { Modal } from './Modal';
A naive tool would flag Button as "exported but unused" in Button.tsx because it looks at the source file in isolation. DeadCode instead:
- Sees that
export { Button }is a re-export from./Button - Checks if
Buttonis imported from the barrel file anywhere - If it is imported from the barrel, marks it as "used via barrel re-export"
- If it's not imported from anywhere, marks the re-export itself as dead
Wildcard Re-export Handling
export * from './utils' re-exports everything from utils.ts. DeadCode resolves the wildcard to the actual exports, then checks if any consumer imports specific symbols. If none of the wildcard-re-exported symbols are used downstream, the entire wildcard is flagged.
Stage 4: Dead Code Classification
After building the reference graph, DeadCode classifies every export and reference into one of several categories:
| Category | Description | Confidence |
|---|---|---|
| Unused export | Exported symbol never imported or referenced anywhere | High |
| Unused default export | Default export never imported by any file | High |
| Dead route | Next.js page file (page.tsx) not reachable from any navigation or redirect | Medium |
| Orphaned CSS | CSS class selector not referenced in any JSX className or styles object | Medium |
| Unused import | Import statement where no symbol is referenced in the file | High |
| Dead re-export | Barrel file re-exports a symbol that no consumer ever imports | High |
Why "Medium" Confidence?
Some categories are inherently uncertain:
- Dead routes — A page might be linked dynamically (
router.push('/users/' + id)) or from an external source. DeadCode uses heuristics (existence of direct imports from the page, file-system convention) but can't catch every dynamic reference. - Orphaned CSS — A CSS class might be used via
classListAPI, string concatenation, or dynamic class generation. DeadCode catchesclassName,class, andstyles.*patterns but not runtime string construction.
For these, DeadCode outputs a review level alongside remove, letting you triage manually.
Next.js App Router Special Handling
Next.js App Router uses file-system routing. DeadCode's Next.js detector looks for the app/ directory and applies special rules:
- page.tsx files — Checked against all
<Link>components,router.push()calls,redirect()calls, andnext.config.jsredirects - layout.tsx files — Always considered "used" if any child route exists (they wrap page content)
- loading.tsx, error.tsx, not-found.tsx — Special files that are automatically used if their parent segment has a page
- route.ts files — API routes checked against fetch() calls in client components
- middleware.ts — Always considered used if present at the root
Performance: How DeadCode Scales
DeadCode uses three optimization strategies to handle large codebases:
- Incremental parsing — Files are parsed once and cached. Only changed files are re-parsed on subsequent runs.
- Lazy reference resolution — The reference graph is built on-demand. If file A imports from file B, only B's exports are loaded, not B's entire reference chain.
- Parallel parsing — All files are parsed in parallel using a thread pool. For a 500-file project, parsing takes ~2 seconds.
Benchmark: A 1,200-file Next.js project with 35,000 exports completes full analysis in 6.8 seconds on a MacBook M3.
Comparison: DeadCode vs. ESLint (no-unused-vars)
| Capability | ESLint no-unused-vars | DeadCode |
|---|---|---|
| Unused exports | Single file only | Cross-file resolution |
| Barrel re-exports | Not detected | Full unwrapping |
| Dead API routes | Not detected | File-system routing analysis |
| Orphaned CSS | Not detected | CSS class reference tracking |
| Dead re-exports | Not detected | Re-export chain analysis |
| Unused imports | Single file | Single file + type-only awareness |
| TypeScript-aware | Via @typescript-eslint | Native tree-sitter TS grammar |
Real-World Impact
We ran DeadCode on the revenueholdings.dev landing page (a 15-page static site, ~2,800 lines of HTML + CSS) and on a production Next.js app (~1,200 files, ~150k lines). Results from the Next.js project:
- 23 unused components (never imported after a page redesign) — removed
- 4 dead API routes from an abandoned feature — removed
- 312 lines of orphaned CSS — removed
- 15 unused imports — removed
- 9 barrel-file re-exports for components that no consumer imported — removed
Total bundle size reduction: 18%. Cleanup time: 20 minutes (mostly reviewing DeadCode output and confirming removals).
Try It Yourself
Clean up your codebase
Install DeadCode and scan your React/Next.js project — no config file needed.
pip install git+https://github.com/Coding-Dev-Tools/deadcode.git
cd your-project
deadcode scan ./src
View on GitHub →
DeadCode is part of the Revenue Holdings developer tool ecosystem — 10 CLI tools built by autonomous AI for autonomous developers.