renderspy
v0.1.0
Published
Catch unnecessary re-renders before they catch you
Maintainers
Readme
renderspy
Catch unnecessary re-renders before they catch you.
[!WARNING]
renderspyis intended for development and profiling only. Remove it from production builds or guard it withprocess.env.NODE_ENV !== 'production'.
Why renderspy?
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.
Install
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.
Quick Start
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.
Core API
<RenderSpy>
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
withRenderSpy(Component, options?)
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>
>;<RenderSpyDashboard>
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
getReport()
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>
</>
);
}clearReport()
Reset all collected metrics back to zero.
import { clearReport } from "renderspy";
clearReport();recordRender() (advanced)
Low-level function used internally by RenderSpy and withRenderSpy. Exported for custom integrations if you need to track renders outside of the standard wrappers.
All Exports
import {
RenderSpy,
withRenderSpy,
RenderSpyDashboard,
getReport,
clearReport,
recordRender,
} from "renderspy";Type-only imports:
import type {
ComponentStats,
RenderSpyProps,
RenderSpyDashboardProps,
} from "renderspy";How It Works
| What | How |
|------|-----|
| Render timing | React Profiler actualDuration |
| Unnecessary render detection | Prop-by-prop === comparison |
| Report sorting | By unnecessaryRenders descending |
CI/CD Example (Render Regression Gate)
You can fail CI when critical components start introducing unnecessary renders.
1) Add an integration-style test
// 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);
});2) Run it in GitHub Actions
# .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.
License
MIT © Meezan Shaikh
