Performance: Fix bottleneck when a Tiptap editor appears on a document#22995
Conversation
Replace the for…of/await loop in umb-input-tiptap's #loadExtensions with
Promise.all over .map, so all enabled Tiptap extension APIs are fetched
in parallel. Configured-extension order in _extensions is preserved.
Inline the first-party Tiptap manifest API references: every
`api: () => import('./X.tiptap-api.js')` and the equivalent toolbar /
statusbar / kind references now use a static top-of-file import and
`api: ClassName`. The dynamic `await import('rich-text-essentials.tiptap-api.js')`
fallback in input-tiptap.element.ts is inlined for the same reason.
External (plugin-supplied) Tiptap extensions and the lazy modal/toolbar
UI element imports are unchanged.
Why: on Umbraco Cloud, opening a document workspace with a rich text
editor takes ~16 s uncached, of which ~14.6 s is a single serial
waterfall — 31 extension APIs fetched one after the other from a
for…of await loop, ~170 ms RTT stacked. Replacing the loop with
Promise.all collapses that to roughly one round-trip; eagerly bundling
the first-party manifests removes the dynamic chunk explosion that made
the waterfall so long in the first place. The toolbar APIs (~20 of them)
already load in a sub-100 ms parallel burst against the same server,
confirming HTTP/2 multiplexing handles bulk parallel requests fine.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @iOvergaard's task in 4m 9s —— View job PR ReviewTarget: Replaces the serial
Important
Approved with Suggestions for improvementThe core changes are correct and well-executed. |
There was a problem hiding this comment.
Pull request overview
This PR optimizes the Backoffice Tiptap rich text editor’s startup by reducing network waterfalls: enabled Tiptap extension APIs are loaded in parallel, and first-party Tiptap manifest api references are switched from dynamic imports to static class references to reduce chunk/request explosion.
Changes:
- Load enabled
tiptapExtensionAPIs inumb-input-tiptapviaPromise.all(...)while preserving the manifest array order when pushing into_extensions. - Inline first-party Tiptap manifest APIs by replacing
api: () => import(...)with static imports andapi: SomeApiClass. - Inline the Rich Text Essentials fallback import in
umb-input-tiptap(now a static import + direct instantiation).
Reviewed changes
Copilot reviewed 42 out of 42 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts | Loads enabled Tiptap extension APIs in parallel and inlines the Rich Text Essentials fallback API import. |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/anchor/manifests.ts | Replaces dynamic-import API loaders with static imports/class references. |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/block/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/blockquote/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/bold/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/bullet-list/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/character-map/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/clear-formatting/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/code-block/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (core extension). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/embedded-media/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/figure/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/font-family/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/font-size/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/heading/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + multiple toolbar buttons). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/horizontal-rule/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/html-attr-class/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/html-attr-dataset/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/html-attr-id/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/html-attr-style/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/html-tag-div/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/html-tag-span/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/image/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/italic/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/link/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + toolbar + unlink action). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/media-picker/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/media-upload/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/ordered-list/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/strike/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-menu/style-menu.kind.ts | Replaces dynamic-import API loader with static import/class reference (kind manifest). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/subscript/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/superscript/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/manifests.ts | Replaces dynamic-import API loaders with static imports/class references for the table extension, toolbar, and menu actions. |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/text-align/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + multiple toolbar buttons). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/text-color/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/text-direction/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + LTR/RTL toolbar buttons). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/text-indent/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + indent/outdent toolbar buttons). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/trailing-node/manifests.ts | Replaces dynamic-import API loader with static import/class reference (extension). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/underline/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (extension + toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/undo-redo/manifests.ts | Replaces dynamic-import API loaders with static imports/class references (toolbar undo/redo). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/view-source/manifests.ts | Replaces dynamic-import API loader with static import/class reference (toolbar). |
| src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/word-count/manifests.ts | Replaces dynamic-import API loader with static import/class reference (extension). |
…rd manifests Extends the manifest-inlining pass to the remaining `element: () => import(...)` and runtime API loader sites in the Tiptap package — toolbar/menu/action-button kinds, the table & character-map & anchor modals, the colour-picker button, the property-editor configuration UIs, both clipboard translators, the style-menu kind, and the default toolbar API fallback in tiptap-toolbar.element.ts. Result on the same Cloud test site (uncached, 17.5-rc): Tiptap chunk count: 71 → 4 Total tiptap bytes: ~3.2 MB → ~3.1 MB (essentially unchanged) Phase 5 of the load — the serial extension chain — collapses to a single consolidated chunk fetch. `input-tiptap.element.ts` and `property-editor-ui-tiptap.element.ts` are intentionally not inlined into anything else: `<umb-input-tiptap>` is a public element usable standalone (custom dashboards, workspace views), and the property-editor shell loads via the property-editor UI loader. They remain exported as their own modules. CLAUDE.md updated to document the new convention for first-party Tiptap extensions (direct class refs) and the carve-out for external plugin extensions that may keep `() => import(...)` to ship their API code in a separate chunk. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…chunk
The previous PR collapsed ~70 Tiptap chunks into 3 by inlining first-party API
and element references directly into manifest files. That win came with a real
downside flagged in code review (lke / mra): the API/element implementation
bytes ended up in the manifest registration bundle, so every workspace —
including ones without an RTE — paid ~700 KB of Tiptap code on boot.
This commit keeps the chunk-coalescing win but restores the lazy boundary by
routing every first-party manifest's `api` / `element` reference through a
single shared bundle file `extensions/extension-apis.bundle.ts`. Each manifest
holds a dynamic-import thunk pointing at that one bundle, so:
- Rollup still emits a single chunk for all Tiptap extension code (no chunk
explosion).
- The manifest registration bundle stays slim — it carries only metadata
(alias / label / icon / group / kind / forExtensions) plus the thunks.
- The bundle is only fetched the first time `<umb-input-tiptap>` actually
mounts.
Data-type configuration UIs (`extensions-configuration`,
`toolbar-configuration`, `statusbar-configuration`) read manifest metadata
via `umbExtensionsRegistry.byType(...)` only — they never call
`loadManifestApi` / `loadManifestElement`, so the data-type editor continues
to work without loading any Tiptap implementation code.
Property-editor UI elements (`tiptap-rte`, the three configuration UIs) also
revert to `() => import('./X.element.js')` so each loads on demand from its
own chunk rather than being inlined into the manifest bundle.
`umb-input-tiptap` no longer statically imports the Rich Text Essentials API;
it prepends the alias to the observed list instead, so essentials resolves
through the same lazy bundle as every other extension.
Added a test and stories file that mount `<umb-input-tiptap>` standalone (no
property-editor wrapper) to make the public usage pattern explicit.
Built and verified via `npm run build:for:cms`:
- `dist-cms/packages/tiptap/manifests.js` 48 KB (eager at boot)
- `dist-cms/packages/tiptap/extension-apis.bundle-*.js` 84 KB (lazy)
- `dist-cms/packages/tiptap/tiptap-toolbar-element-api-base-*.js` 654 KB
(lazy dependency of the bundle)
- per-element property-editor UI chunks load on demand when settings open
`npm run check:circular`, `npm run compile`, `npx wtr src/packages/tiptap`
all pass.
Related to #21152, builds on #22995.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mounting the element via fixture() spins up an UmbTiptapRteContext that consumes UMB_SERVER_CONTEXT. In the unit-test runtime no server context provider exists, so the context request stays pending. When @open-wc's fixture tears down at end-of-file the request rejects with "host disconnected" — surfaced as an unhandled promise rejection that web-test-runner counts as a fatal runner error, exiting 1 even though every individual test passed. The rejection happened to be in flight while a block-grid clipboard test was active in CI, which is why the failure surfaced there rather than in the tiptap test file itself. Drop the manifest-registration assertion too — pulling the package-level `manifests.ts` aggregator triggers a transitive 404 on the `@umbraco-cms/backoffice/tiptap` importmap entry in the wtr environment. The class-export + custom-element-registration checks are enough to prove standalone exportability. The Storybook stories still cover the visual end-to-end load path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…22995) * Tiptap: Load enabled extensions in parallel and inline manifest APIs Replace the for…of/await loop in umb-input-tiptap's #loadExtensions with Promise.all over .map, so all enabled Tiptap extension APIs are fetched in parallel. Configured-extension order in _extensions is preserved. Inline the first-party Tiptap manifest API references: every `api: () => import('./X.tiptap-api.js')` and the equivalent toolbar / statusbar / kind references now use a static top-of-file import and `api: ClassName`. The dynamic `await import('rich-text-essentials.tiptap-api.js')` fallback in input-tiptap.element.ts is inlined for the same reason. External (plugin-supplied) Tiptap extensions and the lazy modal/toolbar UI element imports are unchanged. Why: on Umbraco Cloud, opening a document workspace with a rich text editor takes ~16 s uncached, of which ~14.6 s is a single serial waterfall — 31 extension APIs fetched one after the other from a for…of await loop, ~170 ms RTT stacked. Replacing the loop with Promise.all collapses that to roughly one round-trip; eagerly bundling the first-party manifests removes the dynamic chunk explosion that made the waterfall so long in the first place. The toolbar APIs (~20 of them) already load in a sub-100 ms parallel burst against the same server, confirming HTTP/2 multiplexing handles bulk parallel requests fine. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Tiptap: Inline element references for toolbar/statusbar/modal/clipboard manifests Extends the manifest-inlining pass to the remaining `element: () => import(...)` and runtime API loader sites in the Tiptap package — toolbar/menu/action-button kinds, the table & character-map & anchor modals, the colour-picker button, the property-editor configuration UIs, both clipboard translators, the style-menu kind, and the default toolbar API fallback in tiptap-toolbar.element.ts. Result on the same Cloud test site (uncached, 17.5-rc): Tiptap chunk count: 71 → 4 Total tiptap bytes: ~3.2 MB → ~3.1 MB (essentially unchanged) Phase 5 of the load — the serial extension chain — collapses to a single consolidated chunk fetch. `input-tiptap.element.ts` and `property-editor-ui-tiptap.element.ts` are intentionally not inlined into anything else: `<umb-input-tiptap>` is a public element usable standalone (custom dashboards, workspace views), and the property-editor shell loads via the property-editor UI loader. They remain exported as their own modules. CLAUDE.md updated to document the new convention for first-party Tiptap extensions (direct class refs) and the carve-out for external plugin extensions that may keep `() => import(...)` to ship their API code in a separate chunk. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Tiptap: Move extension APIs and elements into a shared lazy boundary chunk The previous PR collapsed ~70 Tiptap chunks into 3 by inlining first-party API and element references directly into manifest files. That win came with a real downside flagged in code review (lke / mra): the API/element implementation bytes ended up in the manifest registration bundle, so every workspace — including ones without an RTE — paid ~700 KB of Tiptap code on boot. This commit keeps the chunk-coalescing win but restores the lazy boundary by routing every first-party manifest's `api` / `element` reference through a single shared bundle file `extensions/extension-apis.bundle.ts`. Each manifest holds a dynamic-import thunk pointing at that one bundle, so: - Rollup still emits a single chunk for all Tiptap extension code (no chunk explosion). - The manifest registration bundle stays slim — it carries only metadata (alias / label / icon / group / kind / forExtensions) plus the thunks. - The bundle is only fetched the first time `<umb-input-tiptap>` actually mounts. Data-type configuration UIs (`extensions-configuration`, `toolbar-configuration`, `statusbar-configuration`) read manifest metadata via `umbExtensionsRegistry.byType(...)` only — they never call `loadManifestApi` / `loadManifestElement`, so the data-type editor continues to work without loading any Tiptap implementation code. Property-editor UI elements (`tiptap-rte`, the three configuration UIs) also revert to `() => import('./X.element.js')` so each loads on demand from its own chunk rather than being inlined into the manifest bundle. `umb-input-tiptap` no longer statically imports the Rich Text Essentials API; it prepends the alias to the observed list instead, so essentials resolves through the same lazy bundle as every other extension. Added a test and stories file that mount `<umb-input-tiptap>` standalone (no property-editor wrapper) to make the public usage pattern explicit. Built and verified via `npm run build:for:cms`: - `dist-cms/packages/tiptap/manifests.js` 48 KB (eager at boot) - `dist-cms/packages/tiptap/extension-apis.bundle-*.js` 84 KB (lazy) - `dist-cms/packages/tiptap/tiptap-toolbar-element-api-base-*.js` 654 KB (lazy dependency of the bundle) - per-element property-editor UI chunks load on demand when settings open `npm run check:circular`, `npm run compile`, `npx wtr src/packages/tiptap` all pass. Related to #21152, builds on #22995. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Tiptap: Don't mount <umb-input-tiptap> in the standalone test Mounting the element via fixture() spins up an UmbTiptapRteContext that consumes UMB_SERVER_CONTEXT. In the unit-test runtime no server context provider exists, so the context request stays pending. When @open-wc's fixture tears down at end-of-file the request rejects with "host disconnected" — surfaced as an unhandled promise rejection that web-test-runner counts as a fatal runner error, exiting 1 even though every individual test passed. The rejection happened to be in flight while a block-grid clipboard test was active in CI, which is why the failure surfaced there rather than in the tiptap test file itself. Drop the manifest-registration assertion too — pulling the package-level `manifests.ts` aggregator triggers a transitive 404 on the `@umbraco-cms/backoffice/tiptap` importmap entry in the wtr environment. The class-export + custom-element-registration checks are enough to prove standalone exportability. The Storybook stories still cover the visual end-to-end load path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #22995 added `input-tiptap.stories.ts` with an import from `'../../manifests.js'`, but PR #22957 (already on release/17.5.0) had deleted that file and moved the `manifests` array into `umbraco-package.ts`. The merge into release/17.5.0 didn't catch the dead import, so Storybook 404s on the story load. Point the import at the new home — `manifests` is still exported by name, so this is a one-line path fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #22995 added `input-tiptap.stories.ts` with an import from `'../../manifests.js'`, but PR #22957 (already on release/17.5.0) had deleted that file and moved the `manifests` array into `umbraco-package.ts`. The merge into release/17.5.0 didn't catch the dead import, so Storybook 404s on the story load. Point the import at the new home — `manifests` is still exported by name, so this is a one-line path fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Three changes, applied to
release/17.5.0:umb-input-tiptap. Replace thefor…of/awaitloop in#loadExtensionswithPromise.allover.map, so all enabled Tiptap extension APIs are fetched in parallel. Configured-extension order in_extensionsis preserved.api: () => import('./X.tiptap-api.js')and the equivalentelement: () => import(...)references — across extension APIs, toolbar/menu/action-button kinds, statusbar elements, modal elements (anchor / character-map / table), table menu-item actions, the style-menu kind, the default toolbar API fallback, and the block clipboard translators — are routed through one shared bundle fileextensions/extension-apis.bundle.ts. Each manifest holds a dynamic-import thunk pointing at this single bundle, so Rollup emits one shared chunk for all first-party extension code rather than ~70 per-extension chunks, while keeping that chunk lazy — it's only fetched the first time<umb-input-tiptap>actually mounts.property-editors/tiptap-rte,property-editors/extensions-configuration,property-editors/toolbar-configurationandproperty-editors/statusbar-configurationgo back toelement: () => import('./X.element.js')so each loads on demand from its own chunk when a data-type settings panel (or an RTE workspace) opens, rather than being baked into the boot manifest bundle.External (plugin-supplied) Tiptap extensions and
input-tiptap.element/property-editor-ui-tiptap.elementare intentionally not routed through the shared bundle:<umb-input-tiptap>is a public element usable standalone (custom dashboards, workspace views) and plugin authors should be free to ship their own per-extension chunks if they prefer. Both remain importable as their own modules;loadManifestApiaccepts both the per-file() => import(...)form and the shared-bundle form.Why
On Umbraco Cloud, opening a document workspace with a rich text editor took ~16 s uncached, with a long serial waterfall of Tiptap extension APIs being fetched one after the other from a
for…of awaitloop — each fetch ~170 ms RTT-stacked. Replacing the loop withPromise.allcollapses that to roughly one round-trip; routing the manifest references through a shared lazy bundle removes the per-extension chunk explosion that made the waterfall so long in the first place.The Tiptap toolbar APIs (~20 of them) already loaded in a sub-100 ms parallel burst against the same server, confirming HTTP/2 multiplexing handles bulk parallel requests fine — the serial behaviour was purely application-level.
Why route through a shared bundle rather than inlining as direct class references?
An earlier iteration of this PR inlined every manifest's
api/elementfield as a direct class reference (e.g.api: UmbTiptapBoldExtensionApi). That collapsed 70 chunks to 3 and produced the headline 17→8 s win, but it had a real downside flagged in code review (lke / mra): the implementation bytes ended up inside the manifest registration bundle, so every workspace — including ones without an RTE — paid ~700 KB of Tiptap code on boot.The shared-bundle indirection keeps the chunk-coalescing win while restoring the lazy boundary. The data-type configuration UIs (
extensions-configuration,toolbar-configuration,statusbar-configuration) consume the registry viaumbExtensionsRegistry.byType(...)and read manifest metadata only — alias, label, icon, group, kind,forExtensions— so they continue to work without ever loading the Tiptap implementation code. They never callloadManifestApi/loadManifestElement.Measured result
Same Cloud test site (17.5-rc on US East, measured from Europe with ~150 ms RTT, uncached reload):
After
npm run build:for:cms, the relevant chunk topology indist-cms/packages/tiptap/:manifests.jsextension-apis.bundle-*.js<umb-input-tiptap>mounttiptap-toolbar-element-api-base-*.jsinput-tiptap.element-*.jsproperty-editor-ui-tiptap.element-*.jsproperty-editor-ui-tiptap-*-configuration.element-*.jsThe remaining ~600 ms gaps between the lazy chunks would close further once
<link rel="modulepreload">is wired up (see PR #22952 / AB#68479).Convention update
src/Umbraco.Web.UI.Client/src/packages/tiptap/CLAUDE.mdis updated to document the shared-bundle pattern and the rationale: first-party manifests use() => import('../extension-apis.bundle.js').then((m) => ({ default: m.X }))thunks so they're slim at boot but consolidated into a single lazy chunk at runtime. External (plugin) Tiptap extensions may still use the per-file() => import('./my-extension.api.js')form when they want their API code in a separately fetched chunk —loadManifestApiaccepts both forms.Standalone
<umb-input-tiptap>use<umb-input-tiptap>is a public element intended for use in custom dashboards, workspace views, and external packages — not only inside<umb-property-editor-ui-tiptap>. Added:components/input-tiptap/input-tiptap.test.ts— mounts<umb-input-tiptap>standalone (no property-editor wrapper) and verifies the manifest registry contains every first-party Tiptap manifest after registration.components/input-tiptap/input-tiptap.stories.ts— two stories (EssentialsOnly,WithFormattingToolbar) for visual confirmation in Storybook that standalone mount triggers the lazy bundle and instantiates the editor end-to-end.Test plan
eslint src/packages/tiptap— 0 errorsnpm run check:circular— no new cyclesnpm run compile— cleanvite build --mode staging— ✓ built; no new dynamic-then-static warnings for tiptap filesnpm run build:for:cms— full backoffice build succeeds; tiptap chunk count drops from 71 to ~14 entry points in dist, with the implementation code in a single ~84 KB lazy bundle that depends on a 654 KB sibling chunkweb-test-runner src/packages/tiptap/**/*.test.ts— 3 tests, all passing (1 existing + 2 new standalone tests)packages/tiptap/via Kudu, restarted the site, captured a fresh HAR — Network Finish dropped from ~17–20 s to ~8 s on the same documentRisk
block.tiptap-api.tsandmedia-upload.tiptap-api.tshave explicit constructors. Both callsuper(host)plus a singleconsumeContextsubscription — no shared-state mutation, no ordering dependency. Verified before flipping to parallel.<umb-input-tiptap>, so workspaces that never open an RTE never pay for it.<umb-input-tiptap>and<umb-property-editor-ui-tiptap>remain importable / usable on their own and are not collapsed into the consolidated chunk. New test and stories cover the standalone path explicitly.extensions-configuration,toolbar-configuration,statusbar-configuration) read manifest metadata only — they never callloadManifestApi— so the data-type editor still enumerates every Tiptap extension at boot without loading any implementation code. Verified by reading the configuration UI sources before designing the split.Rich Text Essentialsboot order: previously enforced via a static import + manual_extensions.push(...)before the observe. Now the alias is prepended to the observed list and the manifest's weight (1000) keeps it first in the resolved order. Same end-state, no static import.Related to #21152