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 — the fastest JS/TS parser in the ecosystem.
Built with OXC (Rust-native JS/TS parser), Rayon for parallel file processing, and 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 |
unstable_props |
Object/array literals in JSX props — creates a new reference on every render |
large_component |
React components exceeding a configurable logical-line threshold |
no_new_context_value |
Object/array/function passed as Context.Provider value — causes all consumers to re-render |
no_array_index_key |
Array index used as JSX key prop — breaks reconciliation on list mutations |
no_expensive_in_render |
.sort() / .filter() / .reduce() / .find() / .flatMap() called directly in JSX props without useMemo |
Installation
From crates.io (recommended)
Build from source
The binary is at ./target/release/react-perf-analyzer.
Usage
react-perf-analyzer [OPTIONS] <PATH>
Arguments
| Argument | Description |
|---|---|
<PATH> |
File or directory to scan (recursively) |
Options
| Flag | Default | Description |
|---|---|---|
--format <FORMAT> |
text |
Output format: text, json, or html |
--output <FILE> |
stdout | Write output to a file instead of stdout |
--max-component-lines <N> |
300 |
Line threshold for the large_component rule |
--include-tests |
off | Include *.test.*, *.spec.*, *.stories.* files |
Examples
Scan a single file
Scan a full project
Scan an entire monorepo
node_modules, dist, build, and hidden directories are automatically skipped.
Tune the component size threshold
JSON output (for CI or tooling)
HTML report (shareable, self-contained)
# Generates react-perf-report.html in the current directory
# Custom output path
The HTML report includes:
- Summary cards — total issues, files scanned, files with issues
- Per-rule breakdown — colour-coded issue counts for each rule
- Top 10 files bar chart — quickly spot the worst offenders
- Collapsible issue table — all issues grouped by file with inline rule badges
No CDN dependencies — the output is a single self-contained .html file.
Sample output
Text format (default)
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. Wrap with useCallback.
src/pages/Dashboard.tsx:1:1 warning large_component Component 'Dashboard' is 340 lines (310 logical) — limit is 300.
src/contexts/Theme.tsx:12:30 warning no_new_context_value Context Provider 'value' receives a new object literal on every render.
src/lists/UserList.tsx:8:18 warning no_array_index_key Array index used as 'key' prop.
src/tables/DataGrid.tsx:45:30 warning no_expensive_in_render `.filter()` called directly in render — wrap with useMemo.
✖ 6 issues found
Scanned 42 file(s), found 6 issue(s).
HTML report
# ✅ HTML report written to: report.html
Open report.html in any browser — share it with your team, attach it to a Jira ticket, or archive it as a performance snapshot.
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:
onClick={() => doSomething()}onChange={function(e) { ... }}- Functions inside ternaries:
onClick={flag ? () => a() : () => b()}
Ignores: useCallback-wrapped and useMemo-wrapped functions
Fix:
// ❌ Before
// ✅ After
const handleDelete = ;
unstable_props
Detects object and array literals passed directly as JSX prop values. In JavaScript, {a:1} === {a:1} is false — a new reference on every render defeats React.memo.
Detects: style={{ color: "red" }}, columns={["id", "name"]}, literals in ternaries / logical expressions
Ignores: useMemo-wrapped values, stable variable references
Fix:
// ❌ Before
// ✅ After
const COLUMNS = ;
const style = ;
large_component
Flags React components whose logical line count exceeds the configured threshold (default 300). Reports total lines, logical lines, JSX element count, and hook count.
Fix: Extract sub-components and custom hooks by concern.
no_new_context_value
Detects object/array literals or inline functions passed as the value prop to a React Context Provider. Every render creates a new reference → all consumers re-render even when the data hasn't changed.
Detects:
// ❌ New object every render → ALL consumers re-render
// ❌ New array every render
Fix:
// ✅ Stable reference via useMemo
const value = ;
no_array_index_key
Detects .map() callbacks that use the array index as the JSX key prop. When items are inserted, removed, or reordered, React matches elements by key — an index key causes incorrect component reuse and broken state (inputs, focus, animations).
Detects:
// ❌ index as key
items.
items.
items.
Fix:
// ✅ Stable ID from data
items.
no_expensive_in_render
Detects expensive array operations (.sort(), .filter(), .reduce(), .find(), .findIndex(), .flatMap()) called directly in JSX attribute values without useMemo. These recompute on every render, including renders triggered by unrelated state changes.
Detects:
// ❌ filter() runs on every render
// ❌ sort() runs and mutates on every render
// ❌ Even inside a ternary
Fix:
// ✅ Memoized — only recomputes when allUsers changes
const activeUsers = ;
// ✅ Or inline useMemo in the prop
Exit codes
| Code | Meaning |
|---|---|
0 |
No issues found |
1 |
One or more issues found |
2 |
Fatal error (path not found, write error) |
CI usage:
||
Architecture
src/
├── main.rs # Entry point — Rayon parallel file pipeline
├── cli.rs # clap CLI definitions (path, format, output, thresholds)
├── 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, JSON, and HTML output formatters
├── utils.rs # Byte offset → line/column helper
└── rules/
├── mod.rs # Rule trait, Issue/Severity types, registry
├── no_inline_jsx_fn.rs # Rule 1
├── unstable_props.rs # Rule 2
├── large_component.rs # Rule 3
├── no_new_context_value.rs # Rule 4
├── no_array_index_key.rs # Rule 5
└── no_expensive_in_render.rs # Rule 6
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 serialization |
Test fixtures
# Rule 1 — inline functions
# Rule 2 — unstable props
# Rule 3 — large components
# Rule 4 — context value
# Rule 5 — array index key
# Rule 6 — expensive in render
# Zero issues — stable, well-written component