Skip to content

Conversation

bernardobelchior
Copy link
Member

@bernardobelchior bernardobelchior commented Jul 11, 2025

Improve scatter chart pointer move performance when highlight is off.

You might notice I transformed a bunch of anonymous functions into named functions. This helps a lot with performance investigations as the proper function names show up in the flame graph. Although slightly more verbose, I'd prefer if we could use named functions instead of anonymous ones 🙏

Before (74f16ba78e85ccfb6672d77ecd224f681e468360):

Screen.Recording.2025-07-11.at.16.29.36.mov

After:

Screen.Recording.2025-07-11.at.16.29.50.mov

Code from the video:

import { ScatterChart } from "@mui/x-charts";

const dataLength = 10_000;
const data = Array.from({ length: dataLength }).map((_, i) => ({
  x: i,
  y: 50 + Math.sin(i / 5) * 25,
}));

const xData = data.map((d) => d.x);

// https://github.com/mui/mui-x/issues/12960
export default function ScatterChart10kBench() {
  return (
    <ScatterChart
      xAxis={[{ data: xData }]}
      series={[{ data }]}
      width={500}
      height={300}
    />
  );
}

@bernardobelchior bernardobelchior added performance type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. scope: charts Changes related to the charts. labels Jul 11, 2025
Comment on lines +12 to +14
if (!highlightScope || !highlightedItem) {
return alwaysFalse;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the important part. Now if highlightScope or highlightedItem aren't defined, we always return the same function, so no re-renders happen.

Comment on lines +12 to +14
if (!highlightScope || !highlightedItem) {
return alwaysFalse;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the important part. Now if highlightScope or highlightedItem aren't defined, we always return the same function, so no re-renders happen.

@@ -91,39 +91,53 @@ function Scatter(props: ScatterProps) {

const classes = useUtilityClasses(inClasses);

const children = React.useMemo(() => {
Copy link
Member Author

Choose a reason for hiding this comment

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

Memoized the children so that if isFaded or isHighlighted (or any of the other props, really) don't change, then we don't re-render the children.

Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we memo the component then?

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you mean the Scatter component? We can, but the root cause of the re-renders was the useItemHighlightedGetter hook, so memoizing Scatter won't have any effect on this particular case.

It might help in other ones, but I'd be more inclined to memoizing it if there's a concrete case where it would help. Is there a case you're thinking of that you think memoizing the component would help?

Copy link
Member

Choose a reason for hiding this comment

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

No, I mean the children. The Marker component in this case

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, you're right. For this to have any effect, we'd either need to memoize ScatterMarker or g. I've reverted this change.

@mui-bot
Copy link

mui-bot commented Jul 11, 2025

Deploy preview: https://deploy-preview-18775--material-ui-x.netlify.app/

Bundle size report

Total Size Change: 🔺+4.84KB(+0.04%) - Total Gzip Change: 🔺+1.24KB(+0.03%)
Files: 122 total (0 added, 0 removed, 32 changed)

Show details for 100 more bundles (22 more not shown)

@mui/x-chartsparsed: 🔺+572B(+0.19%) gzip: 🔺+186B(+0.20%)
@mui/x-charts-proparsed: 🔺+572B(+0.15%) gzip: 🔺+175B(+0.15%)
@mui/x-charts-pro/RadarChartProparsed: 🔺+572B(+0.28%) gzip: 🔺+214B(+0.34%)
@mui/x-charts/RadarChartparsed: 🔺+572B(+0.30%) gzip: 🔺+194B(+0.33%)
@mui/x-charts/ScatterChartparsed: 🔺+318B(+0.16%) gzip: 🔺+48B(+0.08%)
@mui/x-charts/BarChartparsed: 🔺+202B(+0.10%) gzip: 🔺+41B(+0.06%)
@mui/x-charts-pro/BarChartProparsed: 🔺+195B(+0.07%) gzip: 🔺+39B(+0.04%)
@mui/x-charts-pro/LineChartProparsed: 🔺+195B(+0.07%) gzip: 🔺+17B(+0.02%)
@mui/x-charts/LineChartparsed: 🔺+195B(+0.09%) gzip: 🔺+6B(+0.01%)
@mui/x-charts-pro/FunnelChartparsed: 🔺+189B(+0.08%) gzip: 🔺+34B(+0.04%)
@mui/x-charts-pro/Heatmapparsed: 🔺+189B(+0.08%) gzip: 🔺+45B(+0.06%)
@mui/x-charts-pro/PieChartProparsed: 🔺+189B(+0.08%) gzip: 🔺+35B(+0.05%)
@mui/x-charts-pro/ScatterChartProparsed: 🔺+189B(+0.07%) gzip: 🔺+51B(+0.06%)
@mui/x-charts/PieChartparsed: 🔺+189B(+0.10%) gzip: 🔺+50B(+0.08%)
@mui/x-charts/SparkLineChartparsed: 🔺+189B(+0.09%) gzip: 🔺+47B(+0.07%)
@mui/x-charts-pro/ChartZoomSliderparsed: 🔺+159B(+0.15%) gzip: 🔺+27B(+0.08%)
@mui/x-charts-pro/ChartContainerProparsed: 🔺+30B(+0.02%) gzip: 🔺+4B(+0.01%)
@mui/x-charts-pro/ChartDataProviderProparsed: 🔺+30B(+0.02%) gzip: 🔺+5B(+0.01%)
@mui/x-charts/ChartContainerparsed: 🔺+30B(+0.02%) gzip: 🔺+5B(+0.01%)
@mui/x-charts/ChartDataProviderparsed: 🔺+30B(+0.02%) gzip: 🔺+5B(+0.01%)
@mui/x-charts/Gaugeparsed: 🔺+30B(+0.02%) gzip: 🔺+5B(+0.01%)
@mui/x-charts-pro/ChartsToolbarProparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsAxisparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsAxisHighlightparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsClipPathparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsGridparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsLabelparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsLegendparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsLocalizationProviderparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsOverlayparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsReferenceLineparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsSurfaceparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsTextparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsTooltipparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsXAxisparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/ChartsYAxisparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-charts/Toolbarparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-data-gridparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-data-grid-premiumparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-data-grid-premium/DataGridPremiumparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-data-grid-proparsed: 0B(0.00%) gzip: 🔺+1B(0.00%)
@mui/x-data-grid-pro/DataGridProparsed: 0B(0.00%) gzip: 🔺+1B(0.00%)
@mui/x-data-grid/DataGridparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickersparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-proparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterDateFnsparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterDateFnsJalaliparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterDayjsparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterLuxonparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterMomentparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterMomentHijriparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/AdapterMomentJalaaliparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DateRangeCalendarparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DateRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DateRangePickerDayparsed: 0B(0.00%) gzip: ▼-2B(-0.03%)
@mui/x-date-pickers-pro/DateRangePickerDay2parsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/DateTimeRangePickerparsed: 0B(0.00%) gzip: 🔺+1B(0.00%)
@mui/x-date-pickers-pro/DesktopDateRangePickerparsed: 0B(0.00%) gzip: ▼-1B(0.00%)
@mui/x-date-pickers-pro/DesktopDateTimeRangePickerparsed: 0B(0.00%) gzip: 🔺+1B(0.00%)
@mui/x-date-pickers-pro/DesktopTimeRangePickerparsed: 0B(0.00%) gzip: 🔺+1B(0.00%)
@mui/x-date-pickers-pro/LocalizationProviderparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/MobileDateRangePickerparsed: 0B(0.00%) gzip: 🔺+1B(0.00%)
@mui/x-date-pickers-pro/MobileDateTimeRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/MobileTimeRangePickerparsed: 0B(0.00%) gzip: 🔺+1B(0.00%)
@mui/x-date-pickers-pro/MultiInputDateRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/MultiInputDateTimeRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/MultiInputTimeRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/PickersRangeCalendarHeaderparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/SingleInputDateRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/SingleInputDateTimeRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/SingleInputTimeRangeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers-pro/StaticDateRangePickerparsed: 0B(0.00%) gzip: 🔺+1B(0.00%)
@mui/x-date-pickers-pro/TimeRangePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterDateFnsparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterDateFnsBaseparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterDateFnsJalaliparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterDayjsparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterLuxonparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterMomentparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterMomentHijriparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/AdapterMomentJalaaliparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DateCalendarparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DateFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DatePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DateTimeFieldparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DateTimePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DayCalendarSkeletonparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DesktopDatePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DesktopDateTimePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DesktopTimePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/DigitalClockparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/LocalizationProviderparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/MobileDatePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/MobileDateTimePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/MobileTimePickerparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/MonthCalendarparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/MultiSectionDigitalClockparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/PickerDay2parsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/PickersActionBarparsed: 0B(0.00%) gzip: 0B(0.00%)
@mui/x-date-pickers/PickersCalendarHeaderparsed: 0B(0.00%) gzip: 0B(0.00%)

Details of bundle changes

Generated by 🚫 dangerJS against 30ca7a5

Copy link

codspeed-hq bot commented Jul 11, 2025

CodSpeed Performance Report

Merging #18775 will not alter performance

Comparing bernardobelchior:improve-scatter-highlight-perf (30ca7a5) with master (9c42731)

Summary

✅ 10 untouched benchmarks

@bernardobelchior bernardobelchior marked this pull request as ready for review July 11, 2025 15:04
Copy link
Member

@JCQuintas JCQuintas left a comment

Choose a reason for hiding this comment

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

I find the inline-functions much worse for readability, while it is only useful in the case of debugging.

Though I'll leave that for @alexfauquette discretion.

I've left comments on the ones that feel most unnecessary

Comment on lines 219 to 227
function handleVoronoiMove(event: CustomEvent<PointerGestureEventData>) {
gestureHandler(event);
}
function handleVoronoiPan(event: CustomEvent<PointerGestureEventData>) {
gestureHandler(event);
}
function handleVoronoiQuickPress(event: CustomEvent<PointerGestureEventData>) {
gestureHandler(event);
}
Copy link
Member

Choose a reason for hiding this comment

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

Why?

Copy link
Member Author

Choose a reason for hiding this comment

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

The reasoning is the one I described in the PR description:

You might notice I transformed a bunch of anonymous functions into named functions. This helps a lot with performance investigations as the proper function names show up in the flame graph.

I'm ok with reverting those, it just means that I'll need to add them back again when debugging, but it's fine.

Comment on lines 173 to 181
function handleAxisMove(event: CustomEvent<PointerGestureEventData>) {
gestureHandler(event);
}
function handleAxisPan(event: CustomEvent<PointerGestureEventData>) {
gestureHandler(event);
}
function handleAxisQuickPress(event: CustomEvent<PointerGestureEventData>) {
gestureHandler(event);
}
Copy link
Member

Choose a reason for hiding this comment

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

why?

Copy link
Member Author

Choose a reason for hiding this comment

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

@@ -91,39 +91,53 @@ function Scatter(props: ScatterProps) {

const classes = useUtilityClasses(inClasses);

const children = React.useMemo(() => {
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we memo the component then?

Copy link
Member

@alexfauquette alexfauquette left a comment

Choose a reason for hiding this comment

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

I'm good with moving to more named functions if that help for perf debugging.

I'm a big arrow function user, mostly because It's the way to write maths on paper. But that's an habit I can modify :)

Thanks for moving the item memoisation outside of this PR, we could have a dedicated one to avoid mixing all the perf improvements in a single one 👍

@bernardobelchior bernardobelchior merged commit 1dbdb52 into mui:master Jul 15, 2025
23 checks passed
@bernardobelchior bernardobelchior deleted the improve-scatter-highlight-perf branch July 15, 2025 16:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
performance scope: charts Changes related to the charts. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants