microsoft / fluentui-blazor

Microsoft Fluent UI Blazor components library. For use with ASP.NET Core Blazor applications
https://www.fluentui-blazor.net
MIT License
3.3k stars 305 forks source link

feat: make TreeView scalable #1855

Closed verdie-g closed 1 month ago

verdie-g commented 1 month ago

🙋 Feature Request

Making the building of a large TreeView (thousands of nodes) and expanding a node fast.

🤔 Expected Behavior

Building a tree or expanding a node doesn't affect the user experience.

😯 Current Behavior

It can take up to 20 seconds to render the tree and a second to expand a node.

💁 Possible Solution

Maybe there are some simple optimizations to apply. Otherwise could virtualizing the tree help?

🔦 Context

I'm using TreeView to display the merged stack traces of a nettrace file. You can run this project https://github.com/verdie-g/dotnet-event-viewer/tree/d69dd086bd52de858d1fb57826e598be6fea2d87, feed it a nettrace file from a large program. The duration does not matter, it's the number of unique stack traces that matters. Then navigate to the tree view page.

image

vnbaaij commented 1 month ago

Hi,

Can you please supply such a file. I have no exerience with nettrace files. Thanks.

Virtualization is most probable not going to work. It reu=quires every item to be of the same height so it can calculate what to show/load etc. With a nested structure, such as the TreeView, that is only the case when tree items are collapsed. Once you expand an item, the height changes and scrolling the virtualized 'list' will go wrong.

verdie-g commented 1 month ago

Here is a nettrace file https://we.tl/t-2XFA8WDqyO. On my computer, on development mode, building the tree takes 17 seconds, and ~1 second to expand a node.

verdie-g commented 1 month ago

It seems like selecting an TreeItem is also very slow in that case.

verdie-g commented 1 month ago

image That's the call tree when building the TreeView. It seems like most of the time is spent in fluentui. Maybe I should ask for some help there.

vnbaaij commented 1 month ago

That makes sense, yes. Please do.

verdie-g commented 1 month ago

It seems like selecting an TreeItem is also very slow in that case.

About that one, I may have a lead.

<FluentTreeView>
    @RenderCallTreeNode()
</FluentTreeView>

@code
{
    private static RenderFragment RenderCallTreeNode(int depth = 2)
    {
        return builder =>
        {
            if (depth == 2)
            {
                Console.WriteLine("Hello");
            }

            builder.OpenComponent<FluentTreeItem>(0);
            builder.AddAttribute(1, nameof(FluentTreeItem.Text), depth.ToString());

            if (depth > 0)
            {
                builder.AddAttribute(2, nameof(FluentTreeItem.ChildContent), (RenderFragment)(b =>
                {
                    RenderCallTreeNode2(depth - 1)(b);
                }));
            }

            builder.CloseComponent();
        };
    }

}

If you click on any item of the tree, the log "Hello" will be printed twice. I suppose that means the tree gets rendered again? Could you help me understand if that's an issue with fluentui-blazor before I open an issue in fluentui?

vnbaaij commented 1 month ago

In your code you have RenderCallTreeNode2 but I assume that should be without the '2', right?

I see it is indeed called twice but that is just because of the recursiveness in the RenderCallTreeNode, I believe.

If I change the code to

<FluentTreeView>
    <FluentTreeItem Id="i2" SelectedChanged="@((x)=>Console.WriteLine($"Hello from 2 {x}"))">2
        <FluentTreeItem Id="i1" SelectedChanged="@((x)=>Console.WriteLine($"Hello from 1 {x}"))">1
            <FluentTreeItem Id="i0" SelectedChanged="@((x)=>Console.WriteLine($"Hello from 0 {x}"))">0
            </FluentTreeItem>
        </FluentTreeItem>
    </FluentTreeItem>
</FluentTreeView>

Expanding and clicking on item 0 and then 1 gives me: Hello from 0 True Hello from 0 False Hello from 1 True

Which looks good to me

verdie-g commented 1 month ago

But does it make sense that the RenderFragment of RenderCallTreeNode is called again when selecting an item? I would expect this method to be called only once. For example if instead of selecting an item, I expand it, the log is not emitted. So it really sounds like the tree is being re-rendered whenever an item is selected.

In your example, the log is emitted in the SelectedChanged callback which is not my concern here.

vnbaaij commented 1 month ago

If you select an item (with your recursive code), the web component will fire a 'selectedchance' event which will land in the FluentTreeView code (@onselectedchange="@HandleCurrentSelectedChangeAsync". Because of the way Blazor +handles such events a re-render will take place. It fires twice I think because of 1 time for de-selection and the other time for selection.

verdie-g commented 1 month ago

Because of the way Blazor +handles such events a re-render will take place

Thanks, that was I was missing. In my case it sounds like this could be an important performance bottleneck :(

vnbaaij commented 1 month ago

I think the size of the files you are handling requires some pupose build component.

The library's treeview does not know how to load nodes on demand. Because of what I described earlier about the Virtualization, that is not an option either.

vnbaaij commented 1 month ago

I'm closing this as we are not going to be able to provide a way of working with such large data sets. With the current version of the web components, we cannot build a load-on-demand kind of functionality. As the team is workig on the next versin of the web components, I do not see them spend time on this for this version.

dvoituron commented 1 month ago

@verdie-g We are working on this PR #1945. Could it solve your problems. Any comments or details to add?