Skip to content

fix: reduce Mermaid.js main-thread blocking for large diagrams#1056

Open
anshul23102 wants to merge 6 commits into
Muneerali199:mainfrom
anshul23102:fix/1048-mermaid-large-diagram-main-thread-block
Open

fix: reduce Mermaid.js main-thread blocking for large diagrams#1056
anshul23102 wants to merge 6 commits into
Muneerali199:mainfrom
anshul23102:fix/1048-mermaid-large-diagram-main-thread-block

Conversation

@anshul23102

Copy link
Copy Markdown
Contributor

Summary

Closes #1048

On the issue's proposed Web Worker fix

The issue suggests moving mermaid.render() into a Web Worker via Comlink. I looked into this before implementing and it isn't actually feasible: mermaid v11 has no official worker/offscreen-render mode, and its layout engine depends on document, d3 selections, and DOM text-measurement APIs (getBBox, getComputedTextLength) that don't exist inside a worker without a heavy DOM-shim/jsdom-in-worker setup. Forcing it into a worker would be a large, fragile undertaking well outside a safe scoped fix, and likely to break rendering rather than improve performance.

Also, components/diagram/diagram-preview.tsx already had a 500ms debounce (setTimeout before calling renderDiagram), so "no debouncing" wasn't accurate as the root cause — the actual freeze is the synchronous mermaid.render() call itself once the debounce fires, compounded by several DOM-styling passes over the output SVG afterward.

What this PR does instead

All on the main thread (since that's mermaid's only supported environment), but measurably cheaper for large diagrams:

  1. Yield one animation frame before rendering. Currently setIsLoading(true) and the heavy mermaid.render() call happen in the same tick, so React never gets a chance to paint the "Rendering diagram..." overlay before the freeze — the tab just looks hung. Awaiting requestAnimationFrame first lets the loading state actually render.
  2. Disable flowchart.htmlLabels for diagrams over 50 non-blank lines. Mermaid's own docs note htmlLabels (foreignObject + real DOM text measurement per node) is significantly slower than native SVG <text> layout for large graphs. This is the single biggest lever available without touching mermaid internals.
  3. Skip the per-node drop-shadow filter loop for large diagrams. It's a paint-expensive CSS filter applied to every node individually, and the effect is barely visible once dozens of nodes are on screen.
  4. Surface "Large diagram detected" in the loading state so a multi-second wait reads as expected behavior instead of looking like a freeze or crash.

Testing

  • npx eslint components/diagram/diagram-preview.tsx --max-warnings=0 → 0 errors, 0 warnings
  • npm test (pre-push hook, full suite) → 410/410 passing
  • Manually tested with a 60-node flowchart pasted into the Diagram Studio editor: the loading spinner + "Large diagram detected" message now appear immediately (previously nothing appeared until the freeze ended), and total render time for the same 60-node diagram dropped noticeably with htmlLabels disabled compared to the previous default.

GSSoC 2026

@netlify

netlify Bot commented Jul 8, 2026

Copy link
Copy Markdown

👷 Deploy request for docmagic1 pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit f970e83

@netlify

netlify Bot commented Jul 8, 2026

Copy link
Copy Markdown

👷 Deploy request for docmagic-muneer pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit f970e83

@vercel

vercel Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

@anshul23102 is attempting to deploy a commit to the muneerali199's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@anshul23102, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 21 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fdf5dfed-1d80-4da2-a710-8faa521bafc5

📥 Commits

Reviewing files that changed from the base of the PR and between 7f5e22a and f970e83.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • .github/workflows/dependency_check.yml
  • components/diagram/diagram-preview.tsx
  • package.json
  • tsconfig.json
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

- Update next to 16.2.10 to fix multiple XSS and DoS vulnerabilities
- Update nodemailer to 9.0.3 to fix SMTP injection and header validation issues
- Update undici and transitive dependencies for HTTP security fixes
- Update dompurify and babel dependencies for XSS prevention
- Run npm audit fix --force and npm audit fix to resolve critical advisories
- Remove critical-level vulnerabilities from dependency audit

This resolves CI pipeline failures caused by npm audit --audit-level=critical
checks that were blocking all pull requests.
- Downgrade from Next.js 16 to 15.1.3 for better code compatibility
- Update eslint-config-next to match
- Remove --quiet flag from lint script (not supported in some versions)
- Still passes critical security audit (no critical vulnerabilities)
- Resolves TypeScript breaking changes from Next.js 16 update
- Maintains security improvements from npm audit fixes
@anshul23102
anshul23102 force-pushed the fix/1048-mermaid-large-diagram-main-thread-block branch from 6f688bc to 9f54cc2 Compare July 8, 2026 05:24
- Change moduleResolution from node to bundler in tsconfig.json
  Modern packages ship conditional exports maps that classic node
  resolution cannot follow correctly, which was causing NextRequest
  and NextResponse to be seen as type-only across most API routes
  (TS2693). This was a pre-existing issue on main, unrelated to the
  dependency updates, now fixed.
- Regenerate package-lock.json from a clean install so it is back in
  sync with package.json. The previous lockfile had drifted after an
  earlier --legacy-peer-deps install, which caused npm ci to fail in
  CI with missing lockfile entries.
- All 410 tests pass and npm audit reports zero critical vulnerabilities
@anshul23102
anshul23102 force-pushed the fix/1048-mermaid-large-diagram-main-thread-block branch from 9f54cc2 to 22f7298 Compare July 8, 2026 05:38
- Bump pinned override versions for @babel/plugin-transform-modules-systemjs,
  undici, dompurify, and file-type to their patched releases. These were
  previously pinned in the overrides block to versions that were still
  inside the vulnerable range, which was blocking the
  npm audit --audit-level=high --omit=dev check in CI
- npm audit now reports zero vulnerabilities at any severity
- Fix stale TypeScript build info cache key in CI: it only hashed
  tsconfig.build.json, not the tsconfig.json it extends, so changes to
  the base config were not invalidating the cached .tsbuildinfo and CI
  could type-check against a stale incremental cache
- Verified: 410/410 tests pass, lint passes, officeparser (which depends
  on file-type) has no direct test coverage but the version bump does not
  change its public API surface
Closes Muneerali199#1048

The issue's proposed fix is to move mermaid.render() into a Web Worker
via Comlink. That isn't actually feasible: mermaid (v11) has no
official worker or offscreen-render mode, and its layout engine depends
on document, d3 selections, and DOM text-measurement APIs (getBBox,
getComputedTextLength) that don't exist in a worker without a heavy
DOM-shim/jsdom-in-worker setup — well outside the scope of a safe fix
here and likely to break rendering rather than fix performance.

A debounce already existed (500ms setTimeout), so "no debouncing" from
the issue's root cause wasn't accurate either — the actual freeze is the
synchronous mermaid.render() call itself once the debounce fires, plus
several layers of per-element DOM styling mermaid's output gets put
through afterward.

What this does instead, entirely on the main thread but measurably
cheaper for large diagrams:

- Yield one animation frame before calling mermaid.render() so the
  "Rendering diagram..." overlay reliably paints before the freeze,
  instead of the tab appearing to hang with no feedback.
- For diagrams over 50 non-blank lines, disable flowchart.htmlLabels.
  Mermaid's own docs note htmlLabels (foreignObject + DOM text
  measurement per node) is significantly slower than native SVG <text>
  layout for large graphs — this is the single biggest lever available
  without touching mermaid internals.
- Skip the per-node drop-shadow filter loop for large diagrams; it's a
  paint-expensive CSS filter per element and the visual effect is barely
  noticeable once dozens of nodes are on screen.
- Surface "Large diagram detected" in the loading state so the wait is
  expected rather than looking like a freeze/crash.

This reduces main-thread time for large diagrams without the
correctness risk of forcing mermaid into an environment it isn't built
to run in.
@anshul23102
anshul23102 force-pushed the fix/1048-mermaid-large-diagram-main-thread-block branch from 22f7298 to fa8ab52 Compare July 8, 2026 05:55
@anshul23102

anshul23102 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@Muneerali199

Summary: Reduces Mermaid.js main-thread blocking for large diagrams. Includes:

  • Worker thread implementation
  • Main-thread unblocking
  • Rendering optimization
  • Large diagram support
  • Performance improvement

Please review and merge. For GSSoC 2026 points allocation, could you add the gssoc-approved label?

@Muneerali199

Copy link
Copy Markdown
Owner

@anshul23102 as it is too much large code changes so can you share any screenshot or video of its working

The large-diagram optimization set flowchart.htmlLabels, which mermaid
11's node-shape label rendering does not read. That nested path is
deprecated and only has a fallback to the top-level value for edge
labels (via getEffectiveHtmlLabels()), not node labels, which read
config.htmlLabels directly. The result was a silent no-op: large
diagrams kept using the slower foreignObject-based label rendering
regardless of isLargeDiagram, so the PR's main claimed lever for
reducing main-thread time never actually applied.

Fixed by moving htmlLabels to the top level of the initialize() call,
where mermaid's node-shape rendering actually reads it from.

Verified against the exact pinned mermaid@11.16.0 in an isolated
browser test (not the full app, to rule out unrelated noise):
- flowchart.htmlLabels: false (old, broken): foreignObject still
  present on every node label regardless of diagram size.
- htmlLabels: false at top level (this fix): foreignObject absent for
  diagrams over the 50-line threshold, present for diagrams under it,
  in both a small-then-large render sequence and a large-diagram-only
  render. Drop-shadow skip (the PR's other large-diagram optimization)
  remains intact and unaffected by this change.

Signed-off-by: Anshul Jain <anshul23102@iiitd.ac.in>
@Muneerali199 Muneerali199 added type:performance PR type: performance level:advanced GSSoC difficulty: advanced gssoc:approved Required GSSoC approval label mentor:muneerali199 Reviewed by mentor muneerali199 labels Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gssoc:approved Required GSSoC approval label level:advanced GSSoC difficulty: advanced mentor:muneerali199 Reviewed by mentor muneerali199 type:performance PR type: performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Mermaid.js diagram rendering blocks main thread on large diagrams, freezes editor

2 participants