Skip to content

Conversation

@tothatt81
Copy link
Contributor

When the sidebar is docked with a sufficiently large library open, there is some perceptible lag and choppiness during canvas interaction. This PR aims to improve performance and UX in this scenario.

My first impulse + our offline discussion led me to investigate any unnecessary re-renders. Something that both LibraryMenu and LibraryMenuContent are guilty of. By memoizing both components, I managed to mitigate this. With it, I also improved the pending elements calculation, as in the current version, rotation and resizing do not update the preview element correctly. These optimizations helped somewhat, but did not fully resolve the performance issue.

My next thought was that it might be the large number of (potentially) complex SVGs in the sidebar that may be the culprit. One approach I tried was lazy loading them with IntersectionObserver. The idea was to only insert the currently visible ones into the DOM. This showed some promise and definitely seemed to result in noticeably improved performance, but it still did not match the sidebar-less version.

However, this detour helped me identify the real bottleneck. I noticed that although pointer events no longer triggered React re-renders, there was still some unusual behavior. After some more investigation, I found that the App component frequently changes the --ui-pointerEvent CSS variable to block other elements' interactivity, and applying these changes was the root cause of the lags.

My proposed solution is to remove these changes while maintaining the same UX (i.e., no interaction with outside elements during canvas interaction). During a canvas pointer up event, we capture the pointer events to the canvas itself. This ensures that all other outside elements will be non-interactive until a pointer up event. I tested this solution and it provides the same UX as before, though there could be some edge cases I'm not aware of. Unfortunately, I have noticed the tests are failing because pepjs, the library that polyfills pointer events to jsdom does not properly support the capturing phase for pointer events. Still, I think this idea is worth considering at least.

Here is a short video comparing prod vs PR performance with the fix applied:

excalidraw.mp4

@vercel
Copy link

vercel bot commented Feb 3, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
excalidraw ✅ Ready (Inspect) Visit Preview Feb 4, 2025 8:59pm
excalidraw-package-example ✅ Ready (Inspect) Visit Preview Feb 4, 2025 8:59pm
excalidraw-package-example-with-nextjs ✅ Ready (Inspect) Visit Preview Feb 4, 2025 8:59pm
1 Skipped Deployment
Name Status Preview Updated (UTC)
docs ⬜️ Ignored (Inspect) Visit Preview Feb 4, 2025 8:59pm

@dwelle
Copy link
Member

dwelle commented Feb 3, 2025

Hey!

However, this detour helped me identify the real bottleneck. I noticed that although pointer events no longer triggered React re-renders, there was still some unusual behavior. After some more investigation, I found that the App component frequently changes the --ui-pointerEvent CSS variable to block other elements' interactivity, and applying these changes was the root cause of the lags.

My proposed solution is to remove these changes while maintaining the same UX (i.e., no interaction with outside elements during canvas interaction). During a canvas pointer up event, we capture the pointer events to the canvas itself. This ensures that all other outside elements will be non-interactive until a pointer up event. I tested this solution and it provides the same UX as before, though there could be some edge cases I'm not aware of. Unfortunately, I have noticed the tests are failing because pepjs, the library that polyfills pointer events to jsdom does not properly support the capturing phase for pointer events. Still, I think this idea is worth considering at least.

Interesting find, and solution! I've mocked the API for tests since we weren't testing this part of the UI behavior anyway. We'll see if it bites us in the future.

I've also kept the old logic in case the setPointerCapture is not supported — while it has a good browser support, on mobile it's not quite universal, and a pointer handling shouldn't be something we'll completely break in those cases.

--

I'll need to go through the LibraryMenu changes tomorrow, but so far, looking good 👍.

@dwelle
Copy link
Member

dwelle commented Feb 4, 2025

Alright, so first I removed the pendingElements update optimizations you made because there were issues with updates (e.g. when you change color of current selection, it wouldn't catch the change), and also it seemed it wasn't worth the effort.

But then I realized that in multiplayer scenarios it will rerender the LibraryMenuContent on every remote change because cursorButton will be up most of the time, so I reintroduced the memoization but made it a bit more robust — which also made it more complicated / slower. But it seems there's some non-trivial perf benefit.

That said, there are some non-intuitive things happening, such as that without the optim (technically with the optim as well) there are more rerenders when the remote client is dragging an element you don't have selected (which should bail) as opposed to editing your own selection. Unclear why, something that can be investigated later.

Thus, the quest for making Library sidebar more optimized continues, I would say — but I'm glad we've made a good step toward optimizing it :).

Thanks! ❤️

@dwelle dwelle merged commit 4f64372 into excalidraw:master Feb 4, 2025
8 checks passed
mtolmacs pushed a commit that referenced this pull request Feb 6, 2025
… docked with a large library open (#9086)

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
mtolmacs pushed a commit that referenced this pull request Feb 11, 2025
… docked with a large library open (#9086)

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
@sentry
Copy link

sentry bot commented Feb 12, 2025

Suspect Issues

This pull request was deployed and Sentry observed the following issues:

  • ‼️ ReferenceError: structuredClone is not defined updateElbowArrowPoints(packages/excalidraw/elem... View Issue

Did you find this useful? React with a 👍 or 👎

mtolmacs pushed a commit that referenced this pull request Feb 20, 2025
… docked with a large library open (#9086)

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
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.

2 participants