diff --git a/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php b/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php index f8cef1077f10fa52a1866f7be24e507a547ab64f..0bccf0fa68507e5fd7ee7fdf9806f7fd092164b0 100644 --- a/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php +++ b/core/modules/big_pipe/src/EventSubscriber/HtmlResponseBigPipeSubscriber.php @@ -3,7 +3,7 @@ namespace Drupal\big_pipe\EventSubscriber; use Drupal\Core\Render\HtmlResponse; -use Drupal\big_pipe\Render\BigPipeInterface; +use Drupal\big_pipe\Render\BigPipe; use Drupal\big_pipe\Render\BigPipeResponse; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; @@ -12,7 +12,7 @@ /** * Response subscriber to replace the HtmlResponse with a BigPipeResponse. * - * @see \Drupal\big_pipe\Render\BigPipeInterface + * @see \Drupal\big_pipe\Render\BigPipe * * @todo Refactor once https://www.drupal.org/node/2577631 lands. */ @@ -21,17 +21,17 @@ class HtmlResponseBigPipeSubscriber implements EventSubscriberInterface { /** * The BigPipe service. * - * @var \Drupal\big_pipe\Render\BigPipeInterface + * @var \Drupal\big_pipe\Render\BigPipe */ protected $bigPipe; /** * Constructs a HtmlResponseBigPipeSubscriber object. * - * @param \Drupal\big_pipe\Render\BigPipeInterface $big_pipe + * @param \Drupal\big_pipe\Render\BigPipe $big_pipe * The BigPipe service. */ - public function __construct(BigPipeInterface $big_pipe) { + public function __construct(BigPipe $big_pipe) { $this->bigPipe = $big_pipe; } @@ -102,8 +102,8 @@ public function onRespond(FilterResponseEvent $event) { * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event * A response event. * - * @return \Drupal\big_pipe\Render\BigPipeInterface - * A BigPipe service. + * @return \Drupal\big_pipe\Render\BigPipe + * The BigPipe service. */ protected function getBigPipeService(FilterResponseEvent $event) { return $this->bigPipe; diff --git a/core/modules/big_pipe/src/Render/BigPipe.php b/core/modules/big_pipe/src/Render/BigPipe.php index 9337234337f031f66364cd8a87455f4e2dd88721..976d622e658826c25f25ce12eff39a39f3e29882 100644 --- a/core/modules/big_pipe/src/Render/BigPipe.php +++ b/core/modules/big_pipe/src/Render/BigPipe.php @@ -21,9 +21,133 @@ use Symfony\Component\HttpKernel\KernelEvents; /** - * The default BigPipe service. + * Service for sending an HTML response in chunks (to get faster page loads). + * + * At a high level, BigPipe sends a HTML response in chunks: + * 1. one chunk: everything until just before </body> — this contains BigPipe + * placeholders for the personalized parts of the page. Hence this sends the + * non-personalized parts of the page. Let's call it The Skeleton. + * 2. N chunks: a <script> tag per BigPipe placeholder in The Skeleton. + * 3. one chunk: </body> and everything after it. + * + * This is conceptually identical to Facebook's BigPipe (hence the name). + * + * @see https://www.facebook.com/notes/facebook-engineering/bigpipe-pipelining-web-pages-for-high-performance/389414033919 + * + * The major way in which Drupal differs from Facebook's implementation (and + * others) is in its ability to automatically figure out which parts of the page + * can benefit from BigPipe-style delivery. Drupal's render system has the + * concept of "auto-placeholdering": content that is too dynamic is replaced + * with a placeholder that can then be rendered at a later time. On top of that, + * it also has the concept of "placeholder strategies": by default, placeholders + * are replaced on the server side and the response is blocked on all of them + * being replaced. But it's possible to add additional placeholder strategies. + * BigPipe is just another placeholder strategy. Others could be ESI, AJAX … + * + * @see https://www.drupal.org/developing/api/8/render/arrays/cacheability/auto-placeholdering + * @see \Drupal\Core\Render\PlaceholderGeneratorInterface::shouldAutomaticallyPlaceholder() + * @see \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface + * @see \Drupal\Core\Render\Placeholder\SingleFlushStrategy + * @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy + * + * There is also one noteworthy technical addition that Drupal makes. BigPipe as + * described above, and as implemented by Facebook, can only work if JavaScript + * is enabled. The BigPipe module also makes it possible to replace placeholders + * using BigPipe in-situ, without JavaScript. This is not technically BigPipe at + * all; it's just the use of multiple flushes. Since it is able to reuse much of + * the logic though, we choose to call this "no-JS BigPipe". + * + * However, there is also a tangible benefit: some dynamic/expensive content is + * not HTML, but for example a HTML attribute value (or part thereof). It's not + * possible to efficiently replace such content using JavaScript, so "classic" + * BigPipe is out of the question. For example: CSRF tokens in URLs. + * + * This allows us to use both no-JS BigPipe and "classic" BigPipe in the same + * response to maximize the amount of content we can send as early as possible. + * + * Finally, a closer look at the implementation, and how it supports and reuses + * existing Drupal concepts: + * 1. BigPipe placeholders: 1 HtmlResponse + N embedded AjaxResponses. + * - Before a BigPipe response is sent, it is just a HTML response that + * contains BigPipe placeholders. Those placeholders look like + * <div data-big-pipe-placeholder-id="…"></div>. JavaScript is used to + * replace those placeholders. + * Therefore these placeholders are actually sent to the client. + * - The Skeleton of course has attachments, including most notably asset + * libraries. And those we track in drupalSettings.ajaxPageState.libraries — + * so that when we load new content through AJAX, we don't load the same + * asset libraries again. A HTML page can have multiple AJAX responses, each + * of which should take into account the combined AJAX page state of the + * HTML document and all preceding AJAX responses. + * - BigPipe does not make use of multiple AJAX requests/responses. It uses a + * single HTML response. But it is a more long-lived one: The Skeleton is + * sent first, the closing </body> tag is not yet sent, and the connection + * is kept open. Whenever another BigPipe Placeholder is rendered, Drupal + * sends (and so actually appends to the already-sent HTML) something like + * <script type="application/vnd.drupal-ajax">[{"command":"settings","settings":{…}}, {"command":…}. + * - So, for every BigPipe placeholder, we send such a <script + * type="application/vnd.drupal-ajax"> tag. And the contents of that tag is + * exactly like an AJAX response. The BigPipe module has JavaScript that + * listens for these and applies them. Let's call it an Embedded AJAX + * Response (since it is embedded in the HTML response). Now for the + * interesting bit: each of those Embedded AJAX Responses must also take + * into account the cumulative AJAX page state of the HTML document and all + * preceding Embedded AJAX responses. + * 2. No-JS BigPipe placeholders: 1 HtmlResponse + N embedded HtmlResponses. + * - Before a BigPipe response is sent, it is just a HTML response that + * contains no-JS BigPipe placeholders. Those placeholders can take two + * different forms: + * 1. <div data-big-pipe-nojs-placeholder-id="…"></div> if it's a + * placeholder that will be replaced by HTML + * 2. big_pipe_nojs_placeholder_attribute_safe:… if it's a placeholder + * inside a HTML attribute, in which 1. would be invalid (angle brackets + * are not allowed inside HTML attributes) + * No-JS BigPipe placeholders are not replaced using JavaScript, they must + * be replaced upon sending the BigPipe response. So, while the response is + * being sent, upon encountering these placeholders, their corresponding + * placeholder replacements are sent instead. + * Therefore these placeholders are never actually sent to the client. + * - See second bullet of point 1. + * - No-JS BigPipe does not use multiple AJAX requests/responses. It uses a + * single HTML response. But it is a more long-lived one: The Skeleton is + * split into multiple parts, the separators are where the no-JS BigPipe + * placeholders used to be. Whenever another no-JS BigPipe placeholder is + * rendered, Drupal sends (and so actually appends to the already-sent HTML) + * something like + * <link rel="stylesheet" …><script …><content>. + * - So, for every no-JS BigPipe placeholder, we send its associated CSS and + * header JS that has not already been sent (the bottom JS is not yet sent, + * so we can accumulate all of it and send it together at the end). This + * ensures that the markup is rendered as it was originally intended: its + * CSS and JS used to be blocking, and it still is. Let's call it an + * Embedded HTML response. Each of those Embedded HTML Responses must also + * take into account the cumulative AJAX page state of the HTML document and + * all preceding Embedded HTML responses. + * - Finally: any non-critical JavaScript associated with all Embedded HTML + * Responses, i.e. any footer/bottom/non-header JavaScript, is loaded after + * The Skeleton. + * + * Combining all of the above, when using both BigPipe placeholders and no-JS + * BigPipe placeholders, we therefore send: 1 HtmlResponse + M Embedded HTML + * Responses + N Embedded AJAX Responses. Schematically, we send these chunks: + * 1. Byte zero until 1st no-JS placeholder: headers + <html><head /><div>…</div> + * 2. 1st no-JS placeholder replacement: <link rel="stylesheet" …><script …><content> + * 3. Content until 2nd no-JS placeholder: <div>…</div> + * 4. 2nd no-JS placeholder replacement: <link rel="stylesheet" …><script …><content> + * 5. Content until 3rd no-JS placeholder: <div>…</div> + * 6. [… repeat until all no-JS placeholder replacements are sent …] + * 7. Send content after last no-JS placeholder. + * 8. Send script_bottom (markup to load bottom i.e. non-critical JS). + * 9. 1st placeholder replacement: <script type="application/vnd.drupal-ajax">[{"command":"settings","settings":{…}}, {"command":…} + * 10. 2nd placeholder replacement: <script type="application/vnd.drupal-ajax">[{"command":"settings","settings":{…}}, {"command":…} + * 11. [… repeat until all placeholder replacements are sent …] + * 12. Send </body> and everything after it. + * 13. Terminate request/response cycle. + * + * @see \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber + * @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy */ -class BigPipe implements BigPipeInterface { +class BigPipe { /** * The BigPipe placeholder replacements start signal. @@ -143,7 +267,15 @@ protected function sendChunk($chunk) { } /** - * {@inheritdoc} + * Sends an HTML response in chunks using the BigPipe technique. + * + * @param \Drupal\big_pipe\Render\BigPipeResponse $response + * The BigPipe response to send. + * + * @internal + * This method should only be invoked by + * \Drupal\big_pipe\Render\BigPipeResponse, which is itself an internal + * class. */ public function sendContent(BigPipeResponse $response) { $content = $response->getContent(); diff --git a/core/modules/big_pipe/src/Render/BigPipeInterface.php b/core/modules/big_pipe/src/Render/BigPipeInterface.php deleted file mode 100644 index 420b5db67ba55d65df2912829ca6d6516e4446c7..0000000000000000000000000000000000000000 --- a/core/modules/big_pipe/src/Render/BigPipeInterface.php +++ /dev/null @@ -1,147 +0,0 @@ -<?php - -namespace Drupal\big_pipe\Render; - -/** - * Interface for sending an HTML response in chunks (to get faster page loads). - * - * At a high level, BigPipe sends a HTML response in chunks: - * 1. one chunk: everything until just before </body> — this contains BigPipe - * placeholders for the personalized parts of the page. Hence this sends the - * non-personalized parts of the page. Let's call it The Skeleton. - * 2. N chunks: a <script> tag per BigPipe placeholder in The Skeleton. - * 3. one chunk: </body> and everything after it. - * - * This is conceptually identical to Facebook's BigPipe (hence the name). - * - * @see https://www.facebook.com/notes/facebook-engineering/bigpipe-pipelining-web-pages-for-high-performance/389414033919 - * - * The major way in which Drupal differs from Facebook's implementation (and - * others) is in its ability to automatically figure out which parts of the page - * can benefit from BigPipe-style delivery. Drupal's render system has the - * concept of "auto-placeholdering": content that is too dynamic is replaced - * with a placeholder that can then be rendered at a later time. On top of that, - * it also has the concept of "placeholder strategies": by default, placeholders - * are replaced on the server side and the response is blocked on all of them - * being replaced. But it's possible to add additional placeholder strategies. - * BigPipe is just another placeholder strategy. Others could be ESI, AJAX … - * - * @see https://www.drupal.org/developing/api/8/render/arrays/cacheability/auto-placeholdering - * @see \Drupal\Core\Render\PlaceholderGeneratorInterface::shouldAutomaticallyPlaceholder() - * @see \Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface - * @see \Drupal\Core\Render\Placeholder\SingleFlushStrategy - * @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy - * - * There is also one noteworthy technical addition that Drupal makes. BigPipe as - * described above, and as implemented by Facebook, can only work if JavaScript - * is enabled. The BigPipe module also makes it possible to replace placeholders - * using BigPipe in-situ, without JavaScript. This is not technically BigPipe at - * all; it's just the use of multiple flushes. Since it is able to reuse much of - * the logic though, we choose to call this "no-JS BigPipe". - * - * However, there is also a tangible benefit: some dynamic/expensive content is - * not HTML, but for example a HTML attribute value (or part thereof). It's not - * possible to efficiently replace such content using JavaScript, so "classic" - * BigPipe is out of the question. For example: CSRF tokens in URLs. - * - * This allows us to use both no-JS BigPipe and "classic" BigPipe in the same - * response to maximize the amount of content we can send as early as possible. - * - * Finally, a closer look at the implementation, and how it supports and reuses - * existing Drupal concepts: - * 1. BigPipe placeholders: 1 HtmlResponse + N embedded AjaxResponses. - * - Before a BigPipe response is sent, it is just a HTML response that - * contains BigPipe placeholders. Those placeholders look like - * <div data-big-pipe-placeholder-id="…"></div>. JavaScript is used to - * replace those placeholders. - * Therefore these placeholders are actually sent to the client. - * - The Skeleton of course has attachments, including most notably asset - * libraries. And those we track in drupalSettings.ajaxPageState.libraries — - * so that when we load new content through AJAX, we don't load the same - * asset libraries again. A HTML page can have multiple AJAX responses, each - * of which should take into account the combined AJAX page state of the - * HTML document and all preceding AJAX responses. - * - BigPipe does not make use of multiple AJAX requests/responses. It uses a - * single HTML response. But it is a more long-lived one: The Skeleton is - * sent first, the closing </body> tag is not yet sent, and the connection - * is kept open. Whenever another BigPipe Placeholder is rendered, Drupal - * sends (and so actually appends to the already-sent HTML) something like - * <script type="application/vnd.drupal-ajax">[{"command":"settings","settings":{…}}, {"command":…}. - * - So, for every BigPipe placeholder, we send such a <script - * type="application/vnd.drupal-ajax"> tag. And the contents of that tag is - * exactly like an AJAX response. The BigPipe module has JavaScript that - * listens for these and applies them. Let's call it an Embedded AJAX - * Response (since it is embedded in the HTML response). Now for the - * interesting bit: each of those Embedded AJAX Responses must also take - * into account the cumulative AJAX page state of the HTML document and all - * preceding Embedded AJAX responses. - * 2. No-JS BigPipe placeholders: 1 HtmlResponse + N embedded HtmlResponses. - * - Before a BigPipe response is sent, it is just a HTML response that - * contains no-JS BigPipe placeholders. Those placeholders can take two - * different forms: - * 1. <div data-big-pipe-nojs-placeholder-id="…"></div> if it's a - * placeholder that will be replaced by HTML - * 2. big_pipe_nojs_placeholder_attribute_safe:… if it's a placeholder - * inside a HTML attribute, in which 1. would be invalid (angle brackets - * are not allowed inside HTML attributes) - * No-JS BigPipe placeholders are not replaced using JavaScript, they must - * be replaced upon sending the BigPipe response. So, while the response is - * being sent, upon encountering these placeholders, their corresponding - * placeholder replacements are sent instead. - * Therefore these placeholders are never actually sent to the client. - * - See second bullet of point 1. - * - No-JS BigPipe does not use multiple AJAX requests/responses. It uses a - * single HTML response. But it is a more long-lived one: The Skeleton is - * split into multiple parts, the separators are where the no-JS BigPipe - * placeholders used to be. Whenever another no-JS BigPipe placeholder is - * rendered, Drupal sends (and so actually appends to the already-sent HTML) - * something like - * <link rel="stylesheet" …><script …><content>. - * - So, for every no-JS BigPipe placeholder, we send its associated CSS and - * header JS that has not already been sent (the bottom JS is not yet sent, - * so we can accumulate all of it and send it together at the end). This - * ensures that the markup is rendered as it was originally intended: its - * CSS and JS used to be blocking, and it still is. Let's call it an - * Embedded HTML response. Each of those Embedded HTML Responses must also - * take into account the cumulative AJAX page state of the HTML document and - * all preceding Embedded HTML responses. - * - Finally: any non-critical JavaScript associated with all Embedded HTML - * Responses, i.e. any footer/bottom/non-header JavaScript, is loaded after - * The Skeleton. - * - * Combining all of the above, when using both BigPipe placeholders and no-JS - * BigPipe placeholders, we therefore send: 1 HtmlResponse + M Embedded HTML - * Responses + N Embedded AJAX Responses. Schematically, we send these chunks: - * 1. Byte zero until 1st no-JS placeholder: headers + <html><head /><div>…</div> - * 2. 1st no-JS placeholder replacement: <link rel="stylesheet" …><script …><content> - * 3. Content until 2nd no-JS placeholder: <div>…</div> - * 4. 2nd no-JS placeholder replacement: <link rel="stylesheet" …><script …><content> - * 5. Content until 3rd no-JS placeholder: <div>…</div> - * 6. [… repeat until all no-JS placeholder replacements are sent …] - * 7. Send content after last no-JS placeholder. - * 8. Send script_bottom (markup to load bottom i.e. non-critical JS). - * 9. 1st placeholder replacement: <script type="application/vnd.drupal-ajax">[{"command":"settings","settings":{…}}, {"command":…} - * 10. 2nd placeholder replacement: <script type="application/vnd.drupal-ajax">[{"command":"settings","settings":{…}}, {"command":…} - * 11. [… repeat until all placeholder replacements are sent …] - * 12. Send </body> and everything after it. - * 13. Terminate request/response cycle. - * - * @see \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber - * @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy - */ -interface BigPipeInterface { - - /** - * Sends an HTML response in chunks using the BigPipe technique. - * - * @param \Drupal\big_pipe\Render\BigPipeResponse $response - * The BigPipe response to send. - * - * @internal - * This method should only be invoked by - * \Drupal\big_pipe\Render\BigPipeResponse, which is itself an internal - * class. - */ - public function sendContent(BigPipeResponse $response); - -} diff --git a/core/modules/big_pipe/src/Render/BigPipeResponse.php b/core/modules/big_pipe/src/Render/BigPipeResponse.php index f74cec9a822216005848327f79fb0c6300eb7b47..70b637d34d8d6caa708be730a122cbabeae99da9 100644 --- a/core/modules/big_pipe/src/Render/BigPipeResponse.php +++ b/core/modules/big_pipe/src/Render/BigPipeResponse.php @@ -11,7 +11,7 @@ * it makes the content inaccessible (hidden behind a callback), which means no * middlewares are able to modify the content anymore. * - * @see \Drupal\big_pipe\Render\BigPipeInterface + * @see \Drupal\big_pipe\Render\BigPipe * * @internal * This is a temporary solution until a generic response emitter interface is @@ -23,7 +23,7 @@ class BigPipeResponse extends HtmlResponse { /** * The BigPipe service. * - * @var \Drupal\big_pipe\Render\BigPipeInterface + * @var \Drupal\big_pipe\Render\BigPipe */ protected $bigPipe; @@ -98,10 +98,10 @@ protected function populateBasedOnOriginalHtmlResponse() { /** * Sets the BigPipe service to use. * - * @param \Drupal\big_pipe\Render\BigPipeInterface $big_pipe + * @param \Drupal\big_pipe\Render\BigPipe $big_pipe * The BigPipe service. */ - public function setBigPipeService(BigPipeInterface $big_pipe) { + public function setBigPipeService(BigPipe $big_pipe) { $this->bigPipe = $big_pipe; } diff --git a/core/modules/big_pipe/src/Render/BigPipeResponseAttachmentsProcessor.php b/core/modules/big_pipe/src/Render/BigPipeResponseAttachmentsProcessor.php index 32640c2f7315971c7f8826bca8c8f5490c31fd66..94b8923700ddbd41a9a004bf68770cb279dd5641 100644 --- a/core/modules/big_pipe/src/Render/BigPipeResponseAttachmentsProcessor.php +++ b/core/modules/big_pipe/src/Render/BigPipeResponseAttachmentsProcessor.php @@ -17,7 +17,7 @@ * Processes attachments of HTML responses with BigPipe enabled. * * @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor - * @see \Drupal\big_pipe\Render\BigPipeInterface + * @see \Drupal\big_pipe\Render\BigPipe */ class BigPipeResponseAttachmentsProcessor extends HtmlResponseAttachmentsProcessor { diff --git a/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php b/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php index 7af53f0f518d64c5096b3e0081a8cf1a69ba6a35..46a20c68da4cebd1075018dddaca42d3d39274a3 100644 --- a/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php +++ b/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php @@ -56,7 +56,7 @@ * See \Drupal\big_pipe\Render\BigPipe for detailed documentation on how those * different placeholders are actually replaced. * - * @see \Drupal\big_pipe\Render\BigPipeInterface + * @see \Drupal\big_pipe\Render\BigPipe */ class BigPipeStrategy implements PlaceholderStrategyInterface {