dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.46k stars 10.03k forks source link

Blazor component with flickering but only in MAUI #49755

Open pekspro opened 1 year ago

pekspro commented 1 year ago

Is there an existing issue for this?

Describe the bug

I assume this is a bug, but I’m not sure.

I have a Blazor component that works fine both in WebAssembly and SignalR in all browsers I have tested. It’s a simple component where the user creates a stack of images.

But if I use it in MAUI, it flickers. Here is a sample sequence with four screenshots:

image

At (1) and (2) an image is added without any issue. But when the third image is added, it is a short flickering where you briefly see stage (2.5). Maybe this is how Blazor apply changes to HTML, but I only see this in MAUI. Tested both in Windows and Android. Tested also with .NET 7 and .NET 8 preview 6. Using the component with SignalR and WebAssembly works perfectly.

Expected Behavior

I would expect the component to be updated the same way no matter where the component is executed.

Steps To Reproduce

Here is a full repro: https://github.com/pekspro/MauiIssues/tree/49755_Blazor_component_with_flickering_in_MAUI

And this is the code for the component: https://github.com/pekspro/MauiIssues/blob/49755_Blazor_component_with_flickering_in_MAUI/MauiIssues/RazorClassLibrary1/AnimationComponent.razor

The rendered HTML code for stage 2 is:

<div style="position: relative; border: 1px solid black; height: 240px; width: 40px;" b-yvih2n3an4="">
    <div class="box type1 row4" b-yvih2n3an4="">
        <img class="marble" src="_content/RazorClassLibrary1/marble-2.svg" b-yvih2n3an4="">
    </div>
    <div class="box type0 row5" b-yvih2n3an4="">
        <img class="marble" src="_content/RazorClassLibrary1/marble-1.svg" b-yvih2n3an4="">
    </div>
</div>

And stage 3:

<div style="position: relative; border: 1px solid black; height: 240px; width: 40px;" b-yvih2n3an4="">
    <div class="box type0 row3" b-yvih2n3an4="">
        <img class="marble" src="_content/RazorClassLibrary1/marble-1.svg" b-yvih2n3an4="">
    </div>
    <div class="box type1 row4" b-yvih2n3an4="">
        <img class="marble" src="_content/RazorClassLibrary1/marble-2.svg" b-yvih2n3an4="">
    </div>
    <div class="box type0 row5" b-yvih2n3an4="">
        <img class="marble" src="_content/RazorClassLibrary1/marble-1.svg" b-yvih2n3an4="">
    </div>
</div>

Note that there are just three lines that need to be added. I don’t understand how step 2.5 is generated.

Exceptions (if any)

No response

.NET Version

7.0.306 and 8 preview 6

Anything else?

The flickering is more noticeable in slow computers. If I replace the image with text, I also don’t see any flickering. So, I assume that the browser is partly involved in this.

ghost commented 1 year ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

pekspro commented 1 year ago

I’ve have updated my sample application with some code that monitors changes in the DOM. I think there are two things that causes this issue:

Inefficient diff

My component adds elements in the beginning of a container. Here is simpler component with the same problem:

<div>
    @for(int i = Count; i > 0; i--)
    {
        <p id="id@(i)">Element @(i)</p>
    } 

    <p>        
        <button class="btn btn-primary" @onclick="() => Count++">Add</button>
    </p>
</div>

@code {
    private int Count = 1;
}

This will make Blazor create the worst possible diff; when an element is added all existing elements will be modified, and then a new element is added in the end of the list. This makes since after having read about Retain element, component, and model relationships in ASP.NET Core Blazor and render tree construction and sequence numbers.

It’s possible to work around this issue with a RenderFragment and taking control of the sequence numbers:

<div>
    <p>
        @CustomRender

        <button class="btn btn-primary" @onclick="() => Count++">Add</button>
    </p>
</div>

@code {
    private int Count = 1;

    protected override void OnInitialized()
    {
        CustomRender = CreateComponent();
    }

    private RenderFragment? CustomRender { get; set; }

    private RenderFragment CreateComponent() => builder =>
    {
        for (int i = Count; i > 0; i--)
        {
            // Using -i as sequence number will make sure the correct diff is generated.
            // Using just i will not work, because the number must be increasing.
            builder.OpenElement(-i, "p");
            builder.AddAttribute(1, "id", $"id{i}");
            builder.AddContent(1, $"Element {i}");
            builder.CloseComponent();
        }
    };

Maui Blazor sends one change at the time to the browser

This is my assumption. Even if a large diff is generated, it still works fine using SignalR or WebAssembly. I verified this by deploying a server on the other side of the world, and there was no flickering in my browser.

But when using MAUI, it feels like one change at a time is sent to the browser and this can cause flickering. Especially if images are involved, I haven’t noticed any flickering when I have used just text. I really hope someone can look at this, it feels like something that can increase performance significantly.

pekspro commented 1 year ago

It looks like this was a pure browser/cache question. I have updated the sample application with JavaScript code that makes the same changes that Blazor do, and this also causes flickering (you can make the flickering more obvious by adding 100 MB of comments in one the the SVG-files). Via the developer tools in the browser/WebView2, you can see that in MAUI, images are downloaded every time an img tag is added or src is changed. But in Edge/Chrome/Firefox they are only downloaded once.

And this is due cache-control setting. In SignalR and WebAssemply version it is set to:

cache-control: no-cache

But in MAUI:

Cache-Control: no-cache, max-age=0, must-revalidate, no-store

In the SignalR version you can the cache-control setting by adding this in Program.cs (and keep the the existing UseStaticFiles call):

app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        if (ctx.File.Name.EndsWith(".svg"))
        {
            ctx.Context.Response.Headers.Append("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store");
        }
    }
});

Then you will have the same flickering in Edge/Chrome.

As far as I know, it’s not possible to change the cache-control setting in MAUI applications (at least I don’t thing a setting for this in WebView2).

ghost commented 10 months ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.