core/modules/big_pipe/src/Render/BigPipe.php | 8 +- .../src/Render/Placeholder/BigPipeStrategy.php | 106 ++++++++++++++++----- 2 files changed, 88 insertions(+), 26 deletions(-) diff --git a/core/modules/big_pipe/src/Render/BigPipe.php b/core/modules/big_pipe/src/Render/BigPipe.php index 69de576..61410ce 100644 --- a/core/modules/big_pipe/src/Render/BigPipe.php +++ b/core/modules/big_pipe/src/Render/BigPipe.php @@ -7,6 +7,7 @@ namespace Drupal\big_pipe\Render; +use Drupal\Component\Utility\Html; use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\Asset\AttachedAssets; @@ -202,7 +203,12 @@ public function sendContent($content, $attachments, array $placeholders) { // Create a new AjaxResponse. $ajax_response = new AjaxResponse(); - $ajax_response->addCommand(new ReplaceCommand(sprintf('[data-big-pipe-selector="%s"]', $placeholder), $elements['#markup'])); + // JavaScript's querySelector automatically decodes HTML entities in + // attributes, so we must decode the entities of the current BigPipe + // placeholder (which has HTML entities encoded since we use it to find + // the placeholders). + $big_pipe_js_selector = Html::decodeEntities($placeholder); + $ajax_response->addCommand(new ReplaceCommand(sprintf('[data-big-pipe-selector="%s"]', $big_pipe_js_selector), $elements['#markup'])); $ajax_response->setAttachments($elements['#attached']); // Push a fake request with the asset libraries loaded so far and dispatch diff --git a/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php b/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php index af1358a..63229bc 100644 --- a/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php +++ b/core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php @@ -7,15 +7,50 @@ namespace Drupal\big_pipe\Render\Placeholder; -use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\Html; +use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface; use Drupal\Core\Session\AccountInterface; /** - * Defines the 'big_pipe' render strategy. + * Defines the BigPipe placeholder strategy, to send HTML in chunks. * - * This is the last strategy that always replaces all remaining placeholders. + * The BigPipe placeholder strategy actually consists of two substrategies, + * depending on whether the current session is in a browser with JavaScript + * enabled or not: + * 1. with JavaScript enabled: #attached[big_pipe_js_placeholders]. Their + * replacements are streamed at the end of the page: chunk 1 is the entire + * page until the closing tag, chunks 2 to (N-1) are replacement + * values for the placeholders, chunk N is and everything after it. + * 2. with JavaScript disabled: #attached[big_pipe_nojs_placeholders]. Their + * replacements are streamed in situ: chunk 1 is the entire page until the + * first no-JS BigPipe placeholder, chunk 2 is the replacement for that + * placeholder, chunk 3 is the chunk from after that placeholder until the + * next no-JS BigPipe placeholder, et cetera. + * + * JS BigPipe placeholders are preferred because they result in better perceived + * performance: the entire page can be sent, minus the placeholders. But it + * requires JavaScript. + * + * No-JS BigPipe placeholders result in more visible blocking: only the part of + * the page can be sent until the first placeholder, after it is rendered until + * the second, et cetera. (In essence: multiple flushes.) + * + * Finally, both of those substrategies can also be combined: some placeholders + * live in places that cannot be efficiently replaced by JavaScript, for example + * CSRF tokens in URLs. Using no-JS BigPipe placeholders in those cases allows + * the first part of the page (until the first no-JS BigPipe placeholder) to be + * sent sooner than when they would be replaced using SingleFlushStrategy, which + * would prevent anything from being sent until all those non-HTML placeholders + * would have been replaced. + * + * See \Drupal\big_pipe\Render\BigPipe for detailed documentation on how those + * different placeholders are actually replaced. + * + * @see \Drupal\big_pipe\Render\BigPipe + * + * @todo Rename #attached[big_pipe_placeholders] to #attached[big_pipe_js_placeholders] + * @todo Introduce #attached[big_pipe_nojs_placeholders] and remove the current globals-based switching logic in this class and the BigPipe class */ class BigPipeStrategy implements PlaceholderStrategyInterface { @@ -68,32 +103,53 @@ public function processPlaceholders(array $placeholders) { // @see \Drupal\Core\Form\FormBuilder::renderFormTokenPlaceholder() // @see \Drupal\Core\Form\FormBuilder::renderPlaceholderFormAction() if ($placeholder[0] !== '<' || $placeholder !== Html::normalize($placeholder)) { + // @todo Use #attached[big_pipe_nojs_placeholders] instead of skipping these altogether, that allows parts of the page to be sent faster! continue; } - - $html_placeholder = Html::getId($placeholder); - $return[$placeholder] = [ - '#markup' => '
', - // Big Pipe placeholders are not cacheable. - '#cache' => [ - 'max-age' => 0, - ], - '#attached' => [ - // Use the big_pipe library. - 'library' => [ - 'big_pipe/big_pipe', - ], - // Add the placeholder to a white list of JS processed placeholders. - 'drupalSettings' => [ - 'bigPipePlaceholders' => [ $html_placeholder => TRUE ], - ], - 'big_pipe_placeholders' => [ - $html_placeholder => $placeholder_elements, - ], - ], - ]; + else { + $return[$placeholder] = static::createBigPipeJsPlaceholder($placeholder_elements); + } } return $return; } + + /** + * Creates a BigPipe JS placeholder. + * + * @param array $placeholder_render_array + * The render array for a placeholder. + * + * @return array + * The resulting BigPipe JS placeholder render array. + */ + protected static function createBigPipeJsPlaceholder(array $placeholder_render_array) { + // Generate a BigPipe selector (to be used by BigPipe's JavaScript). + // @see \Drupal\Core\Render\PlaceholderGenerator::createPlaceholder() + $callback = $placeholder_render_array['#lazy_builder'][0]; + $arguments = $placeholder_render_array['#lazy_builder'][1]; + $token = hash('crc32b', serialize($placeholder_render_array)); + $big_pipe_js_selector = UrlHelper::buildQuery(['callback' => $callback, 'args' => $arguments, 'token' => $token]); + + return [ + '#markup' => '
', + '#cache' => [ + 'contexts' => ['session'], + 'max-age' => 0, + ], + '#attached' => [ + 'library' => [ + 'big_pipe/big_pipe', + ], + // Inform BigPipe' JavaScript known BigPipe placeholders (a whitelist). + 'drupalSettings' => [ + 'bigPipePlaceholders' => [$big_pipe_js_selector => TRUE], + ], + 'big_pipe_placeholders' => [ + Html::escape($big_pipe_js_selector) => $placeholder_render_array, + ], + ], + ]; + } + }