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

Streaming HTML #13

Open
naasking opened this issue Mar 2, 2024 · 7 comments
Open

Streaming HTML #13

naasking opened this issue Mar 2, 2024 · 7 comments
Labels
enhancement New feature or request
Milestone

Comments

@naasking
Copy link

naasking commented Mar 2, 2024

There was an interesting HN post recently on streaming HTML without JavaScript, and it got me thinking about how to flush partially rendered HTML in RazorBlade.

Basically, the idea is that the HTML rendered up to the "FLUSH" marker is sent immediately because the content after might take awhile to render:

<div>
  <template shadowrootmode="open">
    <header>Header</header>
    <main>
      <slot name="content"></slot>
    </main>
    <footer>Footer</footer>
  </template>

  <!-- FLUSH -->
  <div slot="content">
    This div will be rendered inside the slot above. Magic!
  </div>
</div>

If RazorBlade used the output stream directly I could invoke it with a TextWriter whose stream was directly attached to a network socket and then directly call Flush/FlushAsync:

<div>
  <template shadowrootmode="open">
    <header>Header</header>
    <main>
      <slot name="content"></slot>
    </main>
    <footer>Footer</footer>
  </template>

  @(await Output.FlushAsync())

  <div slot="content">
    This div will be rendered inside the slot above. Magic!
  </div>
</div>

But it looks like you first render to a string builder, then write that to the output. I think this is a bit outside of your original use case, but is there a compelling reason to not render directly to the given TextWriter?

@ltrzesniewski
Copy link
Owner

Well, the initial implementation wrote directly to the output writer without intermediate buffering, but I had to change it in v0.5.0 in order to support layouts (where you need to call @RenderBody() in the layout page).

I'm not very happy about that either, but I thought this was good enough for a library like this one - I didn't think anyone would want to stream the output with RazorBlade.

I'll think about how to improve it. In the meantime, maybe you could use v0.4.4 as a workaround if you don't need support for layouts.

@ltrzesniewski ltrzesniewski added the enhancement New feature or request label Mar 3, 2024
@ltrzesniewski ltrzesniewski added this to the v0.6.0 milestone Apr 4, 2024
@ltrzesniewski
Copy link
Owner

I released v0.6.0 which adds a FlushAsync method that works similarly to the one in ASP.NET.

Note that you'll have to write @await FlushAsync() in your template (without the Output), and that this feature is incompatible with layouts (which really do require buffering).

I may add an option to disable buffering and write directly to the output stream, but I'll leave that for a future version.

@carlreinke
Copy link

I had to change it in v0.5.0 in order to support layouts

Assuming you were willing to break compatibility and to drop support for @section, you could implement layouts similar to how RazorSlices did, which doesn't require buffering.

@ltrzesniewski
Copy link
Owner

Well, I'm not willing to remove a feature such as layouts, but I'm still not happy about how it forced me to buffer contents. I think I'll add an option to add streaming back - the latest changes should make that easier.

I'm not familiar with RazorSlices do I don't understand what you mean, I'll take a look.

@ltrzesniewski
Copy link
Owner

Ok I've read the RazorSlices code and I see what you meant by having to drop support for @section. They had to replace those with a virtual method because in their code it's the layout that initiates the inner page execution from RenderBodyAsync, and the Razor generator emits code which defines @section content in ExecuteAsync (so potentially too late).

I won't drop support for @section in RazorBlade, as I prefer to keep the features and overall rendering approach closer to what ASP.NET MVC does, but I can still break compatibility since the library is at version 0, so let's explore some options.

Basically, RazorBlade needs to know if there's a layout before it emits anything to Output. Several approaches are possible:

  1. I like RazorSlice's @implements IUsesLayout<_Layout> idea, and could do something similar in RazorBlade, which would require you to implement a CreateLayout() method in a @functions block instead of the Layout property. If your template implements IUsesLayout, it won't be streamable, otherwise streaming would be enabled by default.

  2. Similarly, a virtual CreateLayout() method which returns null could be provided in the base class. If you override it, it means you use a layout, and if you don't override it then streaming would be enabled. That wouldn't require an @implements directive.

  3. There could be two base classes: a streamable version without a Layout property, and one that supports layouts. But that could be confusing and cause code duplication.

  4. The Render/RenderAsync methods could get an additional overload which would accept some kind of RenderOptions object, which would have a bool StreamOutput property or similar. That wouldn't break compatibility, but the option would have to be off by default.

  5. You may call some EnableStreaming() method first thing in your template. Fun fact: I suppose this could already work in the current version if you call PushWriter(Output); before emitting output 😅

So here are my first thoughts. Which would be your preferred solution, or do you have other ideas?

@ltrzesniewski ltrzesniewski reopened this Jan 22, 2025
@carlreinke
Copy link

1 sounds fine to me. Presumably it would be possible to build your own RazorSlices-style streaming layout mechanism on top of it (by not implementing the interface) if you wanted to.

@ltrzesniewski
Copy link
Owner

I think I'll go with something closer to 2, which is essentially a simplified version of 1. When rendering starts, an equivalent of Layout = CreateLayout() would be run before the execution of the generated code, and streaming would be enabled if Layout (which would become read-only) is null.

That would be pretty straightforward, wouldn't require an additional interface, and streaming would be enabled by default unless there's a layout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants