Skip to content

perf(mermaid): load the Mermaid block in one lazy chunk - #680

Open
xyrolle wants to merge 1 commit into
vercel:mainfrom
xyrolle:perf/lazy-mermaid-block
Open

xyrolle wants to merge 1 commit into
vercel:mainfrom
xyrolle:perf/lazy-mermaid-block

Conversation

@xyrolle

@xyrolle xyrolle commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Description

The code component lazy-loads the Mermaid chart, but it imports the block's download and fullscreen controls statically, and the fullscreen control imports the chart. That chain keeps the chart, pan-zoom, the SVG utilities and both controls in the main bundle, so the lazy boundary has no effect. This PR moves the whole block into lib/mermaid/block.tsx and loads it with a single lazy().

The resolved markup and the controls are unchanged. Loading does change: the first diagram on a page now downloads the block's chunk before the chart starts rendering, and the existing code block skeleton covers that wait. The skeleton is shorter than the 200px the chart reserves, as it already is on main.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Performance improvement
  • Refactoring (no functional changes)

Related Issues

Fixes #679

Related to #668, which changes how the same component handles errors from partial charts, and #490, which asks for controls integrated with the host.

Changes Made

  • lib/mermaid/block.tsx (new) renders the block: header, controls and chart, moved from the code component without changes.
  • lib/components.tsx
    • Loads the block with one lazy() inside the same Suspense.
    • No longer imports the controls or the chart.
    • Its cognitive-complexity suppression is unused now, so it goes.
  • __tests__/mermaid-bundle-boundary.test.ts (new), with its helper __tests__/built-consumer.ts:
    • The test bundles an app that imports only Streamdown from the built package with esbuild (through tsup, with code splitting).
    • It checks that the chart, the fullscreen view and the block controls are reachable only through import().
    • It needs pnpm build first, as CI already runs.
  • .changeset/lazy-mermaid-block.md (patch).

Testing

  • All existing tests pass
  • Added new tests for the changes
  • Manually tested the changes

The new test fails on main and passes with the change:

main:
 × loads the Mermaid block controls only through import()
 × loads the Mermaid chart only through import()
 × loads the Mermaid fullscreen view only through import()

this branch:
 ✓ __tests__/mermaid-bundle-boundary.test.ts (3 tests)

Full run on this branch:

pnpm build:packages   # ok
pnpm test             # 6 of 6 tasks successful
  streamdown: Test Files 97 passed (97), Tests 1267 passed (1267)
  remend 552, @streamdown/code 46, @streamdown/cjk 39, @streamdown/mermaid 14, @streamdown/math 10: all passed
pnpm check            # Checked 299 files. No fixes applied.

Test Coverage

The boundary test covers the built package. The existing Mermaid component, download, fullscreen and pan-zoom suites cover the block's behaviour, unchanged.

Bundle size

All sizes are KiB, gzip level 9, measured on tsup output before postbuild adds "use client". The normal build is slightly larger, and the up-front savings are the same.

  • Consumer rows: the app's entry is import { Streamdown } from "streamdown"; export { Streamdown };, with React external and every other dependency bundled. "Up front" means the entry and its static imports.
  • Package rows: this package's own build.
main this PR
Rolldown 1.1.5 (esm, minify): up front 148.18 143.52
Rolldown: block chunk — 5.76
esbuild 0.27.2 via tsup 8.5.1 (esm, splitting, minify): up front 156.94 151.93
esbuild: deferred 0.44 6.31
Package: dist/index.js and its static imports 27.94 22.90
Package: block chunk — 5.98

Screenshots/Demos

N/A

Checklist

  • My code follows the project's code style
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have created a changeset (pnpm changeset)
  • All my commits are signed

Changeset

  • I have created a changeset for these changes

Additional Notes

A failed chunk download still escapes to the host's error boundary, as it does on main for the chart's lazy import, and the chart's errorComponent can't handle a failure before its module loads.

The code component loads the Mermaid chart with lazy(), but it imports
the block's download and fullscreen controls statically, and the
fullscreen control imports the chart to render it full screen. That
chain puts the chart, pan-zoom, the SVG utilities and both controls in
the main bundle, and the chart's lazy chunk is a one-line re-export of
it. Every app pays for Mermaid's UI whether or not it renders a diagram.

Move the whole block (header, controls and chart) into
lib/mermaid/block.tsx and load it with a single lazy() behind the same
Suspense boundary. The resolved markup and the controls are unchanged.
Loading is not: the first diagram on a page now downloads the block's
chunk before the chart starts rendering, and the existing code block
skeleton covers that wait. The skeleton is shorter than the 200px the
chart reserves, as it already was.

A new test bundles an app that imports only Streamdown from the built
package with esbuild (through tsup, with code splitting) and checks
that the chart, the fullscreen view and the block controls are
reachable only through import(). It fails on main.

Sizes in KiB, gzip level 9, measured on tsup output before postbuild
adds "use client" (the normal build is slightly larger; the up-front
savings are the same). The app's entry is
`import { Streamdown } from "streamdown"; export { Streamdown };`
with React external and every other dependency bundled. Up front is
the entry and its static imports.

                                          main       this change
  Rolldown 1.1.5 (esm, minify)
    up front                              148.18     143.52
    block chunk                           -            5.76
  esbuild 0.27.2 via tsup 8.5.1
  (esm, splitting, minify)
    up front                              156.94     151.93
    deferred                                0.44       6.31
  This package's build
    index.js and its static imports        27.94      22.90
    block chunk                           -            5.98

The code component no longer needs its cognitive-complexity
suppression, so it goes.
@vercel

vercel Bot commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

@xyrolle is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

This branch has not been deployed

No deployments
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.

Mermaid's UI ships in the main bundle although the chart is lazy-loaded

1 participant