Skip to content

perf: Reduce re-renders and network requests during drag and drop#276

Merged
rickbutterfield merged 4 commits intov5/devfrom
v5/feature/perf-improvements
Apr 11, 2026
Merged

perf: Reduce re-renders and network requests during drag and drop#276
rickbutterfield merged 4 commits intov5/devfrom
v5/feature/perf-improvements

Conversation

@rickbutterfield
Copy link
Copy Markdown
Owner

Summary

  • Remove 500ms debounce timer — previews render immediately when data changes
  • Add deep comparison (hasChanged) on content/settings properties to prevent re-renders when Umbraco reassigns the same data with new object references during drag and drop
  • Observer callbacks only trigger renderBlockPreview() as a catch-up mechanism when no successful render has occurred yet — fixes initial load race condition without causing re-renders on every sort
  • Remove custom sort mode handling (sortModeActive, observeSortMode, renderSortModeFallback) — this is handled natively by Umbraco since 17.2
  • Switch stylesheets from <link> elements to adoptedStyleSheets with CSSStyleSheet objects cached on the shared BlockPreviewContext, eliminating all stylesheet re-fetching during re-renders or element recreation

Test plan

  • Verify block previews load correctly on initial page load (block grid, block list, rich text)
  • Drag and drop blocks — verify no unnecessary API calls or stylesheet re-fetches in the Network tab
  • Edit block content — verify preview updates immediately without delay
  • Verify stylesheets still apply correctly to preview content in shadow DOM

🤖 Generated with Claude Code

…and drop

- Remove 500ms debounce timer, render previews immediately on data change
- Add deep comparison (hasChanged) on content/settings properties to skip
  re-renders when Umbraco reassigns same data with new object references
- Add catch-up rendering in observer callbacks — only triggers render if
  no successful render has occurred yet, solving initial load race condition
  without causing re-renders on every sort operation
- Remove custom sort mode handling (sortModeActive, observeSortMode,
  renderSortModeFallback) — handled natively by Umbraco since 17.2
- Switch stylesheets from <link> elements to adoptedStyleSheets with
  CSSStyleSheet objects cached on shared BlockPreviewContext, eliminating
  all stylesheet re-fetching during re-renders or element recreation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Mar 17, 2026

test connection

@claude
Copy link
Copy Markdown

claude bot commented Mar 17, 2026

CLAUDE.md - BlockPreview v5

Overview

BlockPreview is an Umbraco community package that enables rich HTML backoffice previews for Block Grid, Block List, and Rich Text editors.

Technologies:

  • .NET 10 (C#) - Backend library and API
  • TypeScript/Lit - Frontend UI components (Umbraco backoffice extension)
  • Vite - Frontend build tooling

Target Platform: Umbraco CMS v17+

Package: Umbraco.Community.BlockPreview on NuGet


Repository Structure

/src                    - Source projects
  /Umbraco.Community.BlockPreview       - Main .NET library (RCL)
  /Umbraco.Community.BlockPreview.UI    - TypeScript frontend (Lit components)
/examples               - Example/test sites
  /Umbraco.Community.BlockPreview.TestSite  - Umbraco 17 test site
/tools                  - Build utilities
  /Umbraco.Community.BlockPreview.SchemaGenerator - JSON schema generator
/docs                   - Documentation
/.github                - CI/CD workflows, README, assets

Project Dependencies:

  • Umbraco.Community.BlockPreview references Umbraco.Community.BlockPreview.UI (embeds compiled JS assets)
  • TestSite references Umbraco.Community.BlockPreview for local development

Build Commands

Full Solution:

dotnet build Umbraco.Community.BlockPreview.sln

Main Package (Release):

dotnet build src/Umbraco.Community.BlockPreview/Umbraco.Community.BlockPreview.csproj --configuration Release
dotnet build examples/Umbraco.Community.BlockPreview.TestSite/Umbraco.Community.BlockPreview.TestSite.csproj

Frontend Assets:

cd src/Umbraco.Community.BlockPreview.UI

# Install dependencies (requires Node.js >=22.12.0)
npm install

# Development mode with hot reload
npm run dev

# Build for production
npm run build

Run Test Site:

dotnet run --project examples/Umbraco.Community.BlockPreview.TestSite

Test site credentials:

  • Username: admin@example.com
  • Password: 1234567890

Versioning

Uses Nerdbank.GitVersioning for automatic versioning.

  • Version defined in version.json (currently 5.0.0)
  • Release tags: release-{version} (e.g., release-5.0.0)
  • Release branches: release/{version}

Teamwork & Collaboration

Branching:

  • Main branch: v5/main
  • Development branch: v5/dev
  • Feature branches merged via PR to v5/dev

CI/CD:

  • release.yml - Builds and pushes to NuGet on release-* tags or release/* branches
  • codeql.yml - Security scanning

Contributing: See .github/CONTRIBUTING.md

  • Fork repository, create PR with changes
  • Issues for bugs/feature discussions

Quick Reference

Key Directories:

Path Description
/src Source code
/examples Test site
/tools Build utilities
/docs Documentation

Projects:

Project Type Description
Umbraco.Community.BlockPreview .NET RCL Main package library
Umbraco.Community.BlockPreview.UI TypeScript/Vite Backoffice UI components
Umbraco.Community.BlockPreview.TestSite .NET Web Development test site
Umbraco.Community.BlockPreview.SchemaGenerator .NET Console appsettings JSON schema generator

Documentation:


Architecture Notes

Razor view caching: Never cache ViewEngineResult objects. ASP.NET Core's RazorView holds a single IRazorPage with mutable state (ViewContext, Output) that is set during RenderAsync. Caching the ViewEngineResult shares the page across concurrent requests, causing race conditions (empty renders, ObjectDisposedException). Cache the resolved view path instead and call _razorViewEngine.GetView(path) per request to get a fresh RazorView/IRazorPage.

Debugging approach: When investigating rendering bugs, add diagnostic logging first before building fixes. Symptoms like empty strings, disposed object exceptions, and load-dependent failures can all stem from a single shared-state concurrency bug. Log the actual exception types and locations to avoid misdiagnosing the root cause.

@claude
Copy link
Copy Markdown

claude bot commented Mar 17, 2026

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning.

5.3.2 - 2026-02-12

Fixed

  • Fix concurrent block preview rendering producing empty strings or ObjectDisposedException by caching view paths instead of ViewEngineResult objects (shared IRazorPage race condition)
  • Fix frontend preview elements treating empty API responses as loading forever (if (data) to if (data != null))
  • Add concurrency-limited request queue (max 3) to prevent overwhelming the server with simultaneous preview requests
  • Add stale response protection to prevent outdated API responses from overwriting newer previews
  • Add retry with child service scope for ObjectDisposedException during Razor view rendering
  • Add diagnostic logging throughout BlockViewRenderer and BlockDataConverter for easier troubleshooting
  • Protect BlockDataConverter.ConvertPropertyValue against exceptions from FromEditor, preserving original value on failure

5.3.1 - 2026-02-12

Fixed

  • Resolve ambiguous constructor error in BlockPreviewService

5.3.0 - 2026-02-12

Added

  • Cached view resolution for improved performance
  • Specific error message when ModelsBuilder is not configured
  • Acceptance tests suite with Playwright (#245)

Fixed

  • Stop data-block-preview-link clicks from bubbling to parent blocks (#246)
  • Block non-edit action bar clicks from propagating to parent block
  • Fix slider values (#244)
  • Return 200 OK instead of 404 when no stylesheets are configured (#243)
  • Use single configured language as culture fallback for block previews (#242)
  • Fix IsBlockPreviewRequest check by removing unreliable IsBackOfficeRequest check (#241)

5.2.1 - 2026-01-23

Fixed

  • Fix RTE blocks stylesheet loading to match BlockGrid/BlockList behavior (#226)

5.2.0 - 2026-01-22

Added

  • Adds IBlockPreviewResponseEnricher (#218)
  • Add IgnoredContentTypes support to BlockPreview config for excluding element types from previews (82133cc)

Fixed

New Contributors: @lakesol (#221), @skttl (#218)

5.1.0 - 2026-01-08

Added

  • Add support for multiple stylesheets per block type (#209)
  • Add async CreateViewDataAsync method with sync fallback (#212)

Changed

  • Remove custom sort mode and upgrade to Umbraco 17.1.0-rc (#211)

Fixed

New Contributors: @Copilot (#175), @GianniDPC (#214)

5.0.0 - 2025-11-27

Added

  • Added features that allow consumers to customise View Location and runtime stylesheet loading (#116)
  • Add Claude Code GitHub Workflow (#187)

Changed

  • Project restructure (#178)

New Contributors: @BenWhite27 (#116)


v4 (Umbraco 16)

4.2.2 - 2026-02-12

Fixed

  • Fix concurrent block preview rendering producing empty strings or ObjectDisposedException by caching view paths instead of ViewEngineResult objects

4.2.1 - 2026-02-12

Fixed

  • Resolve ambiguous constructor error in BlockPreviewService

4.2.0 - 2026-02-12

Added

  • Sort mode property action for block grid

Fixed

  • Stop data-block-preview-link clicks from bubbling to parent blocks
  • Block non-edit action bar clicks from propagating to parent block
  • Return 200 OK instead of 404 when no stylesheets are configured
  • Use single configured language as culture fallback for block previews
  • Remove unreliable IsBackOfficeRequest check from IsBlockPreviewRequest
  • Use generic CMS property editor conversion and fix preview errors

Changed

  • Use UMB_CONTENT_WORKSPACE_CONTEXT for broader workspace compatibility
  • Extract services from BlockPreviewService (IBlockModelFactory, IBlockViewRenderer, IBlockDataConverter)
  • Cached view resolution for improved performance

4.1.1 - 2026-01-23

Fixed

  • Fix RTE blocks stylesheet loading to match BlockGrid/BlockList behavior
  • Stop unnecessary calls to stylesheet API when documentTypeUnique is undefined (#226)

4.1.0 - 2026-01-22

Added

  • Add IBlockPreviewResponseEnricher
  • Add IgnoredContentTypes support to BlockPreview config for excluding element types from previews
  • Add support for multiple stylesheets per block type
  • Add async CreateViewDataAsync method with sync fallback
  • Cache busting for App_Plugins

Changed

  • Reinstate sort mode for v16

Fixed

4.0.7 - 2025-11-28

Fixed

4.0.6 - 2025-11-27

Changed

  • Project restructure

Full Changelog: https://github.com/rickbutterfield/BlockPreview/releases

@claude
Copy link
Copy Markdown

claude bot commented Mar 17, 2026

{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
"version": "5.4.0-alpha",
"assemblyVersion": {
"precision": "build"
},
"gitCommitIdShortFixedLength": 7,
"nuGetPackageVersion": {
"semVer": 2.0
},
"publicReleaseRefSpec": [
"^refs/heads/main$",
"^refs/heads/release/",
"^refs/tags/release-"
],
"cloudBuild": {
"buildNumber": {
"enabled": true
}
},
"release": {
"tagName": "release-{version}",
"branchName": "release/{version}"
}
}

@claude
Copy link
Copy Markdown

claude bot commented Mar 17, 2026

test123

@claude
Copy link
Copy Markdown

claude bot commented Mar 17, 2026

{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
"version": "5.4.0-alpha",
"assemblyVersion": {
"precision": "build"
},
"gitCommitIdShortFixedLength": 7,
"nuGetPackageVersion": {
"semVer": 2.0
},
"publicReleaseRefSpec": [
"^refs/heads/main$",
"^refs/heads/release/",
"^refs/tags/release-"
],
"cloudBuild": {
"buildNumber": {
"enabled": true
}
},
"release": {
"tagName": "release-{version}",
"branchName": "release/{version}"
}
}

@claude
Copy link
Copy Markdown

claude bot commented Mar 17, 2026

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning.

5.3.2 - 2026-02-12

Fixed

  • Fix concurrent block preview rendering producing empty strings or ObjectDisposedException by caching view paths instead of ViewEngineResult objects (shared IRazorPage race condition)
  • Fix frontend preview elements treating empty API responses as loading forever (if (data) to if (data != null))
  • Add concurrency-limited request queue (max 3) to prevent overwhelming the server with simultaneous preview requests
  • Add stale response protection to prevent outdated API responses from overwriting newer previews
  • Add retry with child service scope for ObjectDisposedException during Razor view rendering
  • Add diagnostic logging throughout BlockViewRenderer and BlockDataConverter for easier troubleshooting
  • Protect BlockDataConverter.ConvertPropertyValue against exceptions from FromEditor, preserving original value on failure

5.3.1 - 2026-02-12

Fixed

  • Resolve ambiguous constructor error in BlockPreviewService

5.3.0 - 2026-02-12

Added

  • Cached view resolution for improved performance
  • Specific error message when ModelsBuilder is not configured
  • Acceptance tests suite with Playwright (#245)

Fixed

  • Stop data-block-preview-link clicks from bubbling to parent blocks (#246)
  • Block non-edit action bar clicks from propagating to parent block
  • Fix slider values (#244)
  • Return 200 OK instead of 404 when no stylesheets are configured (#243)
  • Use single configured language as culture fallback for block previews (#242)
  • Fix IsBlockPreviewRequest check by removing unreliable IsBackOfficeRequest check (#241)

5.2.1 - 2026-01-23

Fixed

  • Fix RTE blocks stylesheet loading to match BlockGrid/BlockList behavior (#226)

5.2.0 - 2026-01-22

Added

  • Adds IBlockPreviewResponseEnricher (#218)
  • Add IgnoredContentTypes support to BlockPreview config for excluding element types from previews (82133cc)

Fixed

New Contributors: @lakesol (#221), @skttl (#218)

5.1.0 - 2026-01-08

Added

  • Add support for multiple stylesheets per block type (#209)
  • Add async CreateViewDataAsync method with sync fallback (#212)

Changed

  • Remove custom sort mode and upgrade to Umbraco 17.1.0-rc (#211)

Fixed

New Contributors: @Copilot (#175), @GianniDPC (#214)

5.0.0 - 2025-11-27

Added

  • Added features that allow consumers to customise View Location and runtime stylesheet loading (#116)
  • Add Claude Code GitHub Workflow (#187)

Changed

  • Project restructure (#178)

New Contributors: @BenWhite27 (#116)


v4 (Umbraco 16)

4.2.2 - 2026-02-12

Fixed

  • Fix concurrent block preview rendering producing empty strings or ObjectDisposedException by caching view paths instead of ViewEngineResult objects

4.2.1 - 2026-02-12

Fixed

  • Resolve ambiguous constructor error in BlockPreviewService

4.2.0 - 2026-02-12

Added

  • Sort mode property action for block grid

Fixed

  • Stop data-block-preview-link clicks from bubbling to parent blocks
  • Block non-edit action bar clicks from propagating to parent block
  • Return 200 OK instead of 404 when no stylesheets are configured
  • Use single configured language as culture fallback for block previews
  • Remove unreliable IsBackOfficeRequest check from IsBlockPreviewRequest
  • Use generic CMS property editor conversion and fix preview errors

Changed

  • Use UMB_CONTENT_WORKSPACE_CONTEXT for broader workspace compatibility
  • Extract services from BlockPreviewService (IBlockModelFactory, IBlockViewRenderer, IBlockDataConverter)
  • Cached view resolution for improved performance

4.1.1 - 2026-01-23

Fixed

  • Fix RTE blocks stylesheet loading to match BlockGrid/BlockList behavior
  • Stop unnecessary calls to stylesheet API when documentTypeUnique is undefined (#226)

4.1.0 - 2026-01-22

Added

  • Add IBlockPreviewResponseEnricher
  • Add IgnoredContentTypes support to BlockPreview config for excluding element types from previews
  • Add support for multiple stylesheets per block type
  • Add async CreateViewDataAsync method with sync fallback
  • Cache busting for App_Plugins

Changed

  • Reinstate sort mode for v16

Fixed

4.0.7 - 2025-11-28

Fixed

4.0.6 - 2025-11-27

Changed

  • Project restructure

Full Changelog: https://github.com/rickbutterfield/BlockPreview/releases

@claude
Copy link
Copy Markdown

claude bot commented Mar 17, 2026

{
"$schema": "https://marketplace.umbraco.com/umbraco-marketplace-schema.json",
"AlternatePackageNames": ["Umbraco.Community.BlockPreview", "Our.Umbraco.BlockPreview"],
"AuthorDetails": {
"Name": "Rick Butterfield",
"Description": "Senior Developer (DXP) at Umbraco",
"Url": "https://rickbutterfield.dev",
"ImageUrl": "https://github.com/rickbutterfield.png",
"Contributors": [
{
"Name": "Dave Woestenborghs",
"Url": "https://github.com/dawoe"
},
{
"Name": "Matthew Wise",
"Url": "https://github.com/matthew-wise"
},
{
"Name": "Ben White",
"Url": "https://github.com/BenWhite27",
}
]
},
"Category": "Editor Tools",
"DocumentationUrl": "https://github.com/rickbutterfield/Umbraco.Community.BlockPreview",
"LicenseTypes": [ "Free" ],
"IssueTrackerUrl": "https://github.com/rickbutterfield/Umbraco.Community.BlockPreview/issues",
"PackageType": "Package",
"Screenshots": [
{
"ImageUrl": "https://raw.githubusercontent.com/rickbutterfield/Umbraco.Community.BlockPreview/main/.github/assets/screenshot1.png",
"Caption": "The Umbraco backoffice showing a panel titled 'Select view', with two HTML files in a list available for selection"
}
],
"Title": "BlockPreview"
}

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Mar 17, 2026

Code Review

Overall this is a solid performance PR with well-motivated changes. The refactoring to a base class is a nice improvement, and the adoptedStyleSheets approach is the right call for eliminating redundant fetches. A few issues worth discussing before merging.


Potential Bugs

1. Accumulating context subscriptions in observeBlockPropertyValue

In all three concrete elements, observeBlockValue() sets up an observation callback that calls observeBlockPropertyValue() every time block entry context values change. Each call creates a new consumeContext subscription without cleaning up the previous one. Over the lifetime of a session, subscriptions accumulate.

The fix: guard with a flag so the manager context is only subscribed once, or replace rather than stack the subscription.

2. blockIndex in block-grid is always 0

block-grid-preview.custom-view.element.ts line 138:

this._blockContext.blockIndex = contents.indexOf(this.blockGridValue.contentData[0]);

blockGridValue.contentData is assigned contents (not filtered by contentUdi like block-list does), so contentData[0] === contents[0] and indexOf always returns 0. Suggest using: contents.findIndex(x => x.key === this._blockContext.contentUdi)

3. getOrCreateStylesheet is not safe under concurrent requests

block-preview.context.ts lines 60-70: two elements requesting the same stylesheet concurrently both miss the cache, both fetch it, and the second silently overwrites the first. A proper guard stores the in-flight Promise to deduplicate concurrent requests.

4. RTE: observeBlockWorkspaceFallback called unconditionally

rich-text-preview.custom-view.element.ts line 73 calls observeBlockWorkspaceFallback() synchronously right after starting consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT). Since context resolution is async, _workspaceContextResolved is still false at that point, so the fallback always registers. If the document workspace resolves later, both code paths call handleWorkspaceData then observeBlockValue(), potentially creating duplicate entry context subscriptions.


Breaking Change: RC dependency

Directory.Packages.props bumps the minimum to [17.3.0-rc, 18.0.0). The PR notes sort mode was handled natively since 17.2, but the package now requires an unreleased RC. Users on stable 17.1 or 17.2 cannot install this version. Is this intentional pending 17.3 GA?

Also: removing packageSourceMapping from NuGet.config and placing the Umbraco prereleases MyGet feed before nuget.org could cause unexpected package resolution in CI. Consider restoring source mapping or placing the prerelease feed after nuget.org.


Design Notes

Catch-up mechanism assumption

The guard on !this._htmlMarkup && !this._isLoading means that after the first successful render, changes to layouts/exposes/propertyAlias from the manager context no longer trigger re-renders. Re-renders are driven solely by content/settings property changes via hasChanged in updated(). This is sound for drag-and-drop, but a brief comment documenting the intentional design would help future maintainers avoid accidentally removing the guard.

JSON.stringify deep comparison

The hasChanged guard on content/settings is the right technique. For very large block data it is O(n) on every assignment, but unlikely to be a practical bottleneck.


Minor

  • .preview-alert fallback color is #f0ac00 (amber/yellow) but the class is used for the error state. Pre-existing issue, easy to fix while touching these styles.
  • BlockPreviewRequestQueue is a pure standalone class ideally suited to unit tests. Concurrency edge cases (queue draining on error, task ordering) would benefit from coverage.
  • testing-1-2-3.config in uSync/v17/Content/ looks like scratch data from development. Intentional addition or accidental commit?

Good work overall. The adoptedStyleSheets caching and deep-comparison guard are exactly the right fixes for the drag-and-drop churn. Items 1 and 2 are the ones most worth addressing before merging to avoid subtle rendering correctness issues.

rickbutterfield and others added 2 commits April 11, 2026 08:35
- Guard #observeBlockPropertyValue with flag to prevent accumulating
  context subscriptions on repeated entry context emissions
- Fix blockIndex in block-grid always returning 0 by using findIndex
  with contentUdi instead of indexOf on unfiltered contentData
- Deduplicate concurrent stylesheet fetches by caching the in-flight
  Promise rather than the resolved CSSStyleSheet
- Fix RTE duplicate subscriptions by wrapping document workspace
  context in try/catch so fallback only registers on failure
- Set Umbraco dependency to [17.3.0, 18.0.0) (stable, not RC)
- Remove accidental testing-1-2-3.config scratch data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@rickbutterfield rickbutterfield merged commit 97d7427 into v5/dev Apr 11, 2026
4 checks passed
@rickbutterfield rickbutterfield deleted the v5/feature/perf-improvements branch April 11, 2026 07:36
@claude
Copy link
Copy Markdown

claude bot commented Apr 11, 2026

Code review - see details below

@claude
Copy link
Copy Markdown

claude bot commented Apr 11, 2026

Code Review

The overall direction here is solid. Deep comparison to prevent unnecessary re-renders during drag-and-drop, adoptedStyleSheets for efficient style sharing, and removing the now-redundant sort mode scaffolding are all good improvements. A few things stand out worth addressing before this ships broadly.


Bug: try/catch in #observeDocumentWorkspace breaks the fallback path

File: rich-text-preview.custom-view.element.ts

consumeContext is callback-based and does not throw synchronously when a context is not found — it simply never invokes the callback. The catch block will therefore never execute in normal usage, making observeBlockWorkspaceFallback() effectively dead code. The original code called the fallback unconditionally after consumeContext; the intent was to also watch the block workspace context when no document workspace is present, which this rewrite silently drops.

A cleaner approach: track resolution inside the callback and explicitly call the fallback if the context was never resolved, rather than relying on a try/catch that never fires.


Inconsistency: blockIndex fix not applied to BlockList

File: block-list-preview.custom-view.element.ts

BlockGrid was correctly updated to:

this._blockContext.blockIndex = contents.findIndex(x => x.key === this._blockContext.contentUdi);

BlockList still uses:

this._blockContext.blockIndex = contents?.indexOf(this.blockListValue.contentData[0]);

Array.indexOf uses reference equality — the same identity bug that was fixed in BlockGrid likely exists in BlockList too when Umbraco reassigns objects during drag-and-drop.


JSON.stringify deep comparison has edge cases

File: block-preview-base.element.ts

hasChanged: (val, old) => JSON.stringify(val) !== JSON.stringify(old)

This works in practice but has fragile corners: property key ordering differences will cause false positives/negatives; undefined is silently dropped and NaN/Infinity become null; and O(n) serialization runs on every property assignment for potentially large nested objects. A lightweight structural-equality utility (e.g. fast-deep-equal) would be more robust.


Failed stylesheet fetches are permanently cached

File: block-preview.context.ts

The Promise is stored before it resolves. If the fetch fails (404, network hiccup), the rejected promise is cached indefinitely — no retry is possible for that URL. A simple fix:

promise.catch(() => this.#stylesheetCache.delete(href));
this.#stylesheetCache.set(href, promise);

Silent failure when validatePreviewData() returns false

File: block-preview-base.element.ts

Removing the error/loading state assignment when validation fails means the preview silently shows nothing when data is genuinely missing. During initial load this silence is fine, but for permanent failures the user gets no indication. Consider setting the error only after the workspace context has resolved and validation still fails.


Minimum version floor may be too aggressive

File: Directory.Packages.props

The PR description says sort mode is handled natively since 17.2, yet the floor is raised to 17.3, excluding users on 17.2 without a stated reason. If there is no 17.3-specific API requirement (just an RC used for dev), [17.2.0, 18.0.0) seems more appropriate.


NuGet.config: packageSourceMapping removed

Removing source mapping allows packages to be resolved from either nuget.org or the Umbraco prereleases myget feed without explicit routing. Since the prerelease feed is only needed for RC dev, keeping a narrow Umbraco.* mapping for that source rather than removing source mapping entirely would maintain supply-chain hygiene.


Minor: missing newline at EOF

Directory.Packages.props and NuGet.config both lost their trailing newlines.


Overall this is a meaningful performance improvement and the adoptedStyleSheets caching approach is clean. The try/catch fallback issue in rich-text-preview and the BlockList blockIndex inconsistency are the two items I would prioritize before the next release.

@claude
Copy link
Copy Markdown

claude bot commented Apr 11, 2026

Code Review

Overall this is a well-thought-out performance PR. The core ideas — immediate renders instead of 500ms debounce, adoptedStyleSheets caching, deep-comparison to prevent re-renders, and removing now-redundant sort mode handling — are all sound improvements. A few issues worth addressing before merge.


Bugs

1. Negative promise caching in getOrCreateStylesheet

block-preview.context.ts caches the promise before it resolves:

const promise = fetch(href)
    .then(response => response.text())
    .then(css => { ... });
this.#stylesheetCache.set(href, promise);

If the fetch fails (network error, 404, etc.), the rejected promise is cached. All future calls return the same rejected promise — the stylesheet can never load without a page reload. Fix by removing from the cache on failure:

const promise = fetch(href)
    .then(r => { if (!r.ok) throw new Error(`Failed to fetch stylesheet: ${href}`); return r.text(); })
    .then(css => { const s = new CSSStyleSheet(); s.replaceSync(css); return s; });
promise.catch(() => this.#stylesheetCache.delete(href));
this.#stylesheetCache.set(href, promise);

2. No response.ok check in getOrCreateStylesheet

A 404 response won't throw — fetch() resolves successfully. The HTML error-page body will be fed to CSSStyleSheet.replaceSync() as if it were CSS. Add an if (!response.ok) throw ... guard (combined with fix #1 above).

3. Behavioral change in #observeDocumentWorkspace (rich-text)

try {
    this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, ...);
} catch {
    this.observeBlockWorkspaceFallback();
}

consumeContext does not throw when the context is unavailable — it simply never calls the callback. The original code registered both observers unconditionally (the fallback was the safety net if the primary never resolved). With this change, observeBlockWorkspaceFallback() is only reached on a synchronous throw, which realistically never happens — the fallback is now dead code. This will break RTE preview when the document workspace context is unavailable. The original approach (or a context-resolution timeout guard) should be restored.

4. _error not checked in catch-up render condition

In all three subclasses:

if (!this._htmlMarkup && !this._isLoading) {
    this.renderBlockPreview();
}

This doesn't check _error. If a previous render failed and set _error, the catch-up will call renderBlockPreview() on every subsequent data observation update, flooding the API with retries. Consider adding && !this._error.


Performance

5. JSON.stringify for deep comparison

hasChanged: (val: any, old: any) => JSON.stringify(val) !== JSON.stringify(old)

This serialises the entire block data on every property assignment. For large block grids this could be noticeable. Also, property key order differences can cause false positives ({a:1, b:2} vs {b:2, a:1} would be treated as changed). A lightweight structural-equality helper or memoising the last serialised value would be safer.


Configuration concerns

6. NuGet.config — prerelease source + removed package source mapping

The PR adds the Umbraco prerelease MyGet feed and removes the packageSourceMapping block. This is fine for local development against the RC but could cause contributors to inadvertently pull packages from the prerelease source. This probably shouldn't be merged as-is — better kept in a local override or reverted before targeting v5/dev.

7. Minimum version bump: 17.1.0 → 17.3.0

The PR description says sort mode is "handled natively by Umbraco since 17.2", but the minimum is bumped to 17.3.0. Is 17.3.0 the true minimum for all the APIs used here, or should this be 17.2.0? Worth documenting the specific API that requires 17.3.


Minor

  • Missing newlines at end of file in Directory.Packages.props and NuGet.config.
  • #managerObserved = false — the false initialiser is the JS class field default and is redundant (harmless).

Summary

Priority Issue
Blocking #1 & #2 — negative promise caching + no response.ok check
Blocking #3observeBlockWorkspaceFallback is now unreachable
Should fix #4_error not checked in catch-up render guard
Consider #5JSON.stringify perf/correctness, #6 — NuGet.config, #7 — version rationale
Nit missing EOL, redundant = false

The adoptedStyleSheets design via the shared context is elegant. The #managerObserved guard preventing duplicate observer registration and the blockIndex fix (switching from indexOf(contentData[0]) to findIndex(x => x.key === contentUdi)) are both concrete, correct improvements.

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