Añadir nuevo comentario

Cheap Pipe (sort of BigPipe) in Drupal 7

Difficulty: 
Let's Rock

This post is on how we implemented a simple (yet effective) BigPipe "like" rendering strategy for Drupal 7.

Why is big pipe so important?

Big pipe is a render strategy that assumes that not all the parts of your page are equally important, and that a loaded delay on some of them is acceptable as long as the "core" of the page is delivered ASAP. Furthermore, instead of delivering those delayed pieces with subsequent requests (AJAX) it optimizes network load by using a streamed HTTP response so that you get all those delayed pieces in a single HTTP request/response.

Big pipe does not reduce server load, but dramatically improves your website load time if properly integrated with your application.

Sounds great, and will work very well on some scenarios.

Take for example this landing page (excuses for the poor UX, that's a long story...):

This page has about 20 views and blocks. All of those views and blocks are cached, but can you imagine what a cold cache render of that page looks like? A nightmare....

What if we decided that only 4 of those views were critical to the page, and that the rest of the content could be streamed to the user after the page has loaded? It willl roughly load 70% faster.

UPDATE: Adding support for content streaming has oppened the door to awesome succesfull business strategies - without penalizing initial page load times - such as geolocalizing (or even customizing per user) blocks, advertising and others. All of that while keeping page cache turned on and being able to handle similar amounts of traffic on the same hardware, and without resorting to custom Ajax loading (and coding).

We decided to take a shot and try to implement a big-pipe like render strategy for Drupal 7. We are NOT trying to properly do BigPipe, just something EASY and CHEAP to implement and with little disruption of current core - that's why this is going to be dubbed Cheap Pipe instead of Big Pipe.

Furthermore, it was a requirement that this can be leveraged on any current Drupal application without modifying any existing code. It should be as easy as going to a block or view settings and telling the system to stream it's contents. It should also provide programmatic means of defining content callbacks (placeholders) that should be streamed after the page is served.

We made it, and it worked quite well!

Now every block has a "Cheap Pipe" rendering strategy option:

 

Where:

  • None: Block is rendered as usual.
  • Only Get: Cheap pipe is used only on GET requests
  • Always: Cheap pipe is used on GET/POST and other HTTP methods.

Cheap pipe is never used on AJAX requests no matter what you choose here.

Why these options? Because some blocks might contain logic that could missbehave depending on the circumstances, and we want to break nothing. So you choose what blocks should be cheap piped, how, and in what order.

What happens after you tell a block (the example is for blocks but there is an API to leverage this on any rendered thing) to be cheap-piped?

  • The block->view() callback is never triggered and the block is not renderd but replaced with a placeholder.
  • The page is served (flushed to the user) and becomes fully functional by artificially trigerring the $(document).ready() event. The </body></html> tags a removed before serving so that the rest of the streamed content is properly formed.
  • After the page has been served to the user, all deferred content is streamed by flushing the php buffer as content gets created and rendered.
  • This content is sent to the user in such a way that it leverages the Drupal AJAX  framework (although this is not AJAX) so that every new content that reaches the page gets properly loaded (drupal behaviours attached, etc...)

Take a look at this end-of-page sample:

The output markup even gives you some stats to see what time it took to render each Cheap Piped piece of content.

Because cheap piped elements are generated sequentially, if an element is slow, it will delay the rendering of the rest of the elements. That's why we implemented a "weight" property so that you can choose in what order elements are cheap-piped.

What kind of problems did we find?

  • Deferred content that called drupal_set_message() was, obviously, not working because the messages had already been processed and rendered. Solved by converting the messages rendering into Cheap Pipe and making it the last one to be processed (thanks to the weight property).
  • Deferred content that relied on drupal_goto() (such as forms in blocks) would not work because the page had already been served to the user. drupal_goto() had to be modified so that if cheap pipe rendering had already started, the redirection was done client side with javascript.
  • When fatals are thrown after the main content has been served your page gets stuck in a weird visual state. There is nothing we can do about this because after fatals you loose control of php output.
  • Server load sky rocketed. What used to be anonymous pages served from cache, now require a full Drupal bootstrap to serve out the streamed content.