Catch unnecessary re-renders before they catch you.
Warning
renderspy is intended for development and profiling only.
Remove it from production builds or guard it with process.env.NODE_ENV !== 'production'.
React DevTools is powerful, but it requires opening a browser extension and manually hunting for hot components. renderspy is different — it lives inside your app, logs programmatically, and surfaces unnecessary re-renders with zero configuration.
- No browser extension needed
- Works in any environment (local, staging, CI)
- Log render reports programmatically via
getReport() - Ships with TypeScript types. Supports ESM and CJS.
npm install renderspy
# or
yarn add renderspy
# or
pnpm add renderspyPeer requirements:
| Peer | Version |
|---|---|
react |
>=17.0.0 |
react-dom |
>=17.0.0 |
React 17+ is required because
renderspyuses the React Profiler API introduced in that release.
import { RenderSpy, RenderSpyDashboard } from "renderspy";
function App() {
return (
<>
<RenderSpy threshold={16}>
<YourApp />
</RenderSpy>
{/* Floating dashboard — remove before shipping to production */}
<RenderSpyDashboard />
</>
);
}Open your app. The floating panel appears in the corner showing live render counts, unnecessary renders, and timing per component.
Wrap any part of your React tree to start collecting render metrics.
type RenderSpyProps = {
children: React.ReactNode;
threshold?: number; // ms before a render is flagged as slow (default: 16)
onSlowRender?: (componentName: string, renderTime: number) => void;
enabled?: boolean; // default: true
};Behavior:
- Records both
mountandupdaterenders - Flags a render as "unnecessary" when every prop passes referential equality (
===) - Calls
onSlowRenderforupdaterenders that exceedthreshold
HOC form for tracking a single component without restructuring your tree.
import { withRenderSpy } from "renderspy";
const TrackedUserCard = withRenderSpy(UserCard, { threshold: 8 });
const TrackedCounter = withRenderSpy(Counter);Includes:
- Ref forwarding
- Preserves original
displayName - Static property hoisting via
hoist-non-react-statics
Type signature:
function withRenderSpy<T extends object>(
WrappedComponent: React.ComponentType<T>,
options?: { threshold?: number }
): React.ForwardRefExoticComponent<
React.PropsWithoutRef<T> & React.RefAttributes<unknown>
>;A floating, draggable overlay that shows live render stats.
type RenderSpyDashboardProps = {
defaultOpen?: boolean; // default: true
defaultMinimized?: boolean; // default: false
};Features:
- Fixed, draggable panel — won't interfere with your layout
- Auto-refreshes every 1000ms
- Color-coded rows highlight components with wasted renders
ClearandRefreshcontrols
Read all collected stats at any time. Useful for logging, assertions, or CI checks.
import { getReport } from "renderspy";
const report = getReport();
// Returns ComponentStats[]type ComponentStats = {
componentName: string;
renderCount: number;
unnecessaryRenders: number;
totalTime: number; // ms
averageTime: number; // ms
lastRenderTime: number; // ms
};Example — log report on demand:
import { RenderSpy, getReport } from "renderspy";
function Demo() {
return (
<>
<RenderSpy>
<App />
</RenderSpy>
<button onClick={() => console.table(getReport())}>
Log render report
</button>
</>
);
}Reset all collected metrics back to zero.
import { clearReport } from "renderspy";
clearReport();Low-level function used internally by RenderSpy and withRenderSpy. Exported for custom integrations if you need to track renders outside of the standard wrappers.
import {
RenderSpy,
withRenderSpy,
RenderSpyDashboard,
getReport,
clearReport,
recordRender,
} from "renderspy";Type-only imports:
import type {
ComponentStats,
RenderSpyProps,
RenderSpyDashboardProps,
} from "renderspy";| What | How |
|---|---|
| Render timing | React Profiler actualDuration |
| Unnecessary render detection | Prop-by-prop === comparison |
| Report sorting | By unnecessaryRenders descending |
You can fail CI when critical components start introducing unnecessary renders.
// dashboard-render.test.tsx
import React from "react";
import { render } from "@testing-library/react";
import { RenderSpy, clearReport, getReport } from "renderspy";
type HeaderProps = { title: string };
const Header: React.FC<HeaderProps> = ({ title }) => <h1>{title}</h1>;
test("Header should keep unnecessary renders low", () => {
clearReport();
const { rerender } = render(
<RenderSpy>
<Header title="renderspy" />
</RenderSpy>
);
// Same props on rerender should be tracked as unnecessary.
rerender(
<RenderSpy>
<Header title="renderspy" />
</RenderSpy>
);
const report = getReport();
const header = report.find((item) => item.componentName === "Header");
expect(header).toBeDefined();
expect(header?.unnecessaryRenders).toBeLessThanOrEqual(1);
});# .github/workflows/ci.yml
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build
- run: npm test -- --runInBandTip: start with high-value components (layout, nav, large lists), then tighten limits over time.
MIT © Meezan Shaikh