# 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
| `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
| `<PATH>` | File or directory to scan (recursively) |
### Options
| `--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
| `0` | No issues found |
| `1` | One or more issues found |
Useful for CI pipelines:
```bash
---
## 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:**
| `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
```