Skip to content

patchAttributes causes forced reflow on Chrome #1533

@yyjlincoln

Description

@yyjlincoln

We are using TipTap for our editors, but we have debugged this issue down to ProseMirror.

When initialising the editor (where Prosemirror is attaching to an fresh DOM element), in patchAttributes (reference), the dom.setAttribute statement would set contenteditable to false (where it was undefined and false by default).

Per this gist and its accompanying web.dev post, we have found that this setting of contenteditable does trigger an immediate style recalculation through profiling on the Prosemirror example. This happens even if one sets from undefined (new DOM element -- default to false) to false.

Image

This itself is not an issue with a simple page like this and with one editor; however our use case requires us to use multiple nested editors on a reasonably complex article (e.g. embedded, interactive elements that also uses editors to render its rich text content). This causes multiple setAttributes (across the main and embedded editors) to be called in the same tick, so the browser hangs (and all style recaluations except for the last one in the tick are marked as "forced reflow"). In some extreme cases (where over 50 editors are displayed at the same tick) this causes Interaction-to-Pointer (ITP) delays of over 10 seconds (as the page is very complex with other elements other than the editor). Without these forced reflows, the page performance could increase by more than 90%.

Image

In the image above, the next "Recalculate style", triggered by a nested editor within the same browser tick (as multiple editors are being initialised together) makes the previous style recalculation redundant.

To confirm the issue further, we added debug statements:

function patchAttributes(dom, prev, cur) {
    console.log("PATCH CUR IND", cur);
    for (let name in prev)
        if (name != "class" && name != "style" && name != "nodeName" && !(name in cur))
            dom.removeAttribute(name);
    for (let name in cur)
        if (name != "class" && name != "style" && name != "nodeName" && cur[name] != prev[name]) {
            dom.setAttribute(name, cur[name]);
            if (name === "contenteditable") {
                console.log("SET ATTR", name, cur[name], dom, prev, JSON.stringify(cur)); // <-- debug
                console.trace();
            }
        }

and we observed that prosemirror was trying to set contenteditable to false.

Guarding the dom.setAttribute using:

if (dom.getAttribute(name) !== cur[name])

Seems to have resolved the issue - by default, the DOM element has contenteditable=false so in this case it will skip the setting of contenteditable while initialising, thus avoiding the style recalc/repaint.

Could this please be confirmed by Prosemirror's end?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions