-
Notifications
You must be signed in to change notification settings - Fork 38.4k
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
Progressive HTML rendering support [SPR-14981] #19547
Comments
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 |
Sébastien Deleuze commented Notice that I have added an additional commit that enable flushing by element for |
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. |
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 |
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 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 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 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 ? |
Dave Syer commented A |
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 |
Sébastien Deleuze commented I want also to clarify there are 2 use cases:
I think both are interesting and cover different use cases. |
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 |
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 |
Dave Syer commented
E.g.
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 |
Sébastien Deleuze commented Thanks for the example of the third way (even if I am not sure that will work).
I still don't get it, to me there is 2 main use cases:
In both cases the current Is your use case different? |
Dave Syer commented
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 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. |
Sébastien Deleuze commented Yeah with 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 |
Dave Syer commented So this works quite well already:
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 |
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 |
Dave Syer commented Indeed. I was testing with |
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 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 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 Scenario 1 (one request, with JavaScript) Scenario 1 would consist of a single request with a At the controller (returning All these Thymeleaf-ised 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
At the controller for the second call (SSE), returning So we would have as a result a number of We would then @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, The main (and uncomfortable) difference here is that we would have to declare Thymeleaf 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 ( @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 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 ( 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 :) |
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. |
Rossen Stoyanchev commented
dsyer have you tried with
Indeed I think this whole discussion has little (or nothing) to do with providing more control over flushing. Flushing control makes sense for Daniel Fernández, I think in your second example with SSE, couldn't you just make that an 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 |
Daniel Fernández commented
In all three scenarios I believe that calls to But note that would be |
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
The text was updated successfully, but these errors were encountered: