Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSR as library #46935

Merged
merged 26 commits into from
Mar 3, 2023
Merged

SSR as library #46935

merged 26 commits into from
Mar 3, 2023

Conversation

SteveSandersonMS
Copy link
Member

@SteveSandersonMS SteveSandersonMS commented Feb 28, 2023

This will cover both #38114 and be a basis for passive rendering for Blazor United. It's like the older prerendering logic but tidied up and enabling more control over asynchrony/dispatch.

New public APIs

All new:

namespace Microsoft.AspNetCore.Components.Web;

/// <summary>
/// Provides a mechanism for rendering components non-interactively as HTML markup.
/// </summary>
public sealed class HtmlRenderer : IDisposable, IAsyncDisposable
{
    /// <summary>
    /// Constructs an instance of <see cref="HtmlRenderer"/>.
    /// </summary>
    /// <param name="services">The services to use when rendering components.</param>
    /// <param name="loggerFactory">The logger factory to use.</param>
    public HtmlRenderer(IServiceProvider services, ILoggerFactory loggerFactory)

    /// <inheritdoc />
    public void Dispose()

    /// <inheritdoc />
    public ValueTask DisposeAsync()

    /// <summary>
    /// Gets the <see cref="Components.Dispatcher" /> associated with this instance. Any calls to
    /// <see cref="RenderComponentAsync{TComponent}()"/> or <see cref="BeginRenderingComponent{TComponent}()"/>
    /// must be performed using this <see cref="Components.Dispatcher" />.
    /// </summary>
    public Dispatcher Dispatcher { get; }

    /// <summary>
    /// Adds an instance of the specified component and instructs it to render. The resulting content represents the
    /// initial synchronous rendering output, which may later change. To wait for the component hierarchy to complete
    /// any asynchronous operations such as loading, use <see cref="HtmlComponent.WaitForQuiescenceAsync"/> before
    /// reading content from the <see cref="HtmlComponent"/>.
    /// </summary>
    /// <param name="componentType">The component type. This must implement <see cref="IComponent"/>.</param>
    /// <param name="parameters">Parameters for the component.</param>
    /// <returns>An <see cref="HtmlComponent"/> instance representing the render output.</returns>
    public HtmlComponent BeginRenderingComponent<TComponent>() where TComponent : IComponent
    public HtmlComponent BeginRenderingComponent<TComponent>(ParameterView parameters) where TComponent : IComponent
    public HtmlComponent BeginRenderingComponent(Type componentType)
    public HtmlComponent BeginRenderingComponent(Type componentType, ParameterView parameters)

    /// <summary>
    /// Adds an instance of the specified component and instructs it to render, waiting
    /// for the component hierarchy to complete asynchronous tasks such as loading.
    /// </summary>
    /// <typeparam name="TComponent">The component type.</typeparam>
    /// <returns>A task that completes with <see cref="HtmlComponent"/> once the component hierarchy has completed any asynchronous tasks such as loading.</returns>
    public Task<HtmlComponent> RenderComponentAsync<TComponent>() where TComponent : IComponent
    public Task<HtmlComponent> RenderComponentAsync<TComponent>(ParameterView parameters) where TComponent : IComponent
    public Task<HtmlComponent> RenderComponentAsync(Type componentType)
    public Task<HtmlComponent> RenderComponentAsync(Type componentType, ParameterView parameters)
}

/// <summary>
/// Represents the output of rendering a component as HTML. The content can change if the component instance re-renders.
/// </summary>
public sealed class HtmlComponent
{
    /// <summary>
    /// Gets an instance of <see cref="HtmlComponent"/> that produces no content.
    /// </summary>
    public static HtmlComponent Empty { get; }

    /// <summary>
    /// Obtains a <see cref="Task"/> that completes when the component hierarchy has completed asynchronous tasks such as loading.
    /// </summary>
    /// <returns>A <see cref="Task"/> that completes when the component hierarchy has completed asynchronous tasks such as loading.</returns>
    public Task WaitForQuiescenceAsync()

    /// <summary>
    /// Returns an HTML string representation of the component's latest output.
    /// </summary>
    /// <returns>An HTML string representation of the component's latest output.</returns>
    public string ToHtmlString()

    /// <summary>
    /// Writes the component's latest output as HTML to the specified writer.
    /// </summary>
    /// <param name="output">The output destination.</param>
    public void WriteHtmlTo(TextWriter output)
}

Added:

namespace Microsoft.AspNetCore.Components.Infrastructure;

public class ComponentStatePersistenceManager
{
     public Task PersistStateAsync(IPersistentComponentStateStore store, Renderer renderer)
+    public Task PersistStateAsync(IPersistentComponentStateStore store, Dispatcher dispatcher)
}

This addition is because the prerendering code no longer has a Microsoft.AspNetCore.Components.RenderTree.Renderer instance (because we don't want to expose the renderer internals), but it does have the associated Dispatcher which is all that ComponentStatePersistenceManager actually needs. We could mark the older PersistStateAsync API as obsolete but I don't know whether that would accomplish much. Opinions welcome.

Performance

This PR removes one of the layers of buffering in the output. It no longer prerenders into a ViewBuffer, but instead supplies an IHtmlContent object that writes the components' HTML directly from the RenderTreeFrame buffer to the response's TextWriter. On the surface that sounds likely to improve perf (less work, less copying going on), but it still warrants verifying. Using this new benchmark, running in the perf infrastructure, I got the following results:

Before After
Win Lin Win Lin
Req/sec 36161 29700 37478 30444
35890 29470 37342 30527
36158 29531 37382 30584
Average: 36070 29567 37401 30518
Req/sec (max) 44599 39811 48218 40409
46200 40413 43756 55715
50306 56678 53857 52089
Average: 47035 45634 48610 49404

Conclusion: This PR has a marginal positive effect on performance, but only in the region of 3%. That's fine since the point of this PR isn't about perf but rather is to add a new public API and prepare us for the new Blazor United SSR features.

@ghost ghost added the area-blazor Includes: Blazor, Razor Components label Feb 28, 2023
@SteveSandersonMS SteveSandersonMS force-pushed the stevesa/ssr-as-library branch 2 times, most recently from 34b5b70 to 7a48877 Compare March 1, 2023 12:30
@SteveSandersonMS SteveSandersonMS marked this pull request as ready for review March 2, 2023 12:35
@SteveSandersonMS SteveSandersonMS requested review from a team as code owners March 2, 2023 12:35
@SteveSandersonMS SteveSandersonMS added this to the 8.0-preview3 milestone Mar 2, 2023
@SteveSandersonMS SteveSandersonMS added the api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews label Mar 3, 2023
@ghost
Copy link

ghost commented Mar 3, 2023

Thank you for your API proposal. I'm removing the api-ready-for-review label. API Proposals should be submitted for review through Issues based on this template.

@ghost ghost removed the api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews label Mar 3, 2023
@SteveSandersonMS
Copy link
Member Author

API proposal at #47018

/// <param name="store">The <see cref="IPersistentComponentStateStore"/> to restore the application state from.</param>
/// <param name="dispatcher">The <see cref="Dispatcher"/> corresponding to the components' renderer.</param>
/// <returns>A <see cref="Task"/> that will complete when the state has been restored.</returns>
public Task PersistStateAsync(IPersistentComponentStateStore store, Dispatcher dispatcher)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think we need this API. We are the only ones that can provide a ComponentStatePersistenceManager to the system. Or am I missing something here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a layering thing. The existing caller is in Microsoft.AspNetCore.Mvc.TagHelpers and it can't see the internals of M.A.Components. It's same reason that you added the other PersistStateAsync overload before.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants