包详细信息

react-jitter

sidx102464.6kMIT0.4.0

A developer tool for debugging React performance issues caused by hook changes and component re-renders.

react, react-hooks, performance, re-render

自述文件

React Jitter

npm version License: MIT

React Jitter is a developer tool to help you understand why your React components re-render. It pinpoints the exact hooks and values that have changed, so you can optimize your application's performance with precision.

Table of Contents

Motivation

Unnecessary re-renders are a common source of performance issues in React applications. When a component re-renders, it can be difficult to trace the root cause. React Profiler reports "Hook 7 changed" but not the name of hook or what exactly changed. It also does not provide anything useful for context changes and it just says "Context changed".

Existing tools like why-did-you-render require you to manually whitelist the hooks and components you want to track. This is impractical in large codebases where you want to monitor everything without tedious configuration. Others, like react-scan, are excellent for a high-level overview but does not report hook changes.

React Jitter solves these problems by instrumenting your code at build time with an SWC plugin. It tells you the exact hook that caused a re-render and shows you the previous and current values so you can see exactly what changed.

Getting Started

Installation

npm install --save-dev react-jitter

Usage

React Jitter is designed to be used during development only. You must ensure that both the SWC plugin and the runtime are disabled in your production builds to prevent instrumenting your production code.

Enabling the SWC Plugin (Development Only)

Next.js

In next.config.js, conditionally add the plugin and its configuration based on the environment:

const isDevelopment = process.env.NODE_ENV === "development";

const nextConfig = {
  swcMinify: true,
  experimental: {
    swcPlugins: isDevelopment
      ? [
          [
            "react-jitter/plugin-swc",
            {
              // An array of hooks to ignore. For example, `["useSelector"]`.
              ignoreHooks: [],
              // An array of glob patterns for files to exclude from instrumentation.
              // By default, `**/node_modules/**` is excluded.
              exclude: [],
              // When true, the arguments of a hook will be captured as strings
              // and included in the `onHookChange` callback. This is disabled
              // by default because it can significantly increase the bundle size,
              // especially if hooks receive large objects or functions as arguments.
              includeArguments: false,
            },
          ],
        ]
      : [],
  },
};

module.exports = nextConfig;
.swcrc

Since .swcrc is a static JSON file, you cannot use logic within it. You should use separate build scripts or a JavaScript-based configuration file (e.g., swc.config.js) to apply different SWC configurations for your development and production environments.

Initializing the Runtime (Development Only)

Once the plugin is configured, you need to initialize the runtime in your application. In your main application file (e.g., _app.tsx in Next.js), ensure the call is only made in development:

// In your app's entry file (e.g., _app.tsx)

import { reactJitter } from "react-jitter/runtime";

if (process.env.NODE_ENV === "development") {
  reactJitter({
    enabled: true,
    // This function is called whenever a hook's value changes.
    onHookChange: (change) => {
      console.log("Hook changed:", change);
    },
    // This function is called after a component finishes rendering.
    onRender: (render) => {
      console.log("Component rendered:", render);
    }
  });
}

You can also change enabled, onHookChange, and onRender at runtime from your browser's developer console. This is useful for temporarily disabling logging or changing the callback behavior without a full page reload.

// Disable React Jitter
window.reactJitter.enabled = false;

// Re-enable React Jitter
window.reactJitter.enabled = true;

// Change the onHookChange callback
window.reactJitter.onHookChange = (change) => {
  if (change.unstable) {
    console.warn("Unstable hook value:", change);
  }
};

// Change the onRender callback
window.reactJitter.onRender = (render) => {
  if (render.renderCount > 10) {
    console.warn("High render count:", render);
  }
}

Modern bundlers will tree-shake the import and the function call from your production build, so it will have zero performance impact.

Advanced: Custom Comparator Selection

By default, React Jitter uses the deepEqual comparator to detect changes in hook values. However, you can customize which comparator is used on a per-hook basis using the selectComparator function. This is useful when dealing with circular data structures or when you need different comparison strategies for different hooks.

// Set a custom comparator selector
window.reactJitter.selectComparator = (hookAddress) => {
  // Use circularDeepEqual for hooks that might return circular structures
  if (hookAddress.hook === 'useSelector' || hookAddress.hook === 'useReduxState') {
    return 'circularDeepEqual';
  }

  // Use deepEqual for everything else (default)
  return 'deepEqual';
};

The hookAddress parameter contains information about the hook:

{
  hook: string;        // Hook name, e.g., "useState", "useContext"
  file: string;        // File path where the hook is called
  line: number;        // Line number
  offset: number;      // Column offset
  arguments?: string[]; // Hook arguments (if includeArguments is enabled)
}

Available Comparators:

  • deepEqual (default): Fast deep equality check that handles most cases. Will throw an error if it encounters deeply nested or circular structures.
  • circularDeepEqual: Slower but handles circular references safely. Use this when your hooks return data with circular dependencies or extremely deep nesting.

When to Use circularDeepEqual:

If you see an error like "Maximum call stack size exceeded. Please use the 'circularDeepEqual' comparator", you should configure selectComparator to return 'circularDeepEqual' for the specific hook mentioned in the error message.

API and Configuration

The reactJitter function accepts a configuration object with two callbacks: onHookChange and onRender.

  • onHookChange: Called whenever a hook's value changes. It receives a change object with details about the hook, its location, and the value that changed.
  • onRender: Called after a component finishes rendering. It receives a render object with metadata about the component's render cycle.

Here is an example of the change object from onHookChange when an unstable object is detected:

{
  "hook": "useUser",
  "file": "/src/components/UserProfile.tsx",
  "line": 12,
  "offset": 18,
  "scope": {
    "name": "UserProfile",
    "file": "/src/components/UserProfile.tsx",
    "line": 8,
    "offset": 1
  },
  "unstable": true,
  "unstableKeys": ["address"],
  "changedKeys": [],
  "previousResult": {
    "id": "user-123",
    "address": { "street": "123 Main St" }
  },
  "currentResult": {
    "id": "user-123",
    "address": { "street": "123 Main St" }
  }
}

In this case, the address object was re-created, causing an unstable reference even though its contents are the same.

Here is an example of the render object from onRender:

{
  "scopeId": "UserProfile-0",
  "renderCount": 5,
  "name": "UserProfile",
  "id": "UserProfile",
  "file": "/src/components/UserProfile.tsx",
  "line": 8,
  "offset": 1,
  "hookResults": {
    "4f23ef0": {
      "id": "user-123",
      "address": { "street": "123 Main St" }
    }
  }
}

This object provides the component's unique instance ID, its render count, location metadata, and a map of all hook results for the current render.

You can use the includeArguments option to identify which context has changed. When includeArguments is set to true in the SWC plugin configuration, the onHookChange callback will include the arguments passed to the hook. This is especially useful for useContext, as it allows you to see which context was used.

Here is an example of the change object when includeArguments is enabled:

{
  "hook": "useContext",
  "arguments": ["UserContext"],
  "file": "/src/components/UserProfile.tsx",
  "line": 12,
  "offset": 18,
  "scope": {
    "name": "UserProfile",
    "file": "/src/components/UserProfile.tsx",
    "line": 8,
    "offset": 1
  },
  "unstable": false,
  "unstableKeys": [],
  "changedKeys": ["user"],
  "previousResult": {
    "user": { "name": "John" }
  },
  "currentResult": {
    "user": { "name": "Jane" }
  }
}

In this example, the arguments field shows that the UserContext was used, and the changedKeys field shows that the user property has changed.

How It Works

React Jitter is composed of two parts:

  1. SWC Plugin: A plugin for the SWC compiler that transforms your code to add instrumentation. It finds your React components and wraps your hook calls with a light monitoring function.
  2. Runtime: A small runtime library that is injected into your components. It keeps track of the values returned by your hooks and, when they change, it reports the differences.

One of the most common causes of unnecessary re-renders is "unstable" objects and functions that are re-created on every render. react-jitter helps you identify these issues by tracking when a value's reference changes and reporting it to you.

Limitations

react-jitter detects React components using a set of heuristics. It looks for functions that either use hooks or return JSX, and whose names start with a capital letter. While this covers most common cases, it may not detect all components reliably, especially if you use less common patterns for defining components.

Contributing

Contributions are welcome! Please open an issue or submit a pull request if you have any ideas for improvement.

License

This project is licensed under the MIT License.