Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 55 additions & 5 deletions cli/src/native/react/scripts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ pub const RENDERS_INIT: &str = r#"
if (!hook || window.__AB_RENDERS_ACTIVE__) return;

const MAX_COMPONENTS = 200;
// React fiber flag set when the fiber ran its render function in this
// commit. React DevTools reads the same flag to decide whether a fiber
// rendered.
const PERFORMED_WORK = 1;
const data = {};
const fps = { frames: [], last: 0, rafId: 0 };

Expand Down Expand Up @@ -340,12 +344,21 @@ pub const RENDERS_INIT: &str = r#"
function walkFiber(fiber) {
if (!fiber) return;
const tag = fiber.tag;
const alt = fiber.alternate;
if (tag === 0 || tag === 1 || tag === 2 || tag === 11 || tag === 15) {
// A fiber counts as rendered only when it actually ran in this
// commit. React DevTools uses the same rule (didFiberRender): for
// components that run user code, the PerformedWork flag tells a real
// render from a bailout. The old check (any flag set, or props/state
// differing from a stale alternate) re-counted fibers React bailed
// out on: flags from an earlier render stay on them, and a fiber
// that never updated since mount has no alternate at all, so it was
// counted as a fresh mount on every commit.
const didRender =
fiber.alternate === null ||
fiber.flags > 0 ||
fiber.memoizedProps !== (fiber.alternate && fiber.alternate.memoizedProps) ||
fiber.memoizedState !== (fiber.alternate && fiber.alternate.memoizedState);
alt === null ||
(tag === 2
? fiber.child !== alt.child || (fiber.flags & PERFORMED_WORK) !== 0
: (fiber.flags & PERFORMED_WORK) !== 0);
Comment thread
vercel[bot] marked this conversation as resolved.
Resolved
if (didRender) {
const name = getName(fiber);
if (name) {
Expand Down Expand Up @@ -377,7 +390,17 @@ pub const RENDERS_INIT: &str = r#"
}
}
}
walkFiber(fiber.child);
// Descend only into subtrees that can contain a newly rendered fiber.
// When a fiber bailed out, React leaves its child pointer untouched,
// so an unchanged child pointer means nothing below rendered; walking
// in anyway would re-count fibers whose flags are stale from an
// earlier render (or that never got an alternate) on every commit.
// This mirrors React DevTools, which only descends when
// nextFiber.child !== prevFiber.child. On mount there is no alternate
// yet, so the whole tree is walked.
if (!alt || fiber.child !== alt.child) {
walkFiber(fiber.child);
}
walkFiber(fiber.sibling);
}
})()
Expand Down Expand Up @@ -839,4 +862,31 @@ mod tests {
assert!(PICK_REACT_RENDERER.contains("firstWithRoots"));
assert!(PICK_REACT_RENDERER.contains("firstAny"));
}

/// The renders profiler must count only fibers that actually ran in the
/// commit, like React DevTools (didFiberRender): the PerformedWork flag
/// distinguishes a real render from a bailout, and subtrees are
/// descended into only when their child pointer changed. Counting on
/// any flag being set, or on props/state differing from a stale
/// alternate, re-counts bailed-out fibers on every commit (stale flags
/// survive, and fibers never updated since mount have no alternate).
#[test]
fn renders_profiler_counts_only_fibers_that_rendered() {
assert!(
RENDERS_INIT.contains("const PERFORMED_WORK = 1;"),
"RENDERS_INIT must define the PerformedWork flag value"
);
assert!(
RENDERS_INIT.contains("(fiber.flags & PERFORMED_WORK) !== 0"),
"RENDERS_INIT must gate counting on the PerformedWork flag"
);
assert!(
RENDERS_INIT.contains("fiber.child !== alt.child"),
"RENDERS_INIT must descend only when the child pointer changed"
);
assert!(
!RENDERS_INIT.contains("fiber.flags > 0"),
"RENDERS_INIT must not count a fiber for any flag being set"
);
}
}