Skip to content

Add a switch to defer rendering of components in ComponentTagHelper#40316

Closed
pranavkm wants to merge 1 commit into
dotnet:mainfrom
pranavkm:prkrishn/deferred-rendering
Closed

Add a switch to defer rendering of components in ComponentTagHelper#40316
pranavkm wants to merge 1 commit into
dotnet:mainfrom
pranavkm:prkrishn/deferred-rendering

Conversation

@pranavkm
Copy link
Copy Markdown
Contributor

@pranavkm pranavkm commented Feb 19, 2022

This allows the HeadOutlet component to appear earlier in the document, but for
rendering to be deferred until MVC starts emitting markup.

The way this is done is by introducing a new parameter to ComponentTagHelper / RenderComponentAsync which returns a IHtmlContent that defers rendering the component until IHtmlContent.WriteTo is called. In practice this looks like:

<head>
     ....
    <component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" defer="true" />
</head>
<body>
    ....
   <component type="typeof(App)" render-mode="ServerPrerendered" />
</body>

This behavior takes advantage of MVC's buffered rendering mechanism. MVC builds a tree of IHtmlContent items and defers calling WriteTo on these until it's ready to go to the wire. In our case, the component tag helper rendering HeadOutlet executes but puts a placeholder since it is deferred. The tag helper rendering App executes, and updates the SectionContent. Once MVC is ready to turn IHtmlContent to text, the HeadOutlet component renders and the title configured by App is used.

The only pre-req is that since rendering of IHtmlContent is synchronous, we require deferred components to also finish rendering synchronously. This isn't an issue with HeadOutlet.

Fixes #38400

@pranavkm pranavkm force-pushed the prkrishn/deferred-rendering branch from 16e72e9 to ebebe23 Compare February 20, 2022 01:20
This allows the HeadOutlet component to appear earlier in the document, but for
rendering to be deferred until MVC starts emitting markup.
@pranavkm pranavkm force-pushed the prkrishn/deferred-rendering branch from ebebe23 to 248f457 Compare February 20, 2022 19:38
@Pilchie Pilchie added the area-blazor Includes: Blazor, Razor Components label Feb 20, 2022
@pranavkm pranavkm marked this pull request as ready for review February 21, 2022 14:26
@pranavkm pranavkm requested review from a team, Pilchie and javiercn as code owners February 21, 2022 14:26

public override string ProjectType { get; } = "blazorserver";

[Theory(Skip = "https://github.com/dotnet/aspnetcore/issues/30761")]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will undo - I wanted to verify if the title in the templates worked correctly

}

<component type="typeof(App)" render-mode="ServerPrerendered" />
<!DOCTYPE html>
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Merged _Host.cshtml and _Layout.cshtml together like pre-6.0 days.


if (!renderTask.IsCompleted)
{
throw new InvalidOperationException("Components with deferred rendering must complete synchronously.");
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll update this to use a resx.

@SteveSandersonMS
Copy link
Copy Markdown
Member

This looks very promising to me. I'm definitely enthusiastic about getting rid of the weird host/layout page split and the nonobvious ordering dependency.

I'm wondering though if it could be simplified further, eliminating the need for the Defer parameter and avoiding any limitation about async rendering. I might be missing something essential but it seems like it might be possible:

How expensive is it to use IHtmlContent? Would it be viable for all component prerendering to go through this? Imagine if:

  1. Every prerendered component wrote its output (asynchronously) to an IHtmlContent.
  2. Each time a prerendered root component re-renders, we'd clear the contents of its IHtmlContent and write the new content (again, asynchronously).
  3. Finally, when MVC wants to emit the page output to the response, it calls WriteTo on all the IHtmlContent instances and they all synchronously flush out their most recent render output

So if rendering App causes the HeadOutlet to re-render (which it does - that's exactly how this whole thing works in non-prerendering cases), then by the time App has finished asynchronously prerendering, we would also have finished asynchronously re-pre-rendering HeadOutlet. This is because RenderRootComponentAsync gives a task that only completes when the whole renderer becomes quiescent (at least I think so).

Is there a fatal flaw in this? Is it cheap enough to buffer each prerendered component like this?

@pranavkm
Copy link
Copy Markdown
Contributor Author

pranavkm commented Mar 3, 2022

Closing in favor of #40512

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.

Investigate not requiring unintuitive requirements for pre-rendering PageTitle

3 participants