-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[charts] Improve performance of rendering ticks in x-axis #16536
[charts] Improve performance of rendering ticks in x-axis #16536
Conversation
const domResults = { count: 0, time: 0 }; | ||
if (typeof window !== 'undefined') { | ||
window.domResults = domResults; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rudimentary way to check how long we're spending in text measurements.
By running domResults
, you should see how many times (count
) and how long it took in ms (time
) to measure all the text that was measured so far. These values only include the cache misses.
let previousTextLimit = 0; | ||
const direction = reverse ? -1 : 1; | ||
return withDimension.map((item, labelIndex) => { | ||
const { width, offset, labelOffset, height } = item; | ||
const minGap = 8; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deploy preview: https://deploy-preview-16536--material-ui-x.netlify.app/ |
|
||
currentTextLimit = textPosition - (direction * (gapRatio * distance)) / 2; | ||
if (labelIndex > 0 && direction * currentTextLimit < direction * previousTextLimit) { | ||
if (labelIndex > 0 && direction * (textPosition - minGap) < direction * previousTextLimit) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there isn't enough space between the end of the last text plus the gap and the center of this tick, then we don't even need to calculate the width because we already know it won't fit.
0d87312
to
bc0ef42
Compare
if (!isPointInside({ x: textPosition, y: -1 }, { direction: 'x' })) { | ||
return { ...item, width: 0, height: 0, skipLabel: true }; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't calculate label width if the tick is outside the visible area (useful when zooming)
The results are still early and may change, but look promising. These changes reduced the number of actual DOM measurements from 401 to 83, and the time spent measuring from 126ms to 33ms (MacBook Pro M4 Pro, 48GB RAM, 6x slowdown), which is a 3.8x improvement. This is the component I'm using for benchmarking: import { BarChartPro } from '@mui/x-charts-pro';
const dataLength = 400;
const data = Array.from({ length: dataLength + 1 }).map((_, i) => ({
x: i,
y: 50 + Math.sin(i / 5) * 25,
}));
const xData = data.map((d) => d.x);
const yData = data.map((d) => d.y);
export default function BarChartProBench() {
return (
<BarChartPro
xAxis={[{ id: 'x', scaleType: 'band', data: xData, zoom: { filterMode: 'discard' } }]}
initialZoom={[{ axisId: 'x', start: 25, end: 75 }]}
series={[
{
data: yData,
},
]}
width={500}
height={300}
/>
);
} |
c889253
to
5218dd8
Compare
df4e976
to
0b8b73e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks already pretty solid. I added a commit to fix the benchmark. Your PR modifies which label is visible or not. And it impacted the labels the benchmark test was looking for to compute the rendering duration
Is their something missing to have this as ready for review?
Thanks!
Yeah, I'm experimenting with other approaches. You can see them here and here. The reason I don't want to merge this yet is because my investigations unveiled a bug in the measurement logic. The default label is being measured as 18px, when it is actually 21px. The This lead me to investigating how to performantly and accurately measure the width and height of text, which is what I'm exploring in those PoCs. Feel free to give your opinion. I think they're ready for some feedback 😄 |
bda19ce
to
be9591d
Compare
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
be9591d
to
0c490da
Compare
CodSpeed Performance ReportMerging #16536 will not alter performanceComparing Summary
|
Depends on #16548.
Part of #10928.
Measuring the size of text using
getBoundingClientRect
is expensive, especially when appending elements one by one to the DOM, which is what we're doing at the moment.Knowing that, this PR focuses on reducing the number of times we actually measure text. In the case of tick labels in the x-axis, this is done by:
Only if these two checks are true do we actually measure the text. This helps us reduce the number of measurements, improving performance for when rendering many labels.
Bear in mind that we cache the measurements, so the impact on performance will be less if we're measuring the same strings repeatedly.
Results
Rendering the following bar chart (from the "BarChartPro" benchmark) in a Vite application using production builds:
Before
401 cache misses, around 20ms spent measuring text.
After
99 cache misses, around 7ms spent measuring text.
For this example, this means a 75% reduction of measurements taken and around 65% reduction in time spent measuring.