Skip to content

perf: optimize ScatterChart hover by reducing re-renders from O(n) to O(1)#7133

Merged
PavelVanecek merged 3 commits into
recharts:mainfrom
roy7:test/scatterplot-performance
Jun 24, 2026
Merged

perf: optimize ScatterChart hover by reducing re-renders from O(n) to O(1)#7133
PavelVanecek merged 3 commits into
recharts:mainfrom
roy7:test/scatterplot-performance

Conversation

@roy7

@roy7 roy7 commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Extract ScatterPoint component so each point subscribes to its own isActive selector, instead of the parent ScatterSymbols subscribing to selectActiveTooltipIndex and re-rendering all N points on every hover change
  • On mouse move, only the 1-2 affected points re-render (the point becoming active and the point becoming inactive), instead of all N
  • With 5,000 points, this reduces re-renders per hover from ~5,000 to ~2

This significantly improves hover responsiveness but does not eliminate all lag at very high point counts (10K+), as the remaining cost comes from SVG hit-testing across thousands of DOM elements and per-point hook overhead (ZIndexLayer, useLayoutEffect). Further optimizations to initial render time and component tree depth are possible follow-up work.

Context

This addresses #2862 (Scatter chart performance issues, P1). The root cause is that ScatterSymbols subscribed to selectActiveTooltipIndex at the parent level, causing all scatter points to re-render on every tooltip index change. Each re-render created per-point event handler closures, object spreads, and adaptEventsOfChild wrappers — ~20,000 allocations per mouse move with 5,000 points.

The fix moves the isActive check into each ScatterPoint component via a memoized per-point selector. The parent no longer subscribes to the tooltip index, so it doesn't re-render on hover. Each point's selector is trivial (hasActiveShape && selectActiveTooltipIndex(state) === String(index)) and only triggers a re-render when that specific point's active state changes.

Unlike external memoization workarounds that filter out event handler props (as discussed in #2862), this fix addresses the root cause internally.

Note: direct hover within a dense scatter chart (where points overlap) was verified to work correctly — moving between overlapping points updates the tooltip without requiring empty space in between.

Follow-up optimization opportunities

We identified several additional bottlenecks that affect initial render time (1-1.5s for points to appear, 3-5s before tooltips are interactive at 10K points). These are independent of the hover optimization in this PR:

  • ZIndexLayer overhead: Every scatter point is wrapped in a ZIndexLayer that calls useLayoutEffect + useAppSelector even when inactive (zIndex=undefined takes the early-return path). With 10K points, that's ~50,000 hook instances on mount for no benefit. Lazy portal registration (only when a point becomes active) could eliminate this.
  • Animation during mount: Default 400ms animation interpolates all point positions frame-by-frame, calling computeScatterPoints ~30 times. Large datasets could benefit from skipping animation or deferring tooltip registration until animation completes.
  • Event delegation: Replacing per-point onMouseEnter/onMouseLeave/onClick handlers with a single delegated handler on the parent <g> would eliminate thousands of closure allocations on initial render. However, this approach conflicts with ZIndexLayer portals (portaled elements leave the delegating parent's DOM subtree), so it would require rethinking the portal strategy first.

Test plan

  • Existing scatter tests pass (17 tests in Scatter.spec.tsx)
  • New render-count test verifies that hovering triggers fewer re-renders than the total point count
  • Manual testing with 5,000+ point scatter chart confirms smooth hover interaction
  • activeShape prop works correctly (active point renders with custom shape and z-index portal)
  • User-provided event handlers (onMouseEnter, onMouseLeave, onClick, onMouseOver, onMouseOut) still fire with correct (data, index, event) arguments

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Per-point scatter elements with independent active-state handling and per-point z-index layering.
    • Added a public export for scatter shape props.
  • Performance

    • Selective re-rendering so only the active (and previously active) points update on hover, reducing unnecessary updates.
  • Tests

    • Added tests validating per-point render optimization and hover-driven limited re-renders.
  • Documentation

    • Added component documentation for the per-point scatter element.

@coderabbitai

coderabbitai Bot commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a per-point ScatterPoint component and memoized per-point isActive selector; moves per-point rendering, event binding, and z-index handling into ScatterPoint so only affected points re-render; exports ScatterShapeProps and adds tests for per-point render optimization.

Changes

Cohort / File(s) Summary
Scatter rendering optimization
src/cartesian/Scatter.tsx
Introduce ScatterPoint component; replace inline per-point rendering in ScatterSymbols with ScatterPoint; add memoized per-point isActive selector using RechartsRootState; move per-point z-index handling into ZIndexLayer; route event binding through per-point component; add documentation block for ScatterPoint.
Test coverage & public API
test/cartesian/Scatter.spec.tsx, src/index.ts
Add test suite verifying per-point render optimization and selective re-renders on hover; export ScatterShapeProps from the public entry to support tests and typings.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User as User
    participant Symbols as ScatterSymbols
    participant Point as ScatterPoint
    participant State as RechartsRootState
    participant ZLayer as ZIndexLayer
    participant DOM as DOM

    User->>Symbols: mount / initial render
    Symbols->>Point: map data -> render per-point components
    Point->>State: read per-point activeIndex selector
    State-->>Point: isActive (false for most)
    Point->>ZLayer: wrap point with z-index layer
    ZLayer->>DOM: render point element

    User->>Point: hover point (mouseEnter)
    Point->>State: dispatch activeIndex update
    State-->>Point: memoized selector -> hovered point true, previous false
    Point->>ZLayer: apply active zIndex for active point
    ZLayer->>DOM: update only affected points' DOM
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • ckifer
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'perf: optimize ScatterChart hover by reducing re-renders from O(n) to O(1)' clearly and specifically describes the main performance optimization: reducing re-renders from linear to constant time complexity during hover interactions.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering summary, context, motivation, testing approach, and follow-up opportunities. It aligns with the template's main sections and provides detailed technical rationale.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/cartesian/Scatter.tsx (1)

605-619: Consider simplifying the key construction.

The key uses optional chaining (entry?.cx) but entry is always defined when iterating via points.map. The optional chaining is unnecessary and could be simplified.

More importantly, if cx, cy, or size are undefined, the key becomes symbol-undefined-undefined-undefined-0, which could cause React key collisions if multiple points have undefined coordinates. Since the index i is included, uniqueness is guaranteed, but consider using just the index or a more stable identifier.

Suggested simplification
       {points.map((entry: ScatterPointItem, i: number) => (
         <ScatterPoint
-          key={`symbol-${entry?.cx}-${entry?.cy}-${entry?.size}-${i}`}
+          key={`symbol-${i}`}
           entry={entry}
           index={i}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cartesian/Scatter.tsx` around lines 605 - 619, The key for ScatterPoint
created inside points.map in Scatter.tsx unnecessarily uses optional chaining
(entry?.cx) and can produce "undefined" parts leading to less readable keys;
replace it with a stable, simple key—either use the loop index (e.g.,
key={`symbol-${i}`}) or, if each point has a stable identifier (e.g., entry.id
or entry.key), use that (e.g., key={entry.id})—update the key prop on the
ScatterPoint component instantiation and remove the optional chaining to ensure
concise, collision-resistant keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/cartesian/Scatter.spec.tsx`:
- Around line 701-746: Add fake-timer handling and tighten the assertions in the
"ScatterPoint per-point rendering optimization" test: call vi.useFakeTimers() at
the start of the it block and vi.runOnlyPendingTimers() immediately after each
fireEvent.mouseEnter(symbols[X]) (and after initial render if needed) so Redux
autoBatch updates and rAF finish before asserting shapeSpy.mock.calls; replace
the loose expect(...).toBeLessThan(pointCount) checks with stricter expectations
(e.g., expect(afterFirstHover).toBeLessThanOrEqual(1) and
expect(afterSecondHover).toBeLessThanOrEqual(2)); finally restore timers with
vi.useRealTimers() at the end of the test.

---

Nitpick comments:
In `@src/cartesian/Scatter.tsx`:
- Around line 605-619: The key for ScatterPoint created inside points.map in
Scatter.tsx unnecessarily uses optional chaining (entry?.cx) and can produce
"undefined" parts leading to less readable keys; replace it with a stable,
simple key—either use the loop index (e.g., key={`symbol-${i}`}) or, if each
point has a stable identifier (e.g., entry.id or entry.key), use that (e.g.,
key={entry.id})—update the key prop on the ScatterPoint component instantiation
and remove the optional chaining to ensure concise, collision-resistant keys.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 29a5a697-783d-457e-8bf3-bbc7eb41aca6

📥 Commits

Reviewing files that changed from the base of the PR and between f4e1b9c and 79399c9.

📒 Files selected for processing (2)
  • src/cartesian/Scatter.tsx
  • test/cartesian/Scatter.spec.tsx

Comment thread test/cartesian/Scatter.spec.tsx
@roy7

roy7 commented Mar 13, 2026

Copy link
Copy Markdown
Contributor Author

Regarding the key simplification suggestion (using key={symbol-${i}} instead of key={symbol-${entry?.cx}-${entry?.cy}-${entry?.size}-${i}}): this key format was carried over from the original inline code in ScatterSymbols to preserve identical React reconciliation behavior. Changing it would alter how React matches elements between renders, which is outside the scope of this performance fix.

(Written by Claude Code.)

@codecov

codecov Bot commented Mar 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.27%. Comparing base (6ae9d2d) to head (62dabd6).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #7133   +/-   ##
=======================================
  Coverage   88.27%   88.27%           
=======================================
  Files         607      607           
  Lines       14124    14127    +3     
  Branches     3550     3551    +1     
=======================================
+ Hits        12468    12471    +3     
  Misses       1469     1469           
  Partials      187      187           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ckifer

ckifer commented Mar 13, 2026

Copy link
Copy Markdown
Member

@roy7 can you rebase to get a green build. should be fixed now

@roy7
roy7 force-pushed the test/scatterplot-performance branch from beef355 to 2f7f036 Compare March 13, 2026 23:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/cartesian/Scatter.tsx (1)

555-559: Consider extracting selector factory for clarity and potential reuse.

The inline selector factory pattern works correctly, but could be extracted for clarity:

💡 Optional refactor to extract selector factory
+const createIsActiveSelector = (index: number, hasActiveShape: boolean) => (state: RechartsRootState): boolean =>
+  hasActiveShape && selectActiveTooltipIndex(state) === String(index);
+
 function ScatterPoint({
   // ... props
 }): React.ReactElement {
   const hasActiveShape = activeShape != null && activeShape !== false;
-  const selectIsActive = useMemo(() => {
-    const strIndex = String(index);
-    return (state: RechartsRootState): boolean => hasActiveShape && selectActiveTooltipIndex(state) === strIndex;
-  }, [hasActiveShape, index]);
+  const selectIsActive = useMemo(
+    () => createIsActiveSelector(index, hasActiveShape),
+    [hasActiveShape, index]
+  );
   const isActive: boolean = useAppSelector(selectIsActive) ?? false;

This could make the selector reusable if other components need similar per-index active state logic. However, the current inline approach is also fine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cartesian/Scatter.tsx` around lines 555 - 559, Extract the inline
selector factory into a named function (e.g., makeSelectIsActive) instead of
creating it inside the useMemo; the function should accept index and
hasActiveShape and return a selector that compares
selectActiveTooltipIndex(state) to String(index) and checks hasActiveShape.
Replace the current useMemo/selectIsActive creation with a call to
makeSelectIsActive(index, hasActiveShape) and keep the
useAppSelector(selectIsActive) usage to obtain isActive; this makes selector
logic reusable and clearer while preserving existing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/cartesian/Scatter.tsx`:
- Around line 555-559: Extract the inline selector factory into a named function
(e.g., makeSelectIsActive) instead of creating it inside the useMemo; the
function should accept index and hasActiveShape and return a selector that
compares selectActiveTooltipIndex(state) to String(index) and checks
hasActiveShape. Replace the current useMemo/selectIsActive creation with a call
to makeSelectIsActive(index, hasActiveShape) and keep the
useAppSelector(selectIsActive) usage to obtain isActive; this makes selector
logic reusable and clearer while preserving existing behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 338ce19a-f126-427e-ae5b-5d3585ff1a29

📥 Commits

Reviewing files that changed from the base of the PR and between beef355 and 2f7f036.

📒 Files selected for processing (2)
  • src/cartesian/Scatter.tsx
  • test/cartesian/Scatter.spec.tsx

@roy7

roy7 commented Mar 14, 2026

Copy link
Copy Markdown
Contributor Author

@roy7 can you rebase to get a green build. should be fixed now

I did but seems like another internal error of some sort.

@roy7
roy7 force-pushed the test/scatterplot-performance branch from 2f7f036 to b1a1921 Compare March 14, 2026 11:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
test/cartesian/Scatter.spec.tsx (1)

703-752: Harden timer cleanup with try/finally.

If an assertion fails before Line 751, fake timers can leak into subsequent tests in this file. Wrapping the body guarantees cleanup.

Proposed hardening
 it('should only re-render the active and previously-active points when hovering, not all points', () => {
   vi.useFakeTimers();
-  const shapeSpy = vi.fn((_props: ScatterShapeProps) => <circle r={5} cx={0} cy={0} />);
-  const pointCount = 20;
-  const scatterData = Array.from({ length: pointCount }, (_, i) => ({
-    x: i * 50,
-    y: i * 50,
-    z: 100,
-  }));
+  try {
+    const shapeSpy = vi.fn((_props: ScatterShapeProps) => <circle r={5} cx={0} cy={0} />);
+    const pointCount = 20;
+    const scatterData = Array.from({ length: pointCount }, (_, i) => ({
+      x: i * 50,
+      y: i * 50,
+      z: 100,
+    }));

-  const { container } = render(
-    <ScatterChart width={1000} height={1000}>
-      <XAxis type="number" dataKey="x" />
-      <YAxis type="number" dataKey="y" />
-      <Scatter isAnimationActive={false} data={scatterData} shape={shapeSpy} activeShape={{ fill: 'red' }} />
-    </ScatterChart>,
-  );
-  vi.runOnlyPendingTimers();
+    const { container } = render(
+      <ScatterChart width={1000} height={1000}>
+        <XAxis type="number" dataKey="x" />
+        <YAxis type="number" dataKey="y" />
+        <Scatter isAnimationActive={false} data={scatterData} shape={shapeSpy} activeShape={{ fill: 'red' }} />
+      </ScatterChart>,
+    );
+    vi.runOnlyPendingTimers();

-  // ...existing assertions...
+    // ...existing assertions...
+  } finally {
+    vi.useRealTimers();
+  }
-
-  vi.useRealTimers();
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/cartesian/Scatter.spec.tsx` around lines 703 - 752, Wrap the test body
that calls vi.useFakeTimers() and vi.useRealTimers() in a try/finally so
vi.useRealTimers() always runs; specifically, in the "Scatter" test that sets up
vi.useFakeTimers(), move the render(...) and all assertions into a try block and
call vi.useRealTimers() in the finally block to guarantee timer cleanup even if
an assertion fails (references: vi.useFakeTimers, vi.useRealTimers, render,
shapeSpy).
src/cartesian/Scatter.tsx (1)

555-559: Scope isActive to the active graphical item, not index alone.

On Line 557, isActive is derived only from tooltip index. In multi-<Scatter /> charts, matching indexes across series can become active together. Please verify this behavior with two scatter series using activeShape, and gate by active graphical item id if single-series activation is intended.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cartesian/Scatter.tsx` around lines 555 - 559, The selector
selectIsActive only checks selectActiveTooltipIndex against String(index), so
multiple series with the same point index can all appear active; update
selectIsActive (used by useAppSelector) to also compare the active graphical
item id (e.g., selectActiveGraphicItemId or similar state selector) against this
series' unique id/prop (use props.seriesId or the component's unique key) and
include hasActiveShape in the predicate; specifically, modify the lambda in
selectIsActive to return true only when hasActiveShape &&
selectActiveTooltipIndex(state) === String(index) &&
selectActiveGraphicItemId(state) === String(seriesId) (or the appropriate id
field) so isActive is scoped to the actual graphical item, not index alone.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/cartesian/Scatter.tsx`:
- Around line 555-559: The selector selectIsActive only checks
selectActiveTooltipIndex against String(index), so multiple series with the same
point index can all appear active; update selectIsActive (used by
useAppSelector) to also compare the active graphical item id (e.g.,
selectActiveGraphicItemId or similar state selector) against this series' unique
id/prop (use props.seriesId or the component's unique key) and include
hasActiveShape in the predicate; specifically, modify the lambda in
selectIsActive to return true only when hasActiveShape &&
selectActiveTooltipIndex(state) === String(index) &&
selectActiveGraphicItemId(state) === String(seriesId) (or the appropriate id
field) so isActive is scoped to the actual graphical item, not index alone.

In `@test/cartesian/Scatter.spec.tsx`:
- Around line 703-752: Wrap the test body that calls vi.useFakeTimers() and
vi.useRealTimers() in a try/finally so vi.useRealTimers() always runs;
specifically, in the "Scatter" test that sets up vi.useFakeTimers(), move the
render(...) and all assertions into a try block and call vi.useRealTimers() in
the finally block to guarantee timer cleanup even if an assertion fails
(references: vi.useFakeTimers, vi.useRealTimers, render, shapeSpy).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 92c565d0-4826-4b7e-b691-72f1a4573e7a

📥 Commits

Reviewing files that changed from the base of the PR and between 2f7f036 and b1a1921.

📒 Files selected for processing (2)
  • src/cartesian/Scatter.tsx
  • test/cartesian/Scatter.spec.tsx

@roy7
roy7 force-pushed the test/scatterplot-performance branch from c8d9afc to e6a5215 Compare March 15, 2026 16:07
@codecov

codecov Bot commented Mar 15, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 7.83kB (0.14%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
recharts/bundle-cjs 1.45MB 2.44kB (0.17%) ⬆️
recharts/bundle-es6 1.27MB 2.42kB (0.19%) ⬆️
recharts/bundle-umd 596.17kB 547 bytes (0.09%) ⬆️
recharts/bundle-treeshaking-cartesian 720.49kB 2.42kB (0.34%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: recharts/bundle-treeshaking-cartesian

Assets Changed:

Asset Name Size Change Total Size Change (%)
bundle.js 2.42kB 720.49kB 0.34%
view changes for bundle: recharts/bundle-umd

Assets Changed:

Asset Name Size Change Total Size Change (%)
Recharts.js 547 bytes 596.17kB 0.09%
view changes for bundle: recharts/bundle-cjs

Assets Changed:

Asset Name Size Change Total Size Change (%)
cartesian/Scatter.js 2.44kB 28.41kB 9.39% ⚠️
view changes for bundle: recharts/bundle-es6

Assets Changed:

Asset Name Size Change Total Size Change (%)
cartesian/Scatter.js 2.42kB 26.79kB 9.94% ⚠️

@ckifer

ckifer commented Mar 17, 2026

Copy link
Copy Markdown
Member

any concerns @PavelVanecek ?

@PavelVanecek

Copy link
Copy Markdown
Collaborator

Code change lgtm, I would like to find some time to test this before merging

roy7 and others added 3 commits June 11, 2026 16:07
…ectors

Extract ScatterPoint component from ScatterSymbols so each point
subscribes to its own isActive selector instead of the parent
subscribing to selectActiveTooltipIndex and re-rendering all points.
On hover, only the 1-2 affected points re-render instead of all N.

Addresses recharts#2862.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Address CodeRabbit review feedback:
- Add @param JSDoc tags to ScatterPoint component for 80%+ docstring coverage
- Use vi.useFakeTimers() and vi.runOnlyPendingTimers() in render-count test
  for correct Redux autoBatch timer handling
- Tighten assertions from toBeLessThan(pointCount) to toBeLessThanOrEqual(2)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Ensures vi.useRealTimers() runs even if an assertion fails,
preventing fake timer leaks into subsequent tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@roy7
roy7 force-pushed the test/scatterplot-performance branch from e6a5215 to 62dabd6 Compare June 11, 2026 20:16
@roy7

roy7 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto latest main to resolve the merge conflict with #7215 (new animation props).

The conflict was in ScatterSymbols: main now threads animationElapsedTime / isAnimating / isEntrance into each point's shape props, and isActive moved into ScatterShapeProps. I kept the extracted ScatterPoint component from this PR and threaded the new animation props through it, so the per-point subscription behavior is unchanged and the animation props flow exactly as they do on main.

No functional changes to the optimization itself — typecheck, lint, and the Scatter/ScatterChart suites all pass.

@PavelVanecek

Copy link
Copy Markdown
Collaborator

#7493

I want to measure before merging

@PavelVanecek

Copy link
Copy Markdown
Collaborator

So I am comparing this branch against the baseline from #7493

Mousemove looks way better:

image image

Initial render looks about the same:
image

@PavelVanecek

Copy link
Copy Markdown
Collaborator

I tried to remove the ZIndex completely but I can't see any difference in performance.

@PavelVanecek
PavelVanecek merged commit a3a3533 into recharts:main Jun 24, 2026
58 checks passed
@roy7
roy7 deleted the test/scatterplot-performance branch July 7, 2026 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants