Skip to content

fix(Mermaid): improve preformance when editing mermaid diagram in wysiwyg mode#1158

Open
d3m1d0v wants to merge 3 commits into
mainfrom
fix-mermaid-lags
Open

fix(Mermaid): improve preformance when editing mermaid diagram in wysiwyg mode#1158
d3m1d0v wants to merge 3 commits into
mainfrom
fix-mermaid-lags

Conversation

@d3m1d0v

@d3m1d0v d3m1d0v commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary by Sourcery

Improve Mermaid diagram editing performance and responsiveness in the editor and adjust related tests and styles.

New Features:

  • Add a debounced DelayedTextArea component and hook to delay text updates while typing Mermaid diagrams.
  • Show a loading and updating state overlay while Mermaid diagrams are being (re)rendered in the preview.

Bug Fixes:

  • Prevent race conditions and state updates after unmount when rendering Mermaid diagrams by cancelling pending work.
  • Ensure Playwright tests correctly wait for all loader instances to be hidden before proceeding.

Enhancements:

  • Defer Mermaid diagram parsing and rendering with idle or animation frame scheduling to reduce UI blocking during edits.
  • Update Mermaid preview layout and styles to better handle loading and centering of content.

@sourcery-ai

sourcery-ai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Reviewer's Guide

Improves Mermaid WYSIWYG editing performance by debouncing textarea updates and deferring/canceling Mermaid rendering, plus adjusts Playwright waits for multiple loaders and adds minimal styling for new loading states.

File-Level Changes

Change Details Files
Defer and cancel Mermaid diagram rendering work to keep the editor responsive while text changes frequently.
  • Reworked MermaidPreview effect to set an updating flag and guard state updates with a cancellable flag on cleanup.
  • Wrapped mermaid.parse, theme-dependent initialize, and render calls in an async run function scheduled via requestIdleCallback when available, falling back to requestAnimationFrame otherwise.
  • Ensured cleanup cancels pending idle/animation callbacks with cancelIdleCallback or cancelAnimationFrame and skips state updates when cancelled.
  • Extended Preview markup to include updating state in the BEM class, show a base Loader when svg is missing, and overlay a Loader while updating is true.
packages/editor/src/extensions/additional/Mermaid/MermaidNodeView/MermaidView.tsx
packages/editor/src/extensions/additional/Mermaid/MermaidNodeView/Mermaid.scss
Introduce a debounced textarea abstraction for Mermaid edit mode to reduce the frequency of value updates during typing.
  • Added a generic useDelayedValue hook that keeps an internal currentValue and invokes an external onChange only after a timeout since the last change, resetting the timeout on each call.
  • Synchronized internal currentValue with external value when they differ to support controlled usage.
  • Implemented DelayedTextArea as a wrapper around the Gravity UI TextArea that uses useDelayedValue for debounced onUpdate behavior and proxies onChange to handle immediate clears when hasClear is enabled.
  • Replaced the raw TextArea in DiagramEditMode with DelayedTextArea, wiring value and onUpdate and configuring a 400ms delay.
packages/editor/src/react-utils/hooks/useDelayedValue.ts
packages/editor/src/react-utils/components/DelayedTextArea.tsx
packages/editor/src/extensions/additional/Mermaid/MermaidNodeView/MermaidView.tsx
Support multiple simultaneous loaders in Playwright test utilities to make loader wait helpers robust.
  • Updated loadersHiddenQASelect and loadersHidden fixtures to use a shared allMap helper that waits for all matching loader locators to become hidden instead of just one.
  • Added an allMap utility that materializes locator.all() and runs an async callback over each element via Promise.all.
demo/tests/playwright/core/wait.ts
Add styling hooks for the new Mermaid preview loading states.
  • Made Mermaid__Preview positioned relatively to host overlay content.
  • Added Mermaid__Preview_loading modifier styling to center its contents when in loading mode.
packages/editor/src/extensions/additional/Mermaid/MermaidNodeView/Mermaid.scss

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Comment thread packages/editor/src/extensions/additional/Mermaid/MermaidNodeView/MermaidView.tsx Outdated
@gravity-ui

gravity-ui Bot commented Jun 23, 2026

Copy link
Copy Markdown

Storybook Deployed

@gravity-ui

gravity-ui Bot commented Jun 23, 2026

Copy link
Copy Markdown

🎭 Playwright Report

@d3m1d0v d3m1d0v force-pushed the fix-mermaid-lags branch from c330ed3 to b200fe0 Compare June 23, 2026 17:28
@d3m1d0v d3m1d0v marked this pull request as ready for review June 23, 2026 17:50
@d3m1d0v d3m1d0v requested a review from makhnatkin as a code owner June 23, 2026 17:50

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 4 issues, and left some high level feedback:

  • In MermaidPreview the useEffect sets updating to true unconditionally but never resets it if mermaidInstance is falsy (the early return in run skips the setUpdating(false) paths), which can leave the overlay stuck in a loading state; consider short‑circuiting before setUpdating(true) or explicitly resetting updating when mermaidInstance is not available.
  • The new allMap helper in demo/tests/playwright/core/wait.ts calls locator.all() and then Promise.all over the resulting list; if the intent is to wait for all current and future loaders to disappear, this will only cover the current snapshot—consider whether you instead want to wait on a single locator with a composite selector or poll until count() is zero.
  • In DelayedTextArea, the onChangeProxy relies on event.type === 'click' and event.target.value === '' when hasClear is set; it might be safer to narrow the event target type and/or rely on the component’s clear callback semantics (if available) to avoid edge cases where non‑clear clicks or different event targets could satisfy this condition.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `MermaidPreview` the `useEffect` sets `updating` to `true` unconditionally but never resets it if `mermaidInstance` is falsy (the early return in `run` skips the `setUpdating(false)` paths), which can leave the overlay stuck in a loading state; consider short‑circuiting before `setUpdating(true)` or explicitly resetting `updating` when `mermaidInstance` is not available.
- The new `allMap` helper in `demo/tests/playwright/core/wait.ts` calls `locator.all()` and then `Promise.all` over the resulting list; if the intent is to wait for all current and future loaders to disappear, this will only cover the current snapshot—consider whether you instead want to wait on a single locator with a composite selector or poll until `count()` is zero.
- In `DelayedTextArea`, the `onChangeProxy` relies on `event.type === 'click'` and `event.target.value === ''` when `hasClear` is set; it might be safer to narrow the event target type and/or rely on the component’s clear callback semantics (if available) to avoid edge cases where non‑clear clicks or different event targets could satisfy this condition.

## Individual Comments

### Comment 1
<location path="packages/editor/src/extensions/additional/Mermaid/MermaidNodeView/MermaidView.tsx" line_range="44-45" />
<code_context>
+        let cancelled = false;
+        setUpdating(true);
+
+        const run = async () => {
+            if (!mermaidInstance) return;
+            try {
+                // Validates syntax and throws error if text is invalid
</code_context>
<issue_to_address>
**issue (bug_risk):** The `updating` flag can get stuck as `true` when `mermaidInstance` is falsy.

Because `setUpdating(true)` is called before `run`, and `run` returns early when `mermaidInstance` is falsy, `setUpdating(false)` is never reached in that case (e.g., when the instance becomes `null`/`undefined` after a prop change). Ensure `updating` is reset by handling the `!mermaidInstance` case explicitly in the effect, or by using a `finally` block so `setUpdating(false)` always runs when the effect finishes (and isn’t cancelled).
</issue_to_address>

### Comment 2
<location path="packages/editor/src/react-utils/hooks/useDelayedValue.ts" line_range="25-33" />
<code_context>
+) {
+    const [currentValue, setCurrentValue] = useState<TValue>(value);
+
+    const timeoutRef = useRef<number | undefined>(undefined);
+
+    const delayedOnChange = useCallback(
+        (nextValue: TValue) => {
+            setCurrentValue(nextValue);
+
+            window.clearTimeout(timeoutRef.current);
+
+            timeoutRef.current = window.setTimeout(() => {
+                onChange(nextValue);
+            }, delay);
</code_context>
<issue_to_address>
**issue (bug_risk):** Timeout created in `useDelayedValue` is never cleared on unmount.

Because the timeout is only cleared when scheduling a new one, a pending timeout can still fire after the component unmounts and call `onChange`. Add a `useEffect` cleanup that calls `window.clearTimeout(timeoutRef.current)` to prevent callbacks from running after unmount and to avoid potential leaks.
</issue_to_address>

### Comment 3
<location path="packages/editor/src/extensions/additional/Mermaid/MermaidNodeView/MermaidView.tsx" line_range="85-92" />
<code_context>

     return (
-        <div className={b('Preview')}>
+        <div className={b('Preview', {loading: !svg, updating})}>
             {svg ? <div className="mermaid" dangerouslySetInnerHTML={{__html: svg}} /> : <Loader />}
+            <Overlay visible={updating}>
+                <Loader />
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Two loaders can be rendered at the same time when `updating` is true and `svg` is falsy.

With `svg` falsy and `updating` true, both the base `Loader` and the overlay `Loader` render. If only one spinner should be visible, adjust the conditions—for example, hide the base loader while `updating`, or only show the overlay during updates.

```suggestion
    return (
        <div className={b('Preview', {loading: !svg, updating})}>
            {svg ? (
                <div className="mermaid" dangerouslySetInnerHTML={{__html: svg}} />
            ) : (
                !updating && <Loader />
            )}
            <Overlay visible={updating}>
                <Loader />
            </Overlay>
        </div>
    );
```
</issue_to_address>

### Comment 4
<location path="packages/editor/src/react-utils/components/DelayedTextArea.tsx" line_range="12" />
<code_context>
+
+import {useDelayedValue} from '../hooks/useDelayedValue';
+
+type DelayedTextAreaProps = Required<Pick<TextAreaProps, 'onUpdate' | 'value'>> &
+    Omit<TextAreaProps, 'onUpdate' | 'defaultValue'> & {delay?: number};
+
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the wrapper by tightening the props typing and composing the clear-logic with the original onChange handler to make behavior more explicit and easier to follow.

You can reduce complexity in this wrapper while keeping the behavior by simplifying the props typing and making the clear logic less “hidden” and more composable.

**1. Simplify `DelayedTextAreaProps`**

The current `Required<Pick<...>> & Omit<...>` shape adds cognitive load. You can keep `value` and `onUpdate` required without the indirection:

```ts
type DelayedTextAreaProps = TextAreaProps & {
    value: NonNullable<TextAreaProps['value']>;
    onUpdate: NonNullable<TextAreaProps['onUpdate']>;
    delay?: number;
};
```

This removes the need to reason about `Pick`/`Omit` and still guarantees `value`/`onUpdate` are present.

**2. Make `onChange` composition explicit**

Right now `onChangeProxy` completely replaces `TextArea`’s `onChange` except in the clear case, which is non-obvious and brittle. You can compose the clear special-case with the original `onChange` so consumers still get their handler called for all events:

```ts
function DelayedTextAreaComponent(
    props: DelayedTextAreaProps,
    ref: React.Ref<HTMLSpanElement>,
) {
    const {value, onUpdate, delay, onChange, hasClear, ...textAreaProps} = props;

    const {currentValue: textInputValue, delayedOnChange} = useDelayedValue(
        value,
        onUpdate,
        delay,
    );

    const handleChange: TextAreaProps['onChange'] = (event) => {
        // Always call the original onChange
        onChange?.(event);

        // Special-case native clear
        if (hasClear && event.type === 'click' && event.target.value === '') {
            onUpdate('');
        }
    };

    return (
        <TextArea
            {...textAreaProps}
            hasClear={hasClear}
            ref={ref}
            onChange={handleChange}
            value={textInputValue}
            onUpdate={delayedOnChange}
        />
    );
}
```

This keeps the clear behavior, but:

- The prop surface is simpler to read.
- The interaction between `onChange`/`onUpdate` is explicit and local.
- Consumers don’t lose `onChange` events except in a hidden branch.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread packages/editor/src/extensions/additional/Mermaid/MermaidNodeView/MermaidView.tsx Outdated
Comment thread packages/editor/src/react-utils/hooks/useDelayedValue.ts
Comment thread packages/editor/src/react-utils/components/DelayedTextArea.tsx
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