Skip to content

Document & Media Workspace: Limit route generation (Closes #22910)#23085

Draft
nielslyngsoe wants to merge 3 commits into
v17/devfrom
v17/bugfix/22910-limit-route-generation
Draft

Document & Media Workspace: Limit route generation (Closes #22910)#23085
nielslyngsoe wants to merge 3 commits into
v17/devfrom
v17/bugfix/22910-limit-route-generation

Conversation

@nielslyngsoe
Copy link
Copy Markdown
Member

@nielslyngsoe nielslyngsoe commented Jun 6, 2026

Only generate routes when the dependent data for the routes change.

Fixes #22910

Replaces PR: #22958

No Unit Tests:

This PR has no unit tests, testing this scenario would require quite a bit of mocking, which is hard to maintain and could case tests that parse despite they in real life would not.

As Claude put it:
the test is probably not worth it, and it's likely to cause friction later

Test Notes:

See issue for aspects to test.

@nielslyngsoe nielslyngsoe changed the base branch from main to v17/dev June 6, 2026 20:02
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 6, 2026

@AndyButland
Copy link
Copy Markdown
Contributor

AndyButland commented Jun 7, 2026

This looks to be OK to me @nielslyngsoe. I've tested it using a custom ISegmentService to generate 150 segments:

using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;

namespace Umbraco.Cms.Web.UI.Custom.Segments;

public class MySegmentComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.Services.AddUnique<ISegmentService, MySegmentService>();
        builder.Services.Configure<SegmentSettings>(settings => settings.Enabled = true);
    }
}

public class MySegmentService : ISegmentService
{
    // High segment count to reproduce the N² split-view route explosion from issue #22910.
    // Variant options ≈ cultures × (1 + SegmentCount); the workspace editor generates that count
    // squared in routes. 150 segments on a single culture (~22,800 routes) makes the per-keystroke
    // freeze clearly visible.
    private const int SegmentCount = 150;

    private readonly Segment[] _segments =
        Enumerable.Range(1, SegmentCount)
            .Select(i => new Segment { Alias = $"segment-{i:D3}", Name = $"Segment {i:D3}" })
            .ToArray();

    public Task<Attempt<PagedModel<Segment>?, SegmentOperationStatus>> GetPagedSegmentsAsync(int skip = 0, int take = 100)
        => Task.FromResult
        (
            Attempt.SucceedWithStatus<PagedModel<Segment>?, SegmentOperationStatus>
            (
                SegmentOperationStatus.Success,
                new PagedModel<Segment> { Total = _segments.Length, Items = _segments.Skip(skip).Take(take) }
            )
        );
}

With that in place, when I've opened up a segment variant document there's a very clear lag of several seconds with the code on v17/dev on every key press when you are typing in the name of the document. That's gone with my proposed PR, and also with yours. I also don't see any lag with switching between documents and variants, and the split view displays as expected.

So I'm fine going with this. It's simpler. I wonder if using a dynamic route (as in #22958) is arguably the more correct approach, since it avoids generating static routes for every variant pairing (≈22,800 here, from 151 × 151 variant options), but with your fix in place that generation no longer causes a practical problem.

As far as I can tell, the problematic code is the de-duplication check in umb-router-slot's routes setter at src/Umbraco.Web.UI.Client/src/packages/core/router/route/router-slot.element.ts:30-39. It uses filter + findIndex to detect path-set changes — O(n²).

From the original reporter's scenario, that was ~35k routes, so ~1.25 billion comparisons per keystroke, observed as a ~7-second freeze.

You've effectively fixed this by no longer regenerating the routes on every keystroke (the variant options no longer re-emit when only the document name changes), so the setter is no longer called with a fresh same-length route set — which is the only case that hits the expensive branch. You might consider also improving the algorithm itself, in case it gets called with a large route set again in future:

 	public set routes(value: UmbRoute[] | undefined) {
 		value ??= [];
 		const oldValue = this.#router.routes;
-		if (
-			value.length !== oldValue?.length ||
-			value.filter((route) => oldValue?.findIndex((r) => r.path === route.path) === -1).length > 0
-		) {
+		if (!oldValue || value.length !== oldValue.length) {
+			this.#router.routes = value;
+			return;
+		}
+
+		const oldPaths = new Set(oldValue.map((route) => route.path));
+		if (value.some((route) => !oldPaths.has(route.path))) {
 			this.#router.routes = value;
 		}
 	}

This takes it from O(n²) to O(n), so the comparison cost grows linearly with the route count rather than quadratically. The functionality is unchanged. It's the same "any new path, gated by a length check", just using a Set lookup instead of a per-element findIndex scan.

This is shared router infrastructure rather than part of your change, so it could be a follow-up, but flagging here mainly because this issue is what surfaced it.

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.

Umbraco node name change or first textarea change Umbraco slow loading

2 participants