-
Notifications
You must be signed in to change notification settings - Fork 3.3k
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
Reduce number of component updates #1938
Comments
Hey @mattkrick, thanks for opening this and for presenting a potential solution. Could you explain/decouple the solution for Android from the performance issue? I’m curious if there are other solutions that might allow for Android support, without needing to use |
Ah, just saw the #1935 thread. For more context in that case as to why I’d like to not use Slate is unlike some other React components in that it exists as a layer between your components, not strictly under them in the rendering tree. Because of this, using For example someone may want to change the color of a paragraph when the text content changes. Or they may want to change color based on how far away the cursor is from a specific node. These are just examples, but the point is that the logic required for And last I checked there wasn’t a good way to be able to block rendering in the middle of the tree, but have it continue again further down a specific branch. Maybe the new stuff coming out from fiber eliminates this issue? So that’s why I’m curious if there are other solutions we can make use of. |
there are certainly ways to re-render children even when their parent has
this is a great example! Let's roll with it:
class ChangeColorOnDiff {
shouldSlateUpdate() {
return oldColor != newColor
}
} if it's a legacy plugin or the author is lazy & doesn't include Internally, it's just an itty bitty bit of code: const defaultUpdater = () => true
let pushUpdate = []
for (const plugin of plugins) {
const shouldSlateUpdate = plugin.shouldSlateUpdate || defaultUpdater
const newUpdates = shouldSlateUpdate(argsTBD)
if (newUpdates === true) {
pushUpdate = true
} else if (Array.isArray(newUpdates) || typeof newUpdates === 'string') {
// future proof API
pushUpdate = pushUpdate.concat(newUpdates)
} else {
throw new Error('invalid return val')
}
// short circuit future plugin `shouldSlateUpdate` calls for extra performance
if (pushUpdate === true) break
} |
@mattkrick I think what you're describing is already possible with
Can you explain this? |
Ah shoot, lemme try to explain it better. I'll break it down in 3 different ways, lemme know if I make sense with any of em. By far, the most expensive operation is step 6:
Another way to think about it is a controlled vs. uncontrolled component. Today, it is a controlled component. You trigger an input, the input simultaneously updates the HTML element (cheap!) as well as slate's internal store. Then, it throws away the calculated HTML element value & calculate a new HTML element based on slate's internal store (expensive!). Just like an uncontrolled input, we'd let the HTML do its default thing, and then only when we need the value do we ask the DOM for it. This proposal is to insert a logic gate to decide whether to choose the cheap pre-computed HTML element, or the expensive calculated HTML element. Let's use a real example.
Now, let's add plugins.
To answer your question about |
Hey @mattkrick thanks for expanding. The issue I see is that in your example, the plugin is affecting the “data” by adding its own operation to the queue to change the color of a node. But what I’m trying to account for is not changing color with an operation itself, but purely as a rendering concern. To give another example, imagine a small counter next to every paragraph that showed the word count of the block. It’s not something that adds operations, it simply renders extra information. If the editor decided to use |
great example! so let's break it down & call it rendering to keep it cheap, we need to know when to invalidate So the parent component always renders, but the Now, if the plugin is super advanced & it takes |
@mattkrick I think we're not talking about the same thing, which may be my fault for not being clear. Here's a JSFiddle showing a As far as I can tell, this would not be possible if |
Or, similarly, here's one that styled nodes based on the current selection: https://jsfiddle.net/fj9dvhom/1224/. Again I don't think would be possible if the editor could short-circuit the rendering tree. |
oh wow that 2nd one is fun! i'd expect the logic rules for that plugin to look like this (sorry for the pseudo code): let pushUpdate = false;
if (oldAnchorBlockKey !== anchorBlockKey || oldFocusBlockKey !== focusBlockKey || oldBlocks.length !== blocks.length) {
pushUpdate = true
}
return pushUpdate in other words, if you type the letter "a", no update necessary. if you hit a backspace & you're not at the beginning of the line, no update necessary. But if you click to a different line, or you hit the Enter key, or you paste a bunch of lines of text, yeah, you gotta update. |
@mattkrick maybe, but that just forces the user to end up writing and managing incredibly complex diffing logic themselves, when most of the time it isn't a bottleneck. I'm going to close this. Feel free to open new issues with specific actionable items for which parts of rendering are slow and could be avoided. |
perhaps i didn't make this clear. the onus is on the plugin author. if they don't want to write that logic, that's fine. The performance is no worse than it is today.
I listed the exact bottlenecks in the original comment:
I've detailed out a plan to avoid those bottlenecks without the need for a breaking change. I don't know what else I can do. |
@mattkrick fair enough, I'll keep this open for discussion. Here are some additional thoughts...
That's exactly what worries me. I think the "branch pruning" approach is the one we need to strive for if we want to do something like this. The problem with the "cut down the whole tree" approach is that it has huge collateral damage, which results in leaky abstractions. If a single plugin aborts rendering the entire editor, there's no way of guaranteeing that another plugin didn't depend on the editor being re-rendered with its new state. This could technically be solved by some sort of communication abilities between plugins, but it gets very complex. (At least as far as I can understand from what you've described.)
It's hard to really tell where the performance issues are from your descriptions, because those flamegraphs don't really give much information...
Maybe so, but the If anything, I'd think that React's DOM updating logic runs in constant time for inserting text on small or large documents as long as the paragraphs themselves are the same size, and that it's much more likely that some of the methods in Slate's core whose run time is based on tree size would be the culprits.
Again, maybe so. I believe this one slightly more. But before nuking the entire rendering approach, it merits investigate what exactly is slow, and whether it's something that is avoidable via other means. I'd guess that there is more than one way to render and updated selection. Put simply, the "cut down the entire tree" approach is a huge change to make with many places it can be a leaky abstraction to the layers above it. For that reason, we'd be much better off doing almost any other performance improvements that are restricted to the data layer that might have gains. Based on everything you've described though—again thank you for opening this for discussion—I think there are potential areas for optimization, using the "branch pruning" approach. Since Slate's component hierarchy looks like this in pseudo-code: <UsersApp> (userland)
<Editor>
<Content>
<Node>
<UsersNode> (userland)
<Text>
<Leaf>
<Node>...
<Node>... There are issues with aborting rendering at the But as you mention, there's no easy way to abort early enough to stop the DOM from needing to be updated. We could potentially achieve this with logic that was able to abort at the These could even be done internally, without needing any userland plugins or other logic which can be complex for them to maintain. Operations could potentially contain some extra data about nodes that have had native updates and should not be re-rendered. (As opposed to containing only the nodes that should be re-rendered.) But, like I mentioned before, this is only one potential optimization. And I don't think there's any guarantee yet that it's the most important one to make to improve performance for large documents. If you or anyone else wants to investigate and do some in depth research with numbers/code to back up the assertions I'd welcome it. |
@ianstormtaylor I think the performance of keypress described on this issue was due to the example site was using the dev version of react which has been solved by #1939 . The current performance of keypress with production build is now ~40ms instead of ~300ms. Also tested production build of #1975 with memoization on slate react seems to reduce it down even further to ~20ms. |
I think this one has improved a lot over the past 3-4 years of work, so I'm closing it out. |
Do you want to request a feature or report a bug?
Bug
What's the current behavior?
Render time is slow.
In the HugeDocument example, a keypress event takes ~300ms (CPU slowed down 6x)

Compare that to the exact same document, but without updates propagated through react. It's <100ms:

Proposal:
(Re)introduce selective rendering logic.
As I understand it, here's the current flow:
Here's an improved flow:
pushUpdate
flagshouldComponentUpdate = () => value.get(pushUpdate)
Key notes:
The text was updated successfully, but these errors were encountered: