Ship fast. Stay fast.
Perf Fiscal is a professional ESLint plugin that brings the discipline of a performance engineer to every code review. It understands your whole codebase (cross-file analysis), speaks React and Vue.js fluently, and leverages a Rust core for speed and accuracy.
Prefer Portuguese? Veja a versão traduzida em README-pt.md.
- Why Perf Fiscal
- What’s New
- Quick Start
- Rust Core Engine
- Cross-File Intelligence
- Configuration
- Rule Catalog
- Examples
- Compatibility
- Development
- Contributing
- License
- Stay in the Loop
- Whole-codebase awareness: understands components, props, async flows, and imports across module boundaries.
- React-savvy: protects memo boundaries, dependency arrays, and Context stability with actionable suggestions.
- Vue-optimized: detects inefficient computed properties, watchers, and reactivity patterns in both Options and Composition API.
- Performance-first rules: catch heavy loops, quadratic growth, costly string ops, and bundle pitfalls early.
- Supply-aware imports: detect heavy entrypoints and suggest subpath or alternative imports with confidence.
- Rust acceleration: optional Rust core for parsing, indexing, and security checks with safe JS fallbacks.
- Low friction: flat and classic ESLint presets, zero mandatory setup beyond your existing TS config.
- 🔍 Whole-project analyzer: indexes exports, memo wrappers, and expected prop signatures (function | object | array | primitive) for each React component, dramatically reducing false positives.
- 🙌 Context-aware
no-unstable-inline-props: relaxes warnings for non-memoized components and aligns diagnostics with the prop’s declared kind. - 🛟 Typed
no-unhandled-promises: recognizes Promise-returning helpers imported from other modules rather than relying on names. - 🧱 Extensible infrastructure: rules query shared metadata via
getCrossFileAnalyzer, enabling heuristics that understand the entire project graph.
Perf Fiscal tracks memo boundaries, prop kinds, and async flows across files—delivering smarter, low-noise diagnostics that scale.
tests/fixtures/cross-file/consumer.tsx:21:7
21:7 warning perf-fiscal/no-unhandled-promises Unhandled Promise: await this call or return/chain it to avoid swallowing rejections.
• Origin: useDataSource (exported from tests/fixtures/cross-file/components.tsx)
That single diagnostic traces the async helper to its source file, proving the analyzer understands memo boundaries and async flows beyond the current module.
When running perf-fiscal/no-unstable-inline-props, you'll see context-aware feedback like:
src/pages/Profile.tsx:12:13: [perf-fiscal/no-unstable-inline-props] Passing inline function to memoized child <Child onSelect={...}/> — wrap in useCallback for stable renders (expected prop kind: function)
And for cross-file async flow detection:
src/utils/api.ts:8:5: [perf-fiscal/no-unhandled-promises] Unhandled Promise returned from helper `fetchUserData` (imported from utils/http.ts) — consider awaiting or handling rejections.
These examples show how analyzer-backed diagnostics include origin and expected prop-kind, making fixes faster and more confident.
- ⚡ Cross-File Metadata Graph (Rust): index your project in parallel and expose component/props/import/export metadata through a fast JSON snapshot.
- 🧩 SWC-based Rust parser: new
parseCLI for JS/TS/JSX/TSX with a thin TypeScript bridge and graceful fallbacks. - 🦀 Rust security guardrail:
check-redosbolstersno-redos-regexdetection with safe JSON I/O and timeouts. - 📦 Smarter import hygiene:
no-heavy-bundle-importshelps avoid pulling monolithic entrypoints; docs updated with rationale and options. - 🛡️ React stability:
no-inline-context-valueprevents Context churn before it cascades across your app.
See detailed notes in docs/changelog/0.5.0.md. To opt out of analyzer trace data, keep debugExplain set to false (default) or disable per rule:
{
"perf-fiscal/no-unhandled-promises": ["warn", { "debugExplain": false }]
}Found a regression or noisy warning? Use the dedicated False Positive issue template so we can triage quickly.
🧭 Need typed diagnostics? Review the Typed Analyzer Setup checklist. In short: (1) create a lint-oriented
tsconfigthat includes every file you want to analyze, (2) pointparserOptions.project/tsconfigRootDirto that config, and (3) keep@typescript-eslint/parserin sync with ESLint. If ESLint reports "Cannot read file 'tsconfig...json'" or "parserServices to be generated," double-check thetsconfigRootDirguidance in the setup guide.
npm install --save-dev eslint eslint-plugin-perf-fiscal
# or
yarn add --dev eslint eslint-plugin-perf-fiscal
# or
pnpm add -D eslint eslint-plugin-perf-fiscalPerf Fiscal can optionally leverage a lightweight Rust core for speed and precision. When unavailable, the plugin gracefully falls back to the existing JS implementations.
Build once (local or CI):
cd rust/perf-linter-core
cargo build --releaseOptionally point to the binary (if not on the default path):
export PERF_LINTER_CORE="$(pwd)/target/release/perf-linter-core"Available commands and bridges:
- ReDoS checker:
perf-linter-core check-redos(STDIN{ "pattern": string }→ STDOUT{ "safe": boolean, "rewrite"?: string }) - Parser (SWC):
echo "const x=1" | perf-linter-core parse --filename input.tsx - Project indexer:
perf-linter-core index /path/to/project > metadata.json
TypeScript bridges:
- Parser bridge:
src/utils/rust-parser.ts(parseWithRust(source, filename)with cache + timeout) - Cross-file analyzer bridge:
src/analyzer/cross-file.ts(getCrossFileAnalyzer(projectRoot)with file + memory cache)
import perfFiscal from 'eslint-plugin-perf-fiscal';
const tsParser = await import('@typescript-eslint/parser');
export default [
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser: tsParser.default,
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: import.meta.dirname
}
}
},
perfFiscal.configs.recommended // For React projects
// or
// perfFiscal.configs['flat/vue'] // For Vue.js projects
];Note: The cross-file analyzer benefits from project-aware parser settings (parserOptions.project + tsconfigRootDir) so it can ask the TypeScript checker about symbol relationships across files.
Ready to adopt Perf Fiscal in an existing codebase? Choose the guide that matches your architecture:
- React Application Migration Guide – stage the rollout across React apps and React Native projects while maintaining memo stability.
- Vue.js Application Migration Guide – adopt performance rules for Vue 3 projects using Composition API or Options API.
- Node.js Service Migration Guide – integrate the plugin into backend services, CLIs, and worker processes.
- Mixed Monorepo Migration Guide – coordinate adoption across workspaces that blend frontends, services, and shared packages.
Each guide includes step-by-step rollout plans, configuration snippets, and compatibility notes tailored to the targeted environment.
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: ['./tsconfig.json'],
tsconfigRootDir: __dirname
},
extends: ['plugin:perf-fiscal/recommended'] // For React
// or
// extends: ['plugin:perf-fiscal/vue'] // For Vue.js
};module.exports = {
plugins: ['perf-fiscal'],
rules: {
'perf-fiscal/no-expensive-split-replace': 'warn',
'perf-fiscal/prefer-array-some': 'error',
'perf-fiscal/no-unstable-inline-props': ['warn', {
ignoreProps: ['className'],
checkSpreads: false
}]
}
};Each rule ships with in-depth guidance in docs/rules/<rule-name>.md.
| Rule | Detects | Recommended Action | Documentation |
|---|---|---|---|
perf-fiscal/detect-unnecessary-rerenders |
🚦 Inline handlers passed to memoized children | Hoist callbacks or wrap with useCallback |
docs/rules/detect-unnecessary-rerenders.md |
perf-fiscal/no-expensive-computations-in-render |
🧮 Heavy synchronous work executed during renders | Move logic into useMemo or outside the component |
docs/rules/no-expensive-computations-in-render.md |
perf-fiscal/no-expensive-split-replace |
🔁 Repeated string split/replace inside hot loops |
Pre-compute and reuse results | docs/rules/no-expensive-split-replace.md |
perf-fiscal/no-heavy-bundle-imports |
📦 Default imports from hefty packages (lodash, moment, legacy SDKs) |
Switch to subpath imports or lighter alternatives | docs/rules/no-heavy-bundle-imports.md |
perf-fiscal/no-inline-context-value |
🫧 Inline objects/arrays passed to Context.Provider value |
Wrap the value in useMemo or hoist it outside renders |
docs/rules/no-inline-context-value.md |
perf-fiscal/no-quadratic-complexity |
🧮 Nested loops that scale quadratically | Refactor loops or pre-index collections | docs/rules/no-quadratic-complexity.md |
perf-fiscal/no-redos-regex |
🔥 Regular expressions prone to catastrophic backtracking | Rewrite expression or add explicit bounds | docs/rules/no-redos-regex.md |
perf-fiscal/no-unhandled-promises |
Await or attach .catch/.then handlers |
docs/rules/no-unhandled-promises.md | |
perf-fiscal/no-unstable-inline-props |
✋ Inline functions/objects and prop spreads that churn references | Hoist or memoize prop values before passing | docs/rules/no-unstable-inline-props.md |
perf-fiscal/no-unstable-usememo-deps |
🧩 Non-stable values in dependency arrays | Memoize dependencies or move them outside the render | docs/rules/no-unstable-usememo-deps.md |
perf-fiscal/prefer-array-some |
✅ filter(...).length checks used for existence |
Replace with Array.prototype.some |
docs/rules/prefer-array-some.md |
perf-fiscal/prefer-for-of |
🔄 Using map/forEach purely for side effects |
Switch to for...of for clarity and speed |
docs/rules/prefer-for-of.md |
perf-fiscal/prefer-object-hasown |
🧾 Legacy hasOwnProperty.call patterns |
Use Object.hasOwn |
docs/rules/prefer-object-hasown.md |
perf-fiscal/prefer-promise-all-settled |
🤝 Promise.all expecting partial failures |
Migrate to Promise.allSettled |
docs/rules/prefer-promise-all-settled.md |
-
🧰 Flat vs. classic presets: Use
perfFiscal.configs.recommendedfor flat configs orplugin:perf-fiscal/recommendedfor classic configs. -
🛰️ Enable cross-file intelligence: Configure
@typescript-eslint/parserwithparserOptions.projectandtsconfigRootDirso Perf Fiscal can invoke the TypeScript checker and follow symbols across files. -
🧭 Severity control: Adjust rule severities (
off,warn,error) to match your governance model. -
⚙️ Rule options: Some rules expose targeted settings. Review each rule’s documentation for schema definitions. Example:
'perf-fiscal/no-unstable-inline-props': ['warn', { ignoreProps: ['className', 'data-testid'], checkFunctions: true, checkObjects: true, checkSpreads: true }], 'perf-fiscal/no-heavy-bundle-imports': ['warn', { packages: [ { name: 'lodash', suggestSubpath: true }, { name: '@org/legacy-sdk', allowNamed: true } ] }]
-
🧮 Performance strictness presets: The high-signal rules now accept shared options—
strictness(relaxed|balanced|strict),includeTestFiles,includeStoryFiles, anddebugExplain. Use them to dial noise, skip fixture-heavy folders, or surface confidence hints:'perf-fiscal/no-expensive-computations-in-render': ['warn', { strictness: 'strict', includeTestFiles: false, debugExplain: true }], 'perf-fiscal/no-expensive-split-replace': ['warn', { strictness: 'relaxed' }], 'perf-fiscal/no-unhandled-promises': ['error', { strictness: 'balanced' }]
// Before: re-creates callbacks every render
const Parent = () => <Child onSelect={() => dispatch()} />;
// After: keep reference identity stable
const Parent = () => {
const onSelect = useCallback(() => dispatch(), []);
return <Child onSelect={onSelect} />;
};// Before: expensive split executed for each item
for (const record of records) {
const parts = record.path.split('/');
visit(parts);
}
// After: compute once and reuse
const parts = basePath.split('/');
for (const record of records) {
visit(parts);
}// Before: spread introduces unstable references
const Panel = ({ onSubmit }) => <Form {...{ onSubmit: () => onSubmit() }} />;
// After: memoize the spread payload
const Panel = ({ onSubmit }) => {
const formProps = useMemo(() => ({ onSubmit: () => onSubmit() }), [onSubmit]);
return <Form {...formProps} />;
};// Before: inline object invalidates every consumer on render
return (
<UserContext.Provider value={{ name, role, refresh: () => refetch() }}>
<Profile />
</UserContext.Provider>
);
// After: memoize the value to keep Context stable
const providerValue = useMemo(() => ({ name, role, refresh: () => refetch() }), [name, role, refetch]);
return (
<UserContext.Provider value={providerValue}>
<Profile />
</UserContext.Provider>
);// Before: pulls entire lodash build
import { map } from 'lodash';
// After: import only what is needed
import map from 'lodash/map';The clip above (capture it following docs/examples/cross-file-warning/README.md) shows a single ESLint run catching two unstable props and an unhandled async flow. The demo highlights how the analyzer correlates memo wrappers and async helpers across files.
- Node.js: 18+
- ESLint: ^8.57.0 or ^9.x
- TypeScript: 5.5.x (development dependency aligned with
@typescript-eslint) - React guidance: React-specific diagnostics assume React 16.8+ hooks semantics
🧪 Typed RuleTester: our typed runner and CI simulate real-world React+TS projects with cross-file usage, so every rule ships with analyzer-backed coverage.
npm install
npm run lint
npm run test
npm run build
# Optional: profile rule performance before/after changes
npm run benchmarkEnsure the code compiles, tests pass, and linting remains clean before opening a pull request.
See docs/benchmarking.md for details about the benchmark harness and reference projects it exercises.
Read CONTRIBUTING.md for the quickstart workflow and expectations before opening a pull request.
- Head to GitHub Discussions to ask questions, propose ideas, or respond to the weekly audit summary. Start with the "Community check-in" template so maintainers know how to support you.
- Subscribe to announcements to be pinged when a new audit report drops or when we schedule community syncs.
- Browse issues labeled
good first issuefor bite-sized tasks that build familiarity with the codebase. - Prefer guidance in Portuguese? Filter by the
boa primeira contribuiçãolabel—each ticket outlines clear steps, acceptance criteria, and mentors willing to help.
- Open an issue describing the performance heuristic, proposed signal, and acceptable false positives.
- Implement the rule under
src/rules/, add coverage intests/rules/, and document behavior indocs/rules/<rule-name>.md. - Export the rule from
src/index.ts, update recommended configs if appropriate, and link the documentation. - Run the pipeline (
npm run lint,npm run test,npm run build). - Submit the pull request with a clear explanation of the signal, rationale, and known edge cases.
- Every Monday we publish a community audit using the weekly report template. The recap highlights new contributors, priority issues, and discussion outcomes.
- Missed an update? Check the Announcements category in Discussions for the latest summary and ongoing calls to action.
Need help crafting new rules? Reach out in English or Portuguese—the community is ready to help!
Perf Fiscal is released under the MIT License.
Adopt Perf Fiscal to keep your codebase lean, predictable, and production-ready.
💬 Want updates? ⭐️ Star and follow ruidosujeira/perf-linter to get notified when we ship new heuristics.
