DEV Community

Cover image for eslint-plugin-import Spends 148s Finding Circular Deps in 5,000 Files. import-next Does It in 2.7s.
Ofri Peretz
Ofri Peretz

Posted on • Edited on • Originally published at ofriperetz.dev

eslint-plugin-import Spends 148s Finding Circular Deps in 5,000 Files. import-next Does It in 2.7s.

The no-cycle rule on 5,000 files: eslint-plugin-import takes 148 seconds;
eslint-plugin-import-next takes 2.7154.9x measured, because O(n²)
graph traversal became O(n). At 10K files the gap projects past 100x (we stopped
measuring eslint-plugin-import at 5K — it was already taking ~2.5 minutes per
run). Full numbers, methodology, and a drop-in migration below.

🔄 Drop-in replacement — compatible with the eslint-plugin-import rule set,
with faster graph algorithms, CWE/LLM-optimized messages, and fewer false
positives/negatives.

TL;DR

Benchmark 1K Files 5K Files 10K Files
Core Rules (9) 1.6x 3.3x 5.2x
Recommended Preset 1.4x 3.0x 5.5x
no-cycle Only 25.7x 54.9x ~120x (projected)

The 54.9x is measured; the 10K column is a projection (we stopped running
eslint-plugin-import at 5K — it was already ~2.5 minutes). Details below.


Why eslint-plugin-import is Slow

The original eslint-plugin-import uses an O(n²) module resolution algorithm:

  1. For each file, parse all imports
  2. For each import, resolve the full module path
  3. For no-cycle, traverse the entire dependency graph for every file

This creates quadratic complexity. On 5,000 files with interconnected imports, the no-cycle rule alone takes 148 seconds.

How eslint-plugin-import-next Fixes This

We rewrote the core algorithms:

  1. Cached module resolution — resolve each path once, cache permanently
  2. Incremental graph building — build the dependency graph incrementally, not per-file
  3. Cycle detection with Tarjan's algorithm — O(n) instead of O(n²)

Result: 2.71 seconds for the same 5,000 files.


Benchmark 1: Core Rules (9 rules)

Both plugins configured with identical rules:

Files eslint-plugin-import eslint-plugin-import-next Speedup
1,000 2.80s 1.78s 1.6x
5,000 19.04s 5.76s 3.3x
10,000 58.67s 11.26s 5.2x

Takeaway: Even with basic rules, the performance gap grows with codebase size.


Benchmark 2: Recommended Preset

Using the full recommended configuration from each plugin.

Files eslint-plugin-import eslint-plugin-import-next Speedup
1,000 2.42s 1.78s 1.4x
5,000 18.43s 6.07s 3.0x
10,000 57.74s 10.57s 5.5x

Takeaway: Recommended presets show similar scaling — 5.5x faster at 10K files.


Benchmark 3: no-cycle Rule Only

This is where the difference is massive. The no-cycle rule detects circular dependencies.

Files eslint-plugin-import eslint-plugin-import-next Speedup
1,000 27.03s 1.05s 25.7x
5,000 148.59s 2.71s 54.9x
10,000 ~600s (projected)* ~5s (projected) ~120x (projected)

*10K Projection Note: 5K→10K doubles the file count, so O(n²) roughly quadruples eslint-plugin-import's time (148.59s × 4 ≈ 600s ≈ 10 minutes) — we didn't run it because 10+ minutes per iteration is impractical. eslint-plugin-import-next is O(n), so its time roughly doubles (2.71s × 2 ≈ 5s). 600 / 5 ≈ 120x — a projection, not a measurement; the measured maximum is the 54.9x at 5K.

Takeaway: If you use no-cycle (and you should), the speedup is 25-100x depending on codebase size.

┌────────────────────────────────────────────────────────────────┐
│ no-cycle Rule: 5,000 files                                     │
├────────────────────────────────────────────────────────────────┤
│ eslint-plugin-import:      148.59s ████████████████████████████│
│ eslint-plugin-import-next:   2.71s █                           │
└────────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Why no-cycle is Critical

Circular dependencies cause:

  • Build failures with tree-shaking
  • Runtime bugs with undefined imports
  • Memory leaks in bundlers
  • Test flakiness from initialization order

Most teams disable no-cycle because it's too slow. With eslint-plugin-import-next, you can finally enable it.


Methodology

Apple-to-apple comparisonfull source code

Spec Details
Codebase sizes 1,000 / 5,000 / 10,000 JavaScript files
Iterations 3-5 runs per size, per plugin
Fixtures Realistic JS files with named/default imports, barrel files, cross-file dependencies
Environment Node v20.19.5, Apple Silicon M1 (arm64), ESLint v9.17.0
Cache Cleared between each run

Run It Yourself

git clone https://github.com/ofri-peretz/eslint-benchmark-suite.git
cd eslint-benchmark-suite
npm install
npm run generate:import
npm run benchmark:import
npm run benchmark:import-recommended
npm run benchmark:import-no-cycle
Enter fullscreen mode Exit fullscreen mode

Migration Takes 2 Minutes

# Remove old plugin
npm uninstall eslint-plugin-import

# Install new plugin
npm install --save-dev eslint-plugin-import-next
Enter fullscreen mode Exit fullscreen mode
// eslint.config.mjs — `configs` is a NAMED export (default export is the plugin)
import { configs } from "eslint-plugin-import-next";
export default [configs.recommended];
Enter fullscreen mode Exit fullscreen mode

Compatibility

Surface Support
Package managers npm, yarn, pnpm, bun
Node >= 18.0.0
ESLint `^8.0.0 \
Compatibility drop-in for the {% raw %}eslint-plugin-import rule set
Module system Plugin ships CommonJS; your config can be eslint.config.js or .mjs
Oxlint no-cycle flagship rule wired via the interlace-import-next port, parity-gated

Links

⭐ Star on GitHub if you've ever disabled no-cycle because it was too slow to run.


I'm Ofri Peretz, a security engineering leader and the author of the
Interlace ESLint ecosystem — domain-specific static analysis for security,
reliability, and performance on the Node.js stack.

ofriperetz.dev · LinkedIn · GitHub

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.