A pre-built Docker image of the ping-play repository
832
The ping-play project brings BigPipe streaming to the Play Framework. It includes tools for a) splitting your pages up into small "pagelets", which makes it easier to maintain large websites, and b) streaming those pagelets down to the browser as soon as they are ready, which can significantly reduce page load time.
To fetch the data for a page, modern apps often have to make requests to multiple remote backend services (e.g. RESTful HTTP calls to a profile service, a search service, an ads service, etc). You then have to wait for all of these remote calls to come back before you can send any data back to the browser. For example, the following screen capture shows a page that makes 6 remote service calls, most of which complete in few hundred milliseconds, but one takes over 5 seconds. As a result, the time to first byte is 5 seconds, during which the user sees a completely blank page:

With BigPipe, you can start streaming data back to the browser without waiting for the backends at all, and fill in the page incrementally as each backend responds. For example, the following screen capture shows the same page making the same 6 remote service calls, but this time rendered using BigPipe. The header and much of the markup is sent back instantly, so time to first byte is 10 milliseconds (instead of 5 seconds), static content (i.e. CSS, JS, images) can start loading right away, and then, as each backend service responds, the corresponding part of the page (i.e. the pagelet) is sent to the browser and rendered on the screen:

To understand how to transform your Play app to use BigPipe, it's helpful to first see an example that does not use BigPipe (note, the example is in Scala, but ping-play supports Java too!). Here is the controller code, controllers/WithoutBigPipe.scala, for the example mentioned earlier:
class WithoutBigPipe(serviceClient: FakeServiceClient) extends Controller {
def index = Action.async { implicit request =>
// Make several fake service calls in parallel to represent fetching data from remote backends. Some of the calls
// will be fast, some medium, and some slow.
val profileFuture = serviceClient.fakeRemoteCallMedium("profile")
val graphFuture = serviceClient.fakeRemoteCallMedium("graph")
val feedFuture = serviceClient.fakeRemoteCallSlow("feed")
val inboxFuture = serviceClient.fakeRemoteCallSlow("inbox")
val adsFuture = serviceClient.fakeRemoteCallFast("ads")
val searchFuture = serviceClient.fakeRemoteCallFast("search")
// Wait for all the remote calls to complete
for {
profile <- profileFuture
graph <- graphFuture
feed <- feedFuture
inbox <- inboxFuture
ads <- adsFuture
search <- searchFuture
} yield {
// Render the template once all the data is available
Ok(views.html.withoutBigPipe(profile, graph, feed, inbox, ads, search))
}
}
}
This controller makes 6 remote service calls, gets back 6 Future objects, and when they have all redeemed, it uses
them to render the following template, views/withoutBigPipe.scala.html:
@(profile: data.Response, graph: data.Response, feed: data.Response, inbox: data.Response, ads: data.Response, search: data.Response)
<html>
<head>
<link rel="stylesheet" href="/assets/stylesheets/main.css">
</head>
<body>
<h1>Without Big Pipe</h1>
<table class="wrapper">
<tr>
<td><div id="profile">@views.html.helpers.module(profile)</div></td>
<td><div id="ads">@views.html.helpers.module(ads)</div></td>
<td><div id="feed">@views.html.helpers.module(feed)</div></td>
</tr>
<tr>
<td><div id="search">@views.html.helpers.module(search)</div></td>
<td><div id="inbox">@views.html.helpers.module(inbox)</div></td>
<td><div id="graph">@views.html.helpers.module(graph)</div></td>
</tr>
</table>
</body>
</html>
When you load this page, nothing will show up on the screen until all of the backend calls complete, which will take about 5 seconds.
To transform this page to use BigPipe, you first add the big-pipe dependency to your build (note, this project requires Play 2.4, Scala 2.11.6, SBT 0.13.8, and Java 8):
libraryDependencies += "com.ybrikman.ping" %% "big-pipe" % "0.0.13"
Next, add support for the .scala.stream template type and some imports for it to your build:
TwirlKeys.templateFormats ++= Map("stream" -> "com.ybrikman.ping.scalaapi.bigpipe.HtmlStreamFormat"),
TwirlKeys.templateImports ++= Vector("com.ybrikman.ping.scalaapi.bigpipe.HtmlStream", "com.ybrikman.ping.scalaapi.bigpipe._")
Now you can create streaming templates. These templates can mix normal HTML markup, which will be streamed to the
browser immediately, with the HtmlStream class, which is a wrapper for an Enumerator[Html] that will be streamed
to the browser whenever the Enumerator has data. Here is views/withBigPipe.scala.stream,
which is the streaming version of the template above:
@(bigPipe: BigPipe, profile: Pagelet, graph: Pagelet, feed: Pagelet, inbox: Pagelet, ads: Pagelet, search: Pagelet)
<html>
<head>
<link rel="stylesheet" href="/assets/stylesheets/main.css">
<!-- You need to include the BigPipe JavaScript at the top of the page -->
<script src="/assets/com/ybrikman/ping/big-pipe.js"></script>
</head>
<body>
<h1>With Big Pipe</h1>
@HtmlStream.fromHtml(views.html.helpers.timing())
<!--
Wrap the entire body of your page with a bigPipe.render call. The pagelets parameter contains a Map from
Pagelet id to the HtmlStream for that Pagelet. You should put the HtmlStream for each of your Pagelets
into the appropriate place in the markup.
-->
@bigPipe.render { pagelets =>
<table class="wrapper">
<tr>
<td>@pagelets(profile.id)</td>
<td>@pagelets(ads.id)</td>
<td>@pagelets(feed.id)</td>
</tr>
<tr>
<td>@pagelets(search.id)</td>
<td>@pagelets(inbox.id)</td>
<td>@pagelets(graph.id)</td>
</tr>
</table>
}
</body>
</html>
The key changes to notice from the original template are:
BigPipe.render method.BigPipe.render method gives you a parameter, named pagelets in the example above, that is a Map
from Pagelet id to the HtmlStream for that Pagelet. The idea is to place the HtmlStream for each of your
Pagelets into the proper place in the markup where that Pagelet should appear.big-pipe.js in the head of the document.Now, let's look at the controller you can use with this template, called controllers/WithBigPipe.scala:
class WithBigPipe(serviceClient: FakeServiceClient) extends Controller {
def index = Action {
// Make several fake service calls in parallel to represent fetching data from remote backends. Some of the calls
// will be fast, some medium, and some slow.
val profileFuture = serviceClient.fakeRemoteCallMedium("profile")
val graphFuture = serviceClient.fakeRemoteCallMedium("graph")
val feedFuture = serviceClient.fakeRemoteCallSlow("feed")
val inboxFuture = serviceClient.fakeRemoteCallSlow("inbox")
val adsFuture = serviceClient.fakeRemoteCallFast("ads")
val searchFuture = serviceClient.fakeRemoteCallFast("search")
// Convert each Future into a Pagelet which will be rendered as HTML as soon as the data is available
val profile = HtmlPagelet("profile", profileFuture.map(views.html.helpers.module.apply))
val graph = HtmlPagelet("graph", graphFuture.map(views.html.helpers.module.apply))
val feed = HtmlPagelet("feed", feedFuture.map(views.html.helpers.module.apply))
val inbox = HtmlPagelet("inbox", inboxFuture.map(views.html.helpers.module.apply))
val ads = HtmlPagelet("ads", adsFuture.map(views.html.helpers.module.apply))
val search = HtmlPagelet("search", searchFuture.map(views.html.helpers.module.apply))
// Use BigPipe to compose the pagelets and render them immediately using a streaming template
val bigPipe = new BigPipe(PageletRenderOptions.ClientSide, profile, graph, feed, inbox, ads, search)
Ok.chunked(views.stream.withBigPipe(bigPipe, profile, graph, feed, inbox, ads, search))
}
}
The key changes to notice from the original controller are:
Html as soon as
the data is available, giving you a Future[Html].Future[Html], plus the DOM id of where in the DOM it should be inserted, is wrapped in an HtmlPagelet
object.HtmlPagelet objects are composed into a BigPipe object, and told to use client-side rendering.BigPipe instance and all the HtmlPagelet objects are passed to the streaming template for rendering.When you load this page, you will see the outline of the page almost immediately, and each piece of the page will fill in this outline as soon as the corresponding remote service responds.
There are several BigPipe examples, including the one described above, in sample-app-scala and sample-app-java in this repo (yes, BigPipe streaming works with both Scala and Java). You'll also want to browse sample-app-common, which has some code shared by both sample apps, including all of their templates. For example, here is how to run the Scala sample app (assuming you have Typesafe Activator installed already):
git clone this repo.activator shellproject sampleAppScalarunhttp://localhost:9000/withoutBigPipe to see how long the page takes to load without BigPipe streaming.http://localhost:9000/withBigPipe to see how much faster the page loads with BigPipe streaming.Check out the Documentation to see what APIs are available and FAQ to learn more about BigPipe.
BigPipe streaming is supported for both Scala and Java developers.
Scala developers should primarily be using classes in the com.ybrikman.ping.scalaapi package. In particular, use the
com.ybrikman.ping.scalaapi.bigpipe.HtmlPagelet class to wrap your Future[Html] objects as Pagelet objects, and
use the com.ybrikman.ping.scalaapi.bigpipe.BigPipe class to compose and render your Pagelet objects. See
sample-app-scala for examples.
Java developers should primarily be using classes in the com.ybrikman.ping.javaapi package. In particular, use the
com.ybrikman.ping.javaapi.bigpipe.HtmlPagelet class to wrap your and Promise<Html> as Pagelet objects and use the
com.ybrikman.ping.javaapi.bigpipe.BigPipe class to compose and render your Pagelet objects. See
sample-app-java for examples.
Ping-Play supports both client-side and server-side BigPipe streaming. Client-side streaming sends down the pagelets in whatever order they complete and uses JavaScript to insert each pagelet into the correct spot in the DOM. This gives you the fastest possible loading time, but it does add a dependency on JavaScript. For use cases where you want to avoid JavaScript, such as slower browsers or search engine crawlers (i.e. SEO), you can use server-side rendering, which sends all the pagelets down already rendered as HTML and in the proper order. This will have a longer page-load time than client-side rendering, but still much faster than not using BigPipe at all.
The only part of your code that you have to change to switch between server-side and client-side rendering is the
PageletRenderOptions parameter you pass into the BigPipe constructor. Here is an example of how you could check
the User-Agent header and select PageletRenderOptions.ServerSide if you detect GoogleBot and
PageletRenderOptions.ClientSide otherwise:
def index = Action { request =>
// ... fetch data, create pagelets ...
val bigPipe = new BigPipe(renderOptions(request), pagelet1, pagelet2, ...)
// ... render a streaming template ...
}
private def renderOptions(request: RequestHeader): PageletRenderOptions = {
request.headers.get(HeaderNames.USER_AGENT) match {
case Some(header) if header.contains("GoogleBot") => PageletRenderOptions.ServerSide
case _ => PageletRenderOptions.ClientSide
}
}
Play's built-in .scala.html templates are compiled into functions that append together and return Html, which is
just a wrapper for a StringBuilder, and cannot be streamed. This is why this project introduces a new .scala.stream
template that appends together and returns HtmlStream objects, which are a wrapper for an Enumerator[Html], which
can be streamed. Note that this new template type still uses Play's Twirl
template compiler and its syntax. The only things that are different are:
.scala.stream instead of .scala.html.views.stream.XXX instead of
views.html.XXX.Html object (e.g.
Html(someStringWithMarkup)), wrap it in an HtmlStream object (e.g. HtmlStream.fromString(someStringWithMarkup)).HtmlStream object anywhere in the markup of a .scala.stream template and Play will stream the
content down from the HtmlStream's Enumerator whenever the content is available.The last point is how you get BigPipe style streaming. The HtmlStream class has many helper methods to create an
HtmlStream, including fromHtml and fromHtmlFuture, and to compose several streams into one, such as interleave.
Although you can use the HtmlStream class directly, this project also comes with Pagelet and BigPipe classes that
offer a higher level API for working with HtmlStream. The idea is to break your page down into small "pagelets" that
know how to fetch their own data independently and render themselves. For example, you might have one pagelet that
fetches data from a profile service and knows how to render a user's profile, another pagelet that fetches data from an
ads service and knows how to render an ad unit, and so on. For each pagelet, you make your backend calls, get back
some Future (Scala) or Promise (Java) objects, render them into a Future[Html] or Promise<Html>, and then use
new HtmlPagelet(id, future) or new HtmlPagelet(id, promise) to wrap them in a Pagelet class. You can then compose
multiple Pagelet instances together using the BigPipe constructor.
The BigPipe instance you get back has a render method that you use to actually render your pagelets. The render
method processes your Pagelets as necessary for server-side or client-side rendering and gives you a Map from
Pagelet id to the HtmlStream for that Pagelet. In your template, you should extract the HtmlStream for each of
your Pagelets from this map and put it into the proper place in the markup:
@bigPipe.render { pagelets =>
<h2>The foo pagelet should go here</h2>
<div>@pagelets(fooPagelet.id)</div>
<h2>The bar pagelet should go here</h2>
<div>@pagelets(barPagelet.id)</div>
}
When doing server-side rendering, the HtmlStream you get back from the pagelets Map will contain the fully
rendered HTML. When doing client-side rendering, the HtmlStream will instead contain an empty placeholder that looks
something like this:
<div id="foo-pagelet"></div>
The actual content for your Pagelet will be streamed down at the very end (ie, at the bottom of all the markup you
pass to the BigPipe.render method) and it will be wrapped in markup that makes it invisible when it first arrives in
the browser. It will also include some JavaScript that knows how to extract the content and inject it into the right
placeholder in the DOM. This is what allows the pagelets to be sent down in any order, but still render correctly on
the page. The markup sent back by each Pagelet is in
com.ybrikman.bigpipe.pagelet.scala.html
and looks roughly like this:
<code id="pagelet1"><!--Your content--></code>
<script>BigPipe.onPagelet("pagelet1");</script>
The BigPipe.onPagelet method is part of big-pipe.js,
so make sure to include that script on every page.
The BigPipe.onPagelet method will extract the content from the code tag and call BigPipe.renderPagelet to render
it client-side into the DOM node with the specified id (e.g. pagelet1 in the example above). The default
BigPipe.renderPagelet just inserts the content into the DOM using the innerHTML method. If you wish to use a more
sophisticated method for client-side rendering, simply override the BigPipe.renderPagelet with your own:
BigPipe.renderPagelet = function(id, content) {
// Provide a custom way to insert the specified content into the DOM node with the given id
}
The id parameter will be the id of the DOM node and content will be your content. Note that if your content was
JSON instead of HTML, big-pipe.js will automatically call JSON.parse on it before passing it to you. This can be
convenient if you use client-side templating.
You can use a client-side templating technology, such as mustache.js or handlebars.js to render most of your page
in the browser. To do that, all you need to do is create a Pagelet that contains JSON (a JsValue for Scala
developers or JsonNode for Java developers) instead of HTML:
class MoreBigPipeExamples(serviceClient: FakeServiceClient) extends Controller {
/**
* Instead of rendering each pagelet server-side with Play's templating, you can send back JSON and render each
* pagelet with a client-side templating library such as mustache.js
*
* @return
*/
def clientSideTemplating = Action {
// Make several fake service calls in parallel to represent fetching data from remote backends. Some of the calls
// will be fast, some medium, and some slow.
val profileFuture = serviceClient.fakeRemoteCallJsonMedium("profile")
val graphFuture = serviceClient.fakeRemoteCallJsonMedium("graph")
val feedFuture = serviceClient.fakeRemoteCallJsonSlow("feed")
val inboxFuture = serviceClient.fakeRemoteCallJsonSlow("inbox")
val adsFuture = serviceClient.fakeRemoteCallJsonFast("ads")
val searchFuture = serviceClient.fakeRemoteCallJsonFast("search")
// Convert each Future into a Pagelet which will send the JSON to the browser as soon as it's available
val profile = JsonPagelet("profile", profileFuture)
val graph = JsonPagelet("graph", graphFuture)
val feed = JsonPagelet("feed", feedFuture)
val inbox = JsonPagelet("inbox", inboxFuture)
val ads = JsonPagelet("ads", adsFuture)
val search = JsonPagelet("search", searchFuture)
// Use BigPipe to compose the pagelets and render them immediately using a streaming template
val bigPipe = new BigPipe(PageletRenderOptions.ClientSide, profile, graph, feed, inbox, ads, search)
Ok.chunked(views.stream.clientSideTemplating(bigPipe, profile, graph, feed, inbox, ads, search))
}
}
Next, create your custom BigPipe.renderPagelet method:
// Override the original BigPipe.renderPagelet method with one that uses mustache.js for client-side rendering
BigPipe.renderPagelet = function(id, json) {
var domElement = document.getElementById(id);
if (domElement) {
domElement.innerHTML = Mustache.render(template, json);
} else {
console.log("ERROR: cannot render pagelet because DOM node with id " + id + " does not exist");
}
};
See the clientSideTemplating method in
controllers/MoreBigPipeExamples.scala (Scala developers) or
controllers/MoreBigPipeExamples.java (Java developers) and
big-pipe-with-mustache.js for working examples.
TODO: write documentation
If your page is built out of composable, independent pagelets, then each pagelet will know how to fetch all the data it needs from backend services. If each pagelet is truly independent, that means you may have duplicated service calls. For example, several pagelets may make the exact same backend call to fetch the current user's profile. This is inefficient and increases the load on downstream services.
This project comes with a DedupingCache library that makes it easy to de-dupe service calls. You can use it to
ensure that if several pagelets request the exact same data, you only make one call to a backend service, and all the
other calls get the same cached response. This class has a single method called get that takes a key and a way to
generate the value for that key if it isn't already in the cache.
For example, if you are using Play's WSClient to make remote calls, you could wrap any calls to it with this get
method to ensure that any duplicate calls for a given URL get back a cached value:
class ServiceClient {
val cache = new DedupingCache[String, Future[WSResponse]]
def makeRequest(url: String): Future[WSResponse] = {
cache.get(url, wsClient.url(url).get())
}
}
See controllers/Deduping.scala (Scala developers) or
controllers/Deduping.java (Java developers) for a complete example of
how to setup and use the DedupingCache. You will also have to add the CacheFilter to your filter chain, as shown in
loader/PingApplicationLoader.scala (Scala developers) or
loader/Filters.java (Java developers).
BigPipe is not for everyone. There are some serious drawbacks and caveats you should be aware of before using it:
With BigPipe streaming, you typically start sending the response back to the browser before your backend calls are finished. The first part of that response is the HTTP headers and once you've sent them back to the browser, it's too late to change your mind. If one of those backend calls fails, you've already sent your 200 OK, so you can no longer just send the browser a 500 error or a redirect!
Instead, you must handle errors by injecting JavaScript code into your stream that displays the message when it arrives
in the browser or redirects the user as necessary. See the errorHandling method in
controllers/MoreBigPipeExamples.scala (Scala developers) or
controllers/MoreBigPipeExamples.java (Java developers) for
a working example.
Because of the the way headers and error handling work, be extra careful using BigPipe if you cache entire pages, especially at the CDN level. Otherwise, you may stream out a 200 OK to the CDN, hit an error with a backend call, and accidentally end up caching a page with an error on it.
If your pages are mostly static and can be cached for a long time (e.g. blogs), BigPipe is probably not for you. If your pages are mostly dynamic and cannot be cached (e.g. the news feeds at Facebook, LinkedIn, Twitter), then BigPipe can help.
Pagelets can be sent down to the browser and rendered client-side in any order. Therefore, you have to be careful to avoid too much "pop-in", where rendering each pagelet causes random parts of the page to pop in and move around, which makes the page hard to use.
To avoid annoying your users, use CSS to size the placeholder elements appropriately so they don't resize or move
Content type
Image
Digest
sha256:058a1252f…
Size
404.7 MB
Last updated
almost 10 years ago
docker pull brikis98/ping-play