spring-projects / spring-framework

Spring Framework
https://spring.io/projects/spring-framework
Apache License 2.0
56.14k stars 37.96k forks source link

Progressive HTML rendering support [SPR-14981] #19547

Open spring-projects-issues opened 7 years ago

spring-projects-issues commented 7 years ago

Sébastien Deleuze opened SPR-14981 and commented

We should provide a way to change flushing strategy without using ServerHttpResponse#writeAndFlushWith(Publisher<Publisher<DataBuffer>>) low level method.

That could be via supporting Publisher<Publisher<?>> return values or introducing a new annotation that could allow to flush the data after each element (could make sense when you serialize POJOs).


Issue Links:

3 votes, 8 watchers

spring-projects-issues commented 7 years ago

Sébastien Deleuze commented

Not sure we should do something here since after #19671 and this commit media type is used to enable flushing by element or not + performances have been improved. Annotation based programming model is quite high level and it is possible to use ServerWebExchange to implement custom low level flushing behavior, so I think it is probably better to wait more feedbacks before eventually doing something.

spring-projects-issues commented 7 years ago

Sébastien Deleuze commented

Notice that I have added an additional commit that enable flushing by element for application/stream+json mime type.

spring-projects-issues commented 6 years ago

Florian Stefan commented

I think it would be really useful to be able to control the flushing behaviour per controller method. A concrete example for this would be "Progressive HTML rendering". The idea is to split up the HTML into chunks and send each chunk to the browser as soon as it is available. So the browser can start rendering the page and also download additional resources without the overhead that AJAX via HTTP /1.1 introduces.

Facebook is calling this "BigPipe". We implemented this using the Play framework (see here for the basic ideas).

It would be really cool to do something similar with Spring 5 and Reactor. Having control over flushing behaviour would definitely help.

spring-projects-issues commented 6 years ago

Dave Syer commented

That's a really useful link (the Play talk). I think it highlights the point that actually, it's not really the controller that needs to know about flushing, but the view renderer. Thymeleaf already has a weak form of this (it's still a bit leaky, and you can only drive the flushing behaviour using a single Publisher). Maybe this is getting off topic from the original issue though (which was more about how to flush responses based on the body type).

spring-projects-issues commented 6 years ago

Sébastien Deleuze commented

We are getting a little bit out of the original topic indeed, but I think that's a good thing to understand the context and various alternatives to correctly evaluate what to do here.

I do think progressive HTML rendering would be a very useful feature. By progressive HTML rendering I mean being able to render sequentially templates based on multiple asynchronous model attributes without waiting the resolution of all these attributes, this is super useful for mobile and keep good SEO while avoiding users to wait few seconds to see anything. The most common use case would likely be request remote JSON HTTP endpoints with WebClient and begin to render and flush the HTML to the client asap the first async model attributes complete. WebFlux has the underlying architecture to support it, but there is some missing pieces when you consider the feature end to end. This is obviously not magic and model attributes usually coming with the more latency should be placed at the end of the template.

As pointed out by Dave Syer, in Spring world this should mainly be solved at view rendering level. Reactive template engine supporting progressive HTML rendering should override AbstractView#resolveAsyncAttributes to avoid pre-resolution of async attributes and pass them as they are to the engine to allow it listening async attributes completion. Since a single stream of HTML is produced, such template engine should determine what is the order or resolution based on the templates. There is currently not yet support for that even in Thymeleaf, so this is open for contribution and experimentation.

That said, it is also possible to implement that in a low level fashion by creating a Reactive pipeline that will call manually various codecs and output a Flux<DataBuffer> and call ServerHttpResponse#writeAndFlushWith(Publisher<Publisher<DataBuffer>>). This would be quite complicated, and I understand here the need to support for example flushing per element when returning Flux<String> (for example when you request remotely rendered webpages.

Arjen Poutsma Rossen Stoyanchev Brian Clozel Do you have any proposal about how we could specify this flushing on next element? Maybe a dedicated annotation or annotation attribute for the annotation programming model and dedicated methods or codec hint for the functional API ?

spring-projects-issues commented 6 years ago

Dave Syer commented

A Rendering kind of has some of the pieces that you need to encapsulate a fragment that needs to be flushed (e.g. comparing with the Play example where sections of the screen were built up and flushed as they became available). Maybe what I need is to be able to populate a Model with attributes of type Publisher<Rendering> (or something), and have them somehow flush the response when they are delivered?

spring-projects-issues commented 6 years ago

Sébastien Deleuze commented

Dave Syer If we are talking about the same use case, ie. being able to assemble fragment of HTML remotely rendered (for example by a remote Markdown rendering webservice or a remote template rendering server), it seems to me Mono<String> model attributes would be enough at least for a start (we can imagine having more feature to escape HTML, etc.). What is missing is "just" a Reactive template technology with a View implementation that overrides AbstractView#resolveAsyncAttributes and build itself the reactive pipeline in the right order based on templates and flush that data after each onComplete event (or onNext if we go as far as supporting rendering multiple Flux<T> like Thymeleaf DATA-DRIVEN mode). Thymeleaf 3.1 could be a good candidate, I will talk to Daniel Fernández about that.

spring-projects-issues commented 6 years ago

Sébastien Deleuze commented

I want also to clarify there are 2 use cases:

I think both are interesting and cover different use cases.

spring-projects-issues commented 6 years ago

Dave Syer commented

I think there is a third possibility which is render a single stream and use JS to position the elements as they arrive (you don't need Ajax or SSE, so there's less code in the browser potentially). I'm interested in what the CSS options are though - maybe I'm already doing it, but with no custom CSS (browser seems more than willing to show partially complete HTML, e.g. table rows as they flush, without necessarily needing to see the end of the table).

FWIW I also don't think Publisher<String> is a very nice model (if you mean that the string is HTML), although it might be a stepping stone on the way. I want to be able to compose server side views with templates.

spring-projects-issues commented 6 years ago

Sébastien Deleuze commented

Dave Syer I don't understand the third way with single stream + JS, could you please give more details about it?

What would be the limitations of Publisher<String> model attributes regarding to server side views with template composition? What do you expect instead?

spring-projects-issues commented 6 years ago

Dave Syer commented

could you please give more details about it?

E.g.

<script id="foo" type="text/html">
// render content here
</script>
<script>
  $("#content").append($("#foo").html())
</script>

What would be the limitations of Publisher\ model attributes

You would have to render all the strings yourself before adding them to the model, whereas Spring has a nice abstraction for doing that via a Rendering.

spring-projects-issues commented 6 years ago

Sébastien Deleuze commented

Thanks for the example of the third way (even if I am not sure that will work).

You would have to render all the strings yourself before adding them to the model, whereas Spring has a nice abstraction for doing that via a Rendering.

I still don't get it, to me there is 2 main use cases:

In both cases the current Rendering is use to pass Mono and Flux model attributes. The missing piece is only the template rendering implementation that will not wait all async attributes to be completed to begin rendering.

Is your use case different?

spring-projects-issues commented 6 years ago

Dave Syer commented

Is your use case different?

I think so, but maybe I'm asking for too much. I'm trying to generalize the Thymeleaf features, and copy the guy using Play from LinkedIn to some extent, so the fact that those two examples exist gives me some confidence that we can do better.

If the remote call returns a Flux<Foo> and it can be rendered as a fragment, incrementally, then the HTTP response can be flushed as Foos arrive, not after they all are ready. Similarly the page might contain content from a Mono<Bar> and a Mono<Spam> and I want to render them as they arrive, as fragments (with a View and everything).

As I already said, I think this will require changes in the view template renderer. But hopefully Spring can provide a framework for doing that in a sensible way.

Remote rendering with a markup service is still a nice option. It just isn't the same, and gets a bit awkward if the rendering is actually not remote. Or maybe not. I'm happy to be proved wrong.

spring-projects-issues commented 6 years ago

Sébastien Deleuze commented

Yeah with Flux<Foo>, Foos could be rendered as they arrive.

Maybe at some point we will be able to provide some higher level constructs to help with that, but not sure since this is tricky and very related to each view rendering technology.

In any case, I still think the first steps are implementing these ideas in one of the template rendering running on top of WebFlux like the upcoming Thymeleaf 3.1, and on our side provide a way to flush data after each onNext in order to allow developers doing powerful things programmatically.

spring-projects-issues commented 6 years ago

Dave Syer commented

So this works quite well already:

@GetMapping("/flux")
@ResponseBody
Mono<Void> flux(ServerWebExchange exchange)
          throws Exception {
     return exchange.getResponse().writeAndFlushWith(Flux
               .just("<html>\n<body>\n", "<h2>Demo</h2>\n", "<span>Hello</span>\n",
                                 "</body></html>\n")
               .delayElements(Duration.ofSeconds(1))
               .map(body -> buffer(exchange, body)));
}
private Publisher<DataBuffer> buffer(ServerWebExchange exchange, String body) {
     return Mono.just(exchange.getResponse().bufferFactory().allocateBuffer()
               .write(body.getBytes()));
}

Kind of rough, but it shows the principle, and the browser renders the page progressively. Question: it doesn't work if you don't end the fragments with \n (only complete lines are flushed). Is that expected? Why?

spring-projects-issues commented 6 years ago

Sébastien Deleuze commented

I guess it depends on browsers DOM rendering implementation and about the DOM structure of the page you flush. On my Ubuntu laptop, your example works both with or without \n with Chrome 61 and never works with Firefox 56.

spring-projects-issues commented 6 years ago

Dave Syer commented

Indeed. I was testing with curl (where the \n seem to be important). Chrome works without them.

spring-projects-issues commented 6 years ago

Daniel Fernández commented

I just want to add my two cents, from the Thymeleaf perspective. Sorry for the length of the text...

First of all and for better context, let me clarify the three operation modes currently offered by Thymeleaf when used with Spring WebFlux:

Note that the flush operations mentioned above are performed by means of using response.writeAndFlushWith(...) at the ThymeleafReactiveView class. If I'm right you mention in this ticket the possibility of providing view engines with some kind of additional capability for flushing output after each onNext() from the underlying data stream, but that's something Thymeleaf already can do so I'm probably missing something... could you please give a bit more detail on what you mean with this?

Also note that, in all of the template modes above, all reactive data stream attributes in the model at the time of view-layer execution (except the data-driver model attribute in DATA-DRIVEN mode) will be resolved by Spring WebFlux by means of AbstractView#resolveAsyncAttributes(...) before Thymeleaf actually starts to execute. So a Thymeleaf view layer can be called with any number of reactive data streams in the model, but only one at most can end up driving the rendering of HTML as its elements are published. The rest of them will be fully resolved before HTML starts being generated at all. This is per-construction in Thymeleaf 3.0.

Unfortunately there has been no time yet to create a proper tutorial explaining all the features of the integration between Thymeleaf and Spring WebFlux, so the available documentation detail is scattered in GitHub tickets and JavaDoc, like ISpringWebFluxTemplateEngine or also my talk last May at Spring I/O: "Getting Thymeleaf Ready for Spring 5 and Reactive"

Now this said, I'd like to talk about the scenarios you describe for UI composition, and how I see them from the Thymeleaf perspective. If I understand correctly, you are talking about three different scenarios that approach the browser-side rendering of a complex HTML interface combining multiple (not just one) reactive data streams, or more specifically combining the fragments of HTML rendered for data coming from multiple reactive data streams.

I stress the fact of having multiple data streams because I believe the "single data stream scenario" is already covered today out-of-the-box by Thymeleaf using the DATA-DRIVEN mode as explained above.

Summary of scenarios being proposed

In summary (and in my understanding), these scenarios would be:

Note I'm discarding from this discussion the scenario with multiple full-JavaScript based requests (one request per async fragment, using AJAX, SSE or even WebSockets), as I believe the key aim here is trying to reduce the total amount of requests required for UI composition.

+Possible implementations based on Thymeleaf+

Let's see this in a bit deeper detail and adding Thymeleaf's current capabilities (version 3.0.8) to the equation. As you will see, the trick here will be that we will try to overcome the limitation of Thymeleaf allowing only one reactive data stream per execution by combining multiple Thymeleaf executions into the same response, one for each fragment. Also, we will not be using ThymeleafReactiveView, so we will be first-hand responsible for composing the response and configuring its flushing behaviour from the controller.

Scenario 1 (one request, with JavaScript)

Scenario 1 would consist of a single request with a text/html response. The first chunk sent to the browser would contain the beginning of the HTML document with: 1. <head>, 2. An unclosed <body> containing the general skeleton of the page including placeholders for all the involved fragments, and 3. Some JavaScript code able to position a fragment of HTML (a DOM node, say, a <div>) in one of the defined placeholders by means of checking its class or id. For the sake of the example, let's call this JS function placeFragment(...).

At the controller (returning Mono<Void>) Thymeleaf –specifically a SpringWebFluxTemplateEngine instance– would be directly called once per UI async fragment, in DATA-DRIVEN mode on the data stream corresponding to each fragment, and each of these calls would return a Flux<DataBuffer>. Each element being published by the data-drivers would make Thymeleaf render a piece of HTML delimited by some kind of container (e.g. a <div> block) with some specific class or id values, and after the <div> container would come a small piece of JavaScript that would call the placeFragment(...) function passing the container's node as argument.

All these Thymeleaf-ised Flux<DataBuffer> objects then can be Flux#merge-d into a new Flux<DataBuffer> that would be able to emit all UI fragments unordered. Then this merged stream can be Flux#concat-ed with two Mono<DataBuffer> objects: one that would previously write a DataBuffer containing the HTML skeleton (plus the code for the placeFragment() JavaScript function) and another one that would close the </body> and </html>. The resulting Flux<DataBuffer> would be used in a response.writeAndFlushWith() call, and that should be it.

All of this is of course a huge simplification, but it would be something similar to:

@RequestMapping("/scenario1")
public Mono<Void> scenario1(final ServerWebExchange exchange) {

    Flux<DataBuffer> asyncHTMLFragment1 = this.thymeleafEngine.process("asyncfragment1", ...);
    Flux<DataBuffer> asyncHTMLFragment2 = this.thymeleafEngine.process("asyncfragment2", ...);
    Flux<DataBuffer> asyncHTMLFragment3 = ...

    Flux<DataBuffer> fragments = Flux.merge(asyncHTMLFragment1, asyncHTMLFragment2, ...);

    Mono<DataBuffer> headerAndSkeleton = ...;
    Mono<DataBuffer> closing = ...;

    Flux<DataBuffer> page = Flux.concat(headerAndSkeleton, fragments, closing);

    return exchange.getResponse().writeAndFlushWith(page.window(1));

}

Things to note:

Scenario 2 (two requests, using SSE)

Scenario 2 would be a bit similar to Scenario 1, but we would have two requests. The first one would have a text/html response consisting of a complete HTML document, containing:

At the controller for the second call (SSE), returning Mono<Void>, Thymeleaf (again a SpringWebFluxTemplateEngine object) can be directly called once per UI fragment, configuring the engine to create SSE events as output. These executions would be in DATA-DRIVEN mode, and when configuring the data-driver variable for each execution, a different SSE prefix would be specified for each one (that's done in the ReactiveDataDriverContextVariable constructor, a new feature in Thymeleaf 3.0.8).

So we would have as a result a number of Flux<DataBuffer> objects that would produce SSE events for each of our reactive data streams, each of them with a different prefix so that they can be easily discriminated at the browser side by the EventSource callback mentioned above.

We would then Flux#merge all these Flux<DataBuffer> and instruct the response to .writeAndFlushWith(...), similar to the previous example:

@RequestMapping("/scenario2")
public Mono<Void> scenario2(final ServerWebExchange exchange) {

    // All these "process" calls will be configured to generate SSE with HTML payload and different prefix
    Flux<DataBuffer> asyncHTMLFragment1 = this.thymeleafEngine.process("asyncfragment1", ...);
    Flux<DataBuffer> asyncHTMLFragment2 = this.thymeleafEngine.process("asyncfragment2", ...);
    Flux<DataBuffer> asyncHTMLFragment3 = ...

    Flux<DataBuffer> fragments = Flux.merge(asyncHTMLFragment1, asyncHTMLFragment2, ...);

    return exchange.getResponse().writeAndFlushWith(fragments.window(1));

}

Things to note:

Scenario 3 (one request, no JavaScript)

Scenario 3 removes JavaScript from the scenario, and so my understanding is that it removes in practice the possibility that fragments can reach the browser unordered. Order will be a must, unless we are able to place absolutely everything in its position by means of mere CSS -- which I'm not completely sure about, but I'm no CSS expert. I assume it will depend on the specifics of the page.

Now, once this limitation is established, this scenario would be somewhat similar to Scenario 1. We would call Thymeleaf multiple times in our controller, Flux#concat-ing the different parts in the right order (header, async fragment 1, inner 1-2 fragment, async fragment 2, ..., footer) and finally we would instruct the response to write and flush on the resulting Flux<DataBuffer>.

The main (and uncomfortable) difference here is that we would have to declare Thymeleaf Flux<DataBuffer> executions (non-DATA-DRIVEN in this case) for each of the non-asynchronous inner fragments of HTML appearing between asynchronous fragments. Not pretty at all, I admit.

And of course we would not have the possibility of allowing all these HTML fragments to be sent to the browser unordered. This is, unless CSS magic allowed this. But this is a limitation posed by the scenario definition itself, not by our use of Thymeleaf.

Our code would look like this. Note how we have header, footer and all the inner fragments in the same template file (base.html), and we take advantage of Thymeleaf's capability to process partial template fragments:

@RequestMapping("/scenario3")
public Mono<Void> scenario3(final ServerWebExchange exchange) {

        // Note header, inner fragments and footer come from the same template file (base.html)

    Flux<DataBuffer> header = this.thymeleafEngine.process("base :: header", ...);  
    Flux<DataBuffer> asyncHTMLFragment1 = this.thymeleafEngine.process("asyncfragment1", ...);
        Flux<DataBuffer> inner12 = this.thymeleafEngine.process("base :: inner12", ...);
    Flux<DataBuffer> asyncHTMLFragment2 = this.thymeleafEngine.process("asyncfragment2", ...);
        Flux<DataBuffer> inner23 = this.thymeleafEngine.process("base :: inner23", ...);
    Flux<DataBuffer> asyncHTMLFragment3 = ...
        ...
        Flux<DataBuffer> footer = this.thymeleafEngine.process("base :: footer", ...);

    Flux<DataBuffer> page = 
                Flux.concat(header, asyncHTMLFragment1, inner12, asyncHTMLFragment2, inner23, ..., footer);

    return exchange.getResponse().writeAndFlushWith(page.window(1));

}

Things to note:

Looking forward (Thymeleaf 3.1)

So in general, as you see, my idea here is that these scenarios could be implemented today with Thymeleaf plus an amount of additional Java/JavaScript application code, but they would not be a general solution or constitute any kind of UI composition framework. Note how we are not using ThymeleafReactiveView in the examples above, so in some sense we are working around the view-layer infrastructure in WebFlux. Which isn't great, but IMHO also not a big deal for special cases like this.

Also, the limitation of only being able to use one data-driver model attribute per DATA-DRIVEN execution could be OK if we can match every one of our async data streams to a different async UI fragment — after all async UI fragments are many times developed as separate HTML templates, so processing them as separate Thymeleaf executions makes sense. But this would definitely be an issue if we need to have other additional async model attributes that we don't want to be resolved before template starts rendering (AbstractView#resolveAsyncAttributes(...)), and which we don't want to give the status of "asynchronous UI fragments" either (so we don't want to make them data-drivers of a separate Thymeleaf execution).

As Sébastien mentioned, this last point is one of the topics Thymeleaf 3.1 will try to focus on: the ability to execute a template without the need resolve all-but-one of the async model attributes before execution, therefore effectively allowing multi-data-driver DATA-DRIVEN execution. And therefore allowing the implementation of Scenario 3 out of the box and in a single Thymeleaf execution.

Note however that development of the 3.1 branch has not started yet :)

spring-projects-issues commented 6 years ago

Dave Syer commented

Thanks, Daniel, that is a wonderful deep summary of all that we left unsaid in the above (with a slant to Thymeleaf of course). It's fantastic that Thymeleaf 3 supports these patterns, and even better that you are thinking of improvements in 3.1.

I share some of the reservations that you express about the current implementation. It feels to me like the controller should not have to know about these things, so I am looking for a solution that can be expressed in the view layer. I am also looking for a toolbox that can be used to extend these features to other template engines (Thymeleaf is excellent, but it's not everyone's choice).

I'm not sure I really understand the all-but-one limitation either, but since that is also a Thymeleaf specific feature we should discuss it in a more specific context.

spring-projects-issues commented 6 years ago

Rossen Stoyanchev commented

I was testing with curl (where the \n seem to be important)

dsyer have you tried with -N (no buffering)?

Note that the flush operations mentioned above are performed by means of using response.writeAndFlushWith(...) at the ThymeleafReactiveView class. If I'm right you mention in this ticket the possibility of providing view engines with some kind of additional capability for flushing output after each onNext() from the underlying data stream, but that's something Thymeleaf already can do so I'm probably missing something...

Indeed I think this whole discussion has little (or nothing) to do with providing more control over flushing. Flushing control makes sense for @ResponseBody-style methods but in the case of a templates, it's the view engine that needs to control flushing and for that the available option on ServerHttpResponse seem fine.

Daniel Fernández, I think in your second example with SSE, couldn't you just make that an @ResponseBody returning a Flux<DataBuffer> or Flux<ServerSentEvent>? It would be a minor simplification indeed, simply avoiding the need to call ServerHttpResponse directly.

Overall what I see is that from a Spring WebFlux perspective we could enable this concept of a view composed of multiple other views (HTML fragments) so that the controller doesn't have to do it all. At a high level we need to return a top-level Rendering, with its own view + model, and then some nested Rendering's each with its own view + model. Then either the ViewResolutionResultHandler could handle this, or perhaps it would be somehow delegated to view technologies that can support this. We need to try out to know more.

spring-projects-issues commented 6 years ago

Daniel Fernández commented

Daniel Fernández, I think in your second example with SSE, couldn't you just make that an @ResponseBody returning a Flux\ or Flux\? It would be a minor simplification indeed, simply avoiding the need to call ServerHttpResponse directly.

In all three scenarios I believe that calls to request.getResponse().writeAndFlush(stream.window(1)) could be replaced by simply returning a Flux<DataBuffer> in a @ResponseBody-annotated controller as long as the higher-level infrastructure applied precisely that behaviour to the returned Flux, i.e. to flush after each of the DataBuffer is produced...

But note that would be Flux<DataBuffer> and not Flux<ServerSentEvent>, as Thymeleaf has its own infrastructure for formatting and outputting SSE events and controlling its own execution by means of monitoring the size of the written output. Also, if I remember correctly when I implemented this some things like applying specific id: or event: fields to the generated SSE messages were not available at the out-of-the-box WebFlux SSE infrastructure. And here we are not talking about SSE events containing JSON, but HTML.