UNPKG

closure-builder

Version:

Simple Closure, Soy and JavaScript Build system

168 lines (132 loc) 6.92 kB
# Advanced Java Rendering The `SoySauce` backend for Soy contains some advanced rendering features to help with server stability and performance. This page describes how to use these features. [TOC] ## Rendering goals When rendering on servers there are a number of important things to consider that don't apply when rendering in JavaScript. 1. Documents tend to be larger. Rather than rendering single components Soy is often rendering entire pages 1. Documents should be streamed. Most server side renders produce an HTML document over HTTP. HTTP supports [Chunked Transfer Encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding) which allows servers to stream documents; most Web Browsers can take advantage of this to display partial documents to users (or fetch referenced resources in parallel with document download). 1. Servers often work with data from multiple backends. So the parameters passed to Soy templates may themselves require network operations. 1. Servers are often communicating with slower clients or slow networks. In order to work well in this complex environment, the `SoySauce` backend has the following goals: 1. Stream content from templates as rendering proceeds 1. Detect when the output buffer is too full and 'stop' rendering 1. Detect when rendering requires an unfinished `Future` object and 'stop' rendering 1. Always attempt to make as much progress as possible before reaching ones of these 'stop' conditions In order to achieve all these goals the `SoySauce` backend has implemented a number of unique runtime behaviors. Some of these influence the rendering APIs and others may change behavior in small ways. ### Lazy evaluation {#lazy} In `SoySauce`[^1] all [let](../reference/let.md) and [param](../reference/calls.md#param) commands are evaluated lazily. Specifically, when Soy encounters a `let` instead of calculating the value of the variable immediately[^2] it will generate a [closure](https://en.wikipedia.org/wiki/Closure_\(computer_programming\)) and only evaluate it when it is ultimately needed. This aids performance in a number of ways: 1. Large or complex values are calculated only right before use. So these values spend less time sitting in buffers 2. If the let or param has an asynchronous data dependency then we can delay accessing which might allow the renderer to make more progress than it could otherwise ### Streaming escape directives {#streaming} Soy supports [contextual autoescaping](../concepts/auto-escaping.md). This works by inserting calls to special escaping functions around dynamic content. For example, when you write ```soy <div>{$foo}</div> ``` The compiler rewrites it as ```soy <div>{$foo |escapeHtml}</div> ``` to ensure that the dynamic value `$foo` doesn't inject code. However, `$foo` might be some really large value, or it might be a [lazy](#lazy) value in which case rendering it might require waiting for some asynchronous data. To try to make the maximum amount of progress rendering content Soy has implemented _streaming_ versions of the most common escaping functions. This reduces the amount of data that Soy needs to buffer and ensures that the maximum amount of progress is made before we need to wait for any asynchronous data. ### Asynchronous rendering {#async} The `SoySauce` backend has built in support for responding to asynchronous IO. There are two kinds of asynchronous events that we handle: 1. Users can pass unfinished `java.util.concurrent.Future` objects as template parameters. By doing this users can start rendering before all the required data is available. Due to [streaming](#streaming), page headers can be rendered and flushed to users early which may allow browsers to start displaying a partial page or downloading other resources[^3]. 1. The Soy renderer may write faster than the client can read the data. Soy should avoid blocking on `write` operations or buffering arbitrary amounts of data in memory. This is signaled by the `AdvisingAppendable.softLimitedReached` method on the output stream. Users who want to trigger this behavior will need to supply a custom `AdvisingAppendable` implementation and implement this method. To handle these events the renderer will detect when it is about to evaluate an unfinished future or when the output buffer is full and then pause rendering to return control to the caller. To see how this works, consider this example: ```soy {namespace ns} {template .foo} {@param userName: string} <div> {$userName} </div> {/template} ``` ```java SoySauce soy = ...; WriteContinuation continuation = soy.render("ns.foo") .setData(ImmutableMap.of("userName", fetchUserNameFuture())) .render(outputStream); switch (continuation.result().type()) { case DONE: // do nothing, rendering completed break; case DETACH: // this means rendering stopped because we found a future continueAfter(continuation.result().future(), () -> continuation.continueRender()); break; case LIMITED: // output buffer is full. See below continueAfter(outputStream.isReady(), () -> continuation.continueRender()); break; } ``` In this example, we render the above template by passing it a future for the user name and having it render to an output stream. The `render` method will return a `WriteContinuation` object that can signal the status of the rendering operation. There are 3 different options: 1. `DONE` this means that rendering is complete 1. `DETACH` this means that rendering paused due to an unfinished `Future`. The particular future is available via the `RenderResult.future()` method. 1. `LIMITED` this means that the output stream told us to stop rendering How to handle each event depends stronly on the particular environment of the rendering operation. For example, * If the future is a `ListenableFuture` then a listener could be attached and rendering could continue in the callback. * If the HTTP server you are using supports asynchronous request processing (like the Servlet 3.0 `AsyncContext`), then you could integrate with that to continue your rendering after the future is complete. <br> [^1]: `Tofu` also supports lazy `let` and `param` commands, with largely the same behavior. [^2]: For some 'trivial' `let` or `param` declarations we do calculate them eagerly, but only when we know it is cheap and doesn't have complex data dependencies. [^3]: In `Tofu`, the behavior is trivial, when the renderer comes across an unfinished future, it flushes the output appendable (if it is `Flushable`) and then blocks waiting on the future.