Skip to content

[DevTools] Fix Profiler inaccuracy caused by stale PerformedWork flag on unvisited fibers#436

Closed
everettbu wants to merge 1 commit into
mainfrom
fix/devtools-didFiberRender-accuracy
Closed

[DevTools] Fix Profiler inaccuracy caused by stale PerformedWork flag on unvisited fibers#436
everettbu wants to merge 1 commit into
mainfrom
fix/devtools-didFiberRender-accuracy

Conversation

@everettbu

Copy link
Copy Markdown

Mirror of facebook/react#35645
Original author: yongsk0066


Summary

Fixes an issue where DevTools Profiler reports components as "rendered" and "Highlight updates" flashes on components that were actually skipped.

This can cause confusion for developers using React Compiler, as cached components appear to be re-rendering in DevTools—making it seem like optimizations aren't working when they actually are.

When React Compiler caches JSX elements, the parent may bail out entirely (childLanes === 0), and child fibers are never visited by the reconciler. However, didFiberRender was checking the PerformedWork flag which could be stale from a previous render, causing false positives in the Profiler.

Here's an example to reproduce the issue (also available in my demo repo). Note that ChildComponent is wrapped with a <div>.

// See: https://github.com/yongsk0066/devtools-rendered-demo/blob/main/src/WrappedCase.tsx
import { useState } from "react";

export default function WrappedCase() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  return (
    <section>
      <h2>{"Child wrapped with <div>"}</h2>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Type here..."
      />
      <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
      <div>
        <ChildComponent count={count} />
      </div>
    </section>
  );
}

function ChildComponent({ count }: { count: number }) {
  return <p>Child Component {count}</p>;
}

Compiled output (some parts omitted, full output available in playground or demo repo):

import { c as _c } from "react/compiler-runtime";
import { useState } from "react";

export default function WrappedCase() {
  const $ = _c(12);
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");
  // ...
  let t4;
  let t5;
  if ($[5] !== count) {
    t4 = <button onClick={t3}>Count: {count}</button>;
    t5 = (
      <div>
        <ChildComponent count={count} />
      </div>
    );
    $[5] = count;
    $[6] = t4;
    $[7] = t5;
  } else {
    t4 = $[6];
    t5 = $[7]; // same JSX reference returned
  }
  // When text changes, Parent re-renders but t5 stays the same.
  // React bails out on the cached subtree - ChildComponent fiber is never visited.
  // But Profiler incorrectly reports ChildComponent as "rendered".
  let t6;
  if ($[8] !== t2 || $[9] !== t4 || $[10] !== t5) {
    t6 = (
      <section>
        {t0}
        {t2}
        {t4}
        {t5}
      </section>
    );
    $[8] = t2;
    $[9] = t4;
    $[10] = t5;
    $[11] = t6;
  } else {
    t6 = $[11];
  }
  return t6;
}

I referenced a similar comparison pattern from renderer.js#L4936-L4940 (introduced in #30684), which uses fiber identity to check if a child was not visited. This fix adds the same check before checking the PerformedWork flag—if the fiber is the same object, it was never visited this render, so any flag is stale.

function didFiberRender(prevFiber, nextFiber) {
  if (prevFiber === nextFiber) {
    return false; // fiber was never visited
  }
  // ... existing flag check
}

Note: This is different from the "We don't reflect bailouts" case—that's for components that were visited but bailed out via shouldComponentUpdate or React.memo, while this fix handles fibers that were never visited because the parent bailed out entirely with childLanes === 0.

How did you test this change?

Added a test case using useMemoCache to simulate React Compiler's JSX caching behavior. The test verifies that cached children are not reported in fiberActualDurations.

I created a reproduction demo at https://github.com/yongsk0066/devtools-rendered-demo (live demo). Tested with React Developer Tools 7.0.1 (10/20/2025). To reproduce the issue, enable "Highlight updates when components render" and check if highlights appear on cached children, or check in Profiler if cached components are shown without hatching.

yarn test-build-devtools --testPathPattern="profilerStore-test" --testNamePattern="cached"

I also built the DevTools extension locally with the fix

Before fix:
profiling json

wrapped.mp4

After fix:
profiling json

fixed_wrapped.mp4

cc @hoxyq

@greptile-apps

greptile-apps Bot commented Jan 27, 2026

Copy link
Copy Markdown

Greptile Overview

Greptile Summary

This PR fixes a DevTools Profiler bug where components were incorrectly reported as "rendered" when they were actually skipped due to JSX element caching (e.g., from React Compiler).

The Issue:
When React Compiler caches JSX elements, the parent may bail out entirely (childLanes === 0), meaning child fibers are never visited by the reconciler. However, didFiberRender was checking the PerformedWork flag on the fiber object, which could be stale from a previous render - causing false positives in the Profiler.

The Fix:
Added an identity check (prevFiber === nextFiber) at the start of didFiberRender. When the fiber object is the same reference, it means the fiber was never visited during this render cycle, so any flags are stale and should be ignored.

Key Changes:

  • renderer.js:2091-2093 - Added early return when prevFiber === nextFiber to handle unvisited fibers
  • profilerStore-test.js:239-318 - Added comprehensive test case simulating React Compiler's useMemoCache behavior to verify cached children aren't reported as rendered

This follows the same pattern used elsewhere in the codebase for detecting unvisited fibers (ref: renderer.js:4942) and is distinct from the "bailout" case where fibers are visited but skip via shouldComponentUpdate or React.memo.

Confidence Score: 5/5

  • This PR is safe to merge - it's a focused bug fix with comprehensive test coverage
  • The fix is minimal (6 lines), well-documented, follows existing patterns in the codebase, has comprehensive test coverage, and addresses a specific, well-understood bug without affecting other functionality
  • No files require special attention

Important Files Changed

Filename Overview
packages/react-devtools-shared/src/backend/fiber/renderer.js Adds fiber identity check in didFiberRender to detect unvisited fibers with stale flags
packages/react-devtools-shared/src/tests/profilerStore-test.js Adds test case verifying cached JSX children aren't incorrectly reported as rendered

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@everettbu

Copy link
Copy Markdown
Author

Upstream PR was closed or merged. Code is synced via branch mirror.

@everettbu everettbu closed this Feb 12, 2026
@everettbu
everettbu deleted the fix/devtools-didFiberRender-accuracy branch February 12, 2026 03:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants