# react-perf-analyzer

A high-performance Rust CLI that detects React performance anti-patterns in JS/TS/JSX files using deep AST analysis.

> ⚡ Powered by [OXC](https://oxc-project.github.io/) — the fastest JS/TS parser in the ecosystem.

Built with [OXC](https://oxc-project.github.io/) (Rust-native JS/TS parser), [Rayon](https://github.com/rayon-rs/rayon) for parallel file processing, and [clap](https://github.com/clap-rs/clap) for the CLI.

---

## Rules

| Rule | What it detects |
|---|---|
| `no_inline_jsx_fn` | Inline arrow/function expressions in JSX props — creates a new function reference on every render |
| `large_component` | React components exceeding a configurable logical-line threshold |
| `unstable_props` | Object/array literals in JSX props — creates a new reference on every render |

---

## Installation

### Prerequisites

- [Rust](https://rustup.rs/) (edition 2021, stable toolchain)

### Build from source

```bash
git clone <repo-url>
cd react-perf-analyzer
cargo build --release
```

The binary is at `./target/release/react-perf-analyzer`.

### Install globally

```bash
cargo install --path .
```

Then call `react-perf-analyzer` from anywhere.

---

## Usage

```
react-perf-analyzer [OPTIONS] <PATH>
```

### Arguments

| Argument | Description |
|---|---|
| `<PATH>` | File or directory to scan (recursively) |

### Options

| Flag | Default | Description |
|---|---|---|
| `--format <text\|json>` | `text` | Output format |
| `--max-component-lines <N>` | `300` | Line threshold for `large_component` rule |

---

## Examples

### Scan a single file

```bash
react-perf-analyzer src/components/UserCard.tsx
```

### Scan a full project

```bash
react-perf-analyzer ./src
```

### Scan an entire monorepo

```bash
react-perf-analyzer /path/to/monorepo
```

`node_modules`, `dist`, and `build` directories are automatically skipped.

### Tune the component size threshold

```bash
react-perf-analyzer ./src --max-component-lines 150
```

### JSON output (for CI or tooling)

```bash
react-perf-analyzer ./src --format json > results.json
```

### Global install, then scan from anywhere

```bash
cargo install --path .
react-perf-analyzer /path/to/monorepo
```

---

## Sample output

```
src/components/UserCard.tsx:14:17  warning  unstable_props     Object literal in 'style' prop creates a new reference on every render.
src/components/UserCard.tsx:21:24  warning  no_inline_jsx_fn   Inline arrow function in 'onClick' prop. Extract to a named handler or wrap with useCallback.
src/pages/Dashboard.tsx:1:1        warning  large_component    Component 'Dashboard' is 340 lines (310 logical) — limit is 300.

✖ 3 issues found

Scanned 42 file(s), found 3 issue(s).
```

---

## Rule details

### `no_inline_jsx_fn`

Detects inline functions passed as JSX props that create a new reference on every render, breaking `React.memo` and `shouldComponentUpdate` optimizations.

**Detects:**
- Arrow functions: `onClick={() => doSomething()}`
- Function expressions: `onClick={function() { ... }}`
- Functions inside ternaries: `onClick={flag ? () => a() : () => b()}`
- Functions inside logical expressions: `onClick={enabled && (() => submit())}`

**Ignores:**
- `useCallback`-wrapped functions: `onClick={useCallback(() => ..., [deps])}`
- `useMemo`-wrapped functions: `render={useMemo(() => () => ..., [deps])}`

**Example fix:**
```jsx
// ❌ Before
<Button onClick={() => handleDelete(id)} />

// ✅ After
const handleDelete = useCallback(() => deleteItem(id), [id]);
<Button onClick={handleDelete} />
```

---

### `large_component`

Flags React components whose logical line count (total lines minus blank lines and comments) exceeds the configured threshold. Counts logical lines to avoid penalizing well-commented code.

Reports:
- Total lines, logical lines, blank lines
- JSX element count
- Hook call count
- Context-aware suggestion (extract custom hook vs. split sub-components)

**Detects all component forms:**
- `function MyComponent() {}`
- `const MyComponent = () => {}`
- `export default function() {}`
- `export const MyComponent = () => {}`
- `memo()`-wrapped components
- `forwardRef()`-wrapped components

**Example fix:**
```jsx
// ❌ Before — one 400-line component doing everything
function Dashboard() { /* ... */ }

// ✅ After — split by concern
function useDashboardData() { /* data-fetching hooks */ }
function DashboardHeader() { /* ... */ }
function DashboardContent() { /* ... */ }
function Dashboard() {
  const data = useDashboardData();
  return <><DashboardHeader /><DashboardContent data={data} /></>;
}
```

---

### `unstable_props`

Detects object and array literals passed directly as JSX prop values. In JavaScript, `{a:1} === {a:1}` is `false` — a new reference is created on every render, defeating `React.memo`.

**Detects:**
- Direct object literals: `style={{ color: "red" }}`
- Direct array literals: `columns={["id", "name"]}`
- Literals inside ternaries: `options={flag ? { type: "bar" } : defaults}`
- Literals inside logical expressions: `config={enabled && { dense: true }}`
- Parenthesized literals: `style={({ color: "red" })}`

**Ignores:**
- `useMemo`-wrapped values: `style={useMemo(() => ({ color }), [color])}`
- `React.useMemo`-wrapped values
- Stable variable references

**Example fix:**
```jsx
// ❌ Before
<UserCard style={{ color: "red", fontSize: 14 }} />
<DataTable columns={["id", "name", "email"]} />

// ✅ After — stable references
const COLUMNS = ["id", "name", "email"];
function UserCard({ color }) {
  const cardStyle = useMemo(() => ({ color, fontSize: 14 }), [color]);
  return <UserCard style={cardStyle} />;
}
<DataTable columns={COLUMNS} />
```

---

## Exit codes

| Code | Meaning |
|---|---|
| `0` | No issues found |
| `1` | One or more issues found |

Useful for CI pipelines:

```bash
react-perf-analyzer ./src || echo "Performance issues detected — failing build"
```

---

## Architecture

```
src/
├── main.rs          # Entry point — Rayon parallel file pipeline
├── cli.rs           # clap CLI definitions
├── file_loader.rs   # Recursive file discovery (walkdir)
├── parser.rs        # OXC JS/TS/JSX parser wrapper
├── analyzer.rs      # Runs all rules against a parsed AST
├── reporter.rs      # Text and JSON output formatters
├── utils.rs         # Byte offset → line/column helper
└── rules/
    ├── mod.rs               # Rule trait, Issue/Severity types
    ├── no_inline_jsx_fn.rs  # Rule 1
    ├── large_component.rs   # Rule 2
    └── unstable_props.rs    # Rule 3
```

**Key dependencies:**

| Crate | Purpose |
|---|---|
| `oxc_parser` / `oxc_ast` / `oxc_ast_visit` | Rust-native JS/TS/JSX parser and AST |
| `rayon` | Data-parallel file processing |
| `walkdir` | Recursive directory traversal |
| `clap` | CLI argument parsing |
| `serde` / `serde_json` | JSON output |

---

## Test fixtures

```bash
# Rule 1 — inline functions (12 issues expected)
react-perf-analyzer ./test_fixtures/inline_fn_cases.tsx

# Rule 2 — large components (5 issues expected)
react-perf-analyzer ./test_fixtures/large_component_cases.tsx --max-component-lines 20

# Rule 3 — unstable props (20 issues expected)
react-perf-analyzer ./test_fixtures/unstable_props_cases.tsx

# All rules — combined bad component (15 issues expected)
react-perf-analyzer ./test_fixtures/bad_component.tsx

# Zero issues — stable, well-written component
react-perf-analyzer ./test_fixtures/good_component.tsx
```
