Skip to content

Conversation

@nabbydude
Copy link
Contributor

Description
normalizeNode makes some expensive checks that really stack up when a lot of nodes change at once

Issue
Addresses #5945, thank you to the people in that thread for the research & analysis

Example
Running the test described in the above issue, pasting 10000 lines into the plaintext demo:

main:
image
this PR:
image

Current version takes ~11.9 seconds on my machine to handle the paste event once dispatched by React, ~9.7s of which is spent in normalizeNode. This PR shrinks those to ~1.9s and ~0.4s respectively.

Context
as @12joan pointed out, most of the time is taken up by isEditor, but there's only one editor in any given tree and its passed as an argument to the function so checking if a node is an editor is as simple as node === editor.

This implementation assumes any children that needed normalizing have been normalized already and are thus valid nodes. It also assumes its being called with normalizing disabled and so it can predict eg. that exactly one node will be removed when removeNodes is called on a path.

Checking for inline/blockiness of children is disentangled into two separate loops.

Adding children: [] to a node that is neither text nor element to coerce it into an element now happens when you call normalizeNode on that node, not on its parent.

My regression testing has consisted of the mocha/jest suite and anecdotal testing with examples but this seems like the sort of area that could cause unexpected issues, especially mingling with custom extensions, but from what I can tell it seems fine.

Checks

  • The new code matches the existing patterns and styles.
  • The tests pass with yarn test.
  • The linter passes with yarn lint. (Fix errors with yarn fix.)
  • The relevant examples still work. (Run examples with yarn start.)
  • You've added a changeset if changing functionality. (Add one with yarn changeset add.)

@changeset-bot
Copy link

changeset-bot bot commented Nov 16, 2025

🦋 Changeset detected

Latest commit: be4b0f7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
slate Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment on lines +45 to +54
// Determine whether the node should have only block or only inline children.
// - The editor should have only block children.
// - Inline elements should have only inline children.
// - Elements that begin with a text child or an inline element child should have only inline children.
// - All other elements should have only block children.
const shouldHaveInlines =
!(element === editor) &&
(editor.isInline(element) ||
Text.isText(element.children[0]) ||
editor.isInline(element.children[0]))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this might be worth factoring into its own function, would allow implementers to easily enforce certain blocks to only allow block children (eg. lists -- currently if a list somehow ends up with a text node as its first child, its items will all become unwrapped)

Comment on lines -43 to -48
for (let i = 0; i < node.children.length; i++, n++) {
const currentNode = Node.get(editor, path)
if (Text.isText(currentNode)) continue
const child = currentNode.children[n] as Descendant
const prev = currentNode.children[n - 1] as Descendant
const isLast = i === node.children.length - 1
Copy link
Contributor Author

Choose a reason for hiding this comment

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

old implementation used i as an index into the snapshot of the node children passed into normalizeNode and n as an index into the children of the live node as its changed--this would be useful if we could use i to avoid Node.get calls by reading from the cached node whenever possible, but needing to know the prev child, which might be a newly added node, creates complexity.

Comment on lines -67 to -69
Transforms.unwrapNodes(editor, { at: path.concat(n), voids: true })
}
n--
Copy link
Contributor Author

Choose a reason for hiding this comment

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

pretty sure there's an invisible bug if this runs. we add some number of new child nodes and step back to read them--but we still only run the loop a number of times equal to the number of original children (everywhere else we add a child we skip over it and num iterations stay the same)

I dont think it caused issues due to the multi-pass nature that normalizeNode gets called but we can avoid running it more times than we need to by accounting for it.

element = Node.get(editor, path) as Element
n++
}
if (n === element.children.length - 1) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

using if instead of else-if here so we can make the before and after nodes in the same pass--this could cause issues if any wrappers around normalizeNode rely on doing something in between but I can't imagine why anything reasonably would

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.

1 participant