Commit dda84876 authored by effulgentsia's avatar effulgentsia

Issue #2469431 by Wim Leers, Fabianx, swentel, nod_, yched, dawehner,...

Issue #2469431 by Wim Leers, Fabianx, swentel, nod_, yched, dawehner, tstoeckler, catch, borisson_: BigPipe for auth users: first send+render the cheap parts of the page, then the expensive parts
parent 1703af3b
......@@ -261,6 +261,10 @@ Basic Auth module
- Klaus Purer 'klausi' https://www.drupal.org/u/klausi
- Juampy Novillo Requena 'juampy' https://www.drupal.org/u/juampy
BigPipe module
- Wim Leers 'Wim Leers' https://www.drupal.org/u/wim-leers
- Fabian Franz 'Fabianx' https://www.drupal.org/u/fabianx
Block module
- Tim Plunkett 'tim.plunkett' https://www.drupal.org/u/tim.plunkett
- Ben Dougherty 'benjy' https://www.drupal.org/u/benjy
......
......@@ -48,6 +48,7 @@
"drupal/bartik": "self.version",
"drupal/ban": "self.version",
"drupal/basic_auth": "self.version",
"drupal/big_pipe": "self.version",
"drupal/block": "self.version",
"drupal/block_content": "self.version",
"drupal/book": "self.version",
......
name: BigPipe
type: module
description: 'Sends pages in a way that allows browsers to show them much faster. Uses the BigPipe technique.'
package: Core (Experimental)
version: VERSION
core: 8.x
big_pipe:
version: VERSION
js:
js/big_pipe.js: {}
drupalSettings:
bigPipePlaceholderIds: []
dependencies:
- core/jquery
- core/jquery.once
- core/drupal.ajax
- core/drupalSettings
<?php
/**
* @file
* Adds BigPipe no-JS detection.
*/
use Drupal\big_pipe\Render\Placeholder\BigPipeStrategy;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
/**
* Implements hook_help().
*/
function big_pipe_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.big_pipe':
$output = '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The BigPipe module sends pages with dynamic content in a way that allows browsers to show them much faster. For more information, see the <a href=":big_pipe-documentation">online documentation for the BigPipe module</a>.', [':big_pipe-documentation' => 'https://www.drupal.org/documentation/modules/big_pipe']) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Speeding up your site') . '</dt>';
$output .= '<dd>' . t('The module requires no configuration. Every part of the page contains metadata that allows BigPipe to figure this out on its own.') . '</dd>';
$output .= '</dl>';
return $output;
}
}
/**
* Implements hook_page_attachments().
*
* @see \Drupal\big_pipe\Controller\BigPipeController::setNoJsCookie()
*/
function big_pipe_page_attachments(array &$page) {
// Routes that don't use BigPipe also don't need no-JS detection.
if (\Drupal::routeMatch()->getRouteObject()->getOption('_no_big_pipe')) {
return;
}
$request = \Drupal::request();
// BigPipe is only used when there is an actual session, so only add the no-JS
// detection when there actually is a session.
// @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy.
$session_exists = \Drupal::service('session_configuration')->hasSession($request);
$page['#cache']['contexts'][] = 'session.exists';
// Only do the no-JS detection while we don't know if there's no JS support:
// avoid endless redirect loops.
$has_big_pipe_nojs_cookie = $request->cookies->has(BigPipeStrategy::NOJS_COOKIE);
$page['#cache']['contexts'][] = 'cookies:' . BigPipeStrategy::NOJS_COOKIE;
if ($session_exists && !$has_big_pipe_nojs_cookie) {
$page['#attached']['html_head'][] = [
[
// Redirect through a 'Refresh' meta tag if JavaScript is disabled.
'#tag' => 'meta',
'#noscript' => TRUE,
'#attributes' => [
'http-equiv' => 'Refresh',
// @todo: Switch to Url::fromRoute() once https://www.drupal.org/node/2589967 is resolved.
'content' => '0; URL=' . Url::fromUri('internal:/big_pipe/no-js', ['query' => \Drupal::service('redirect.destination')->getAsArray()])->toString(),
],
],
'big_pipe_detect_nojs',
];
}
}
big_pipe.nojs:
path: '/big_pipe/no-js'
defaults:
_controller: '\Drupal\big_pipe\Controller\BigPipeController:setNoJsCookie'
_title: 'BigPipe no-JS check'
options:
no_cache: TRUE
requirements:
_access: 'TRUE'
services:
html_response.big_pipe_subscriber:
class: Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber
tags:
- { name: event_subscriber }
arguments: ['@big_pipe']
placeholder_strategy.big_pipe:
class: Drupal\big_pipe\Render\Placeholder\BigPipeStrategy
arguments: ['@session_configuration', '@request_stack', '@current_route_match']
tags:
- { name: placeholder_strategy, priority: 0 }
big_pipe:
class: Drupal\big_pipe\Render\BigPipe
arguments: ['@renderer', '@session', '@request_stack', '@http_kernel', '@event_dispatcher']
html_response.attachments_processor.big_pipe:
public: false
class: \Drupal\big_pipe\Render\BigPipeResponseAttachmentsProcessor
decorates: html_response.attachments_processor
decoration_inner_name: html_response.attachments_processor.original
arguments: ['@html_response.attachments_processor.original', '@asset.resolver', '@config.factory', '@asset.css.collection_renderer', '@asset.js.collection_renderer', '@request_stack', '@renderer', '@module_handler']
route_subscriber.no_big_pipe:
class: Drupal\big_pipe\EventSubscriber\NoBigPipeRouteAlterSubscriber
tags:
- { name: event_subscriber }
/**
* @file
* Renders BigPipe placeholders using Drupal's Ajax system.
*/
(function ($, Drupal, drupalSettings) {
'use strict';
/**
* Executes Ajax commands in <script type="application/json"> tag.
*
* These Ajax commands replace placeholders with HTML and load missing CSS/JS.
*
* @param {number} index
* Current index.
* @param {HTMLScriptElement} placeholderReplacement
* Script tag created by BigPipe.
*/
function bigPipeProcessPlaceholderReplacement(index, placeholderReplacement) {
var placeholderId = placeholderReplacement.getAttribute('data-big-pipe-replacement-for-placeholder-with-id');
var content = this.textContent.trim();
// Ignore any placeholders that are not in the known placeholder list. Used
// to avoid someone trying to XSS the site via the placeholdering mechanism.
if (typeof drupalSettings.bigPipePlaceholderIds[placeholderId] !== 'undefined') {
// If we try to parse the content too early (when the JSON containing Ajax
// commands is still arriving), textContent will be empty which will cause
// JSON.parse() to fail. Remove once so that it can be processed again
// later.
// @see bigPipeProcessDocument()
if (content === '') {
$(this).removeOnce('big-pipe');
}
else {
var response = JSON.parse(content);
// Create a Drupal.Ajax object without associating an element, a
// progress indicator or a URL.
var ajaxObject = Drupal.ajax({
url: '',
base: false,
element: false,
progress: false
});
// Then, simulate an AJAX response having arrived, and let the Ajax
// system handle it.
ajaxObject.success(response, 'success');
}
}
}
/**
* Processes a streamed HTML document receiving placeholder replacements.
*
* @param {HTMLDocument} context
* The HTML document containing <script type="application/json"> tags
* generated by BigPipe.
*
* @return {bool}
* Returns true when processing has been finished and a stop signal has been
* found.
*/
function bigPipeProcessDocument(context) {
// Make sure we have BigPipe-related scripts before processing further.
if (!context.querySelector('script[data-big-pipe-event="start"]')) {
return false;
}
$(context).find('script[data-big-pipe-replacement-for-placeholder-with-id]')
.once('big-pipe')
.each(bigPipeProcessPlaceholderReplacement);
// If we see the stop signal, clear the timeout: all placeholder
// replacements are guaranteed to be received and processed.
if (context.querySelector('script[data-big-pipe-event="stop"]')) {
if (timeoutID) {
clearTimeout(timeoutID);
}
return true;
}
return false;
}
function bigPipeProcess() {
timeoutID = setTimeout(function () {
if (!bigPipeProcessDocument(document)) {
bigPipeProcess();
}
}, interval);
}
var interval = 200;
// The internal ID to contain the watcher service.
var timeoutID;
bigPipeProcess();
// If something goes wrong, make sure everything is cleaned up and has had a
// chance to be processed with everything loaded.
$(window).on('load', function () {
if (timeoutID) {
clearTimeout(timeoutID);
}
bigPipeProcessDocument(document);
});
})(jQuery, Drupal, drupalSettings);
<?php
/**
* @file
* Contains \Drupal\big_pipe\Controller\BigPipeController.
*/
namespace Drupal\big_pipe\Controller;
use Drupal\big_pipe\Render\Placeholder\BigPipeStrategy;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Routing\LocalRedirectResponse;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
/**
* Returns responses for BigPipe module routes.
*/
class BigPipeController {
/**
* Sets a BigPipe no-JS cookie, redirects back to the original location.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return \Drupal\Core\Routing\LocalRedirectResponse
* A response that sets the no-JS cookie and redirects back to the original
* location.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* Thrown when the no-JS cookie is already set or when there is no session.
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
* Thrown when the original location is missing, i.e. when no 'destination'
* query argument is set.
*
* @see \Drupal\big_pipe\Render\Placeholder\BigPipeStrategy
*/
public function setNoJsCookie(Request $request) {
// This controller may only be accessed when the browser does not support
// JavaScript. It is accessed automatically when that's the case thanks to
// big_pipe_page_attachments(). When this controller is executed, deny
// access when either:
// - the no-JS cookie is already set: this indicates a redirect loop, since
// the cookie was already set, yet the user is executing this controller;
// - there is no session, in which case BigPipe is not enabled anyway, so it
// is pointless to set this cookie.
if ($request->cookies->has(BigPipeStrategy::NOJS_COOKIE) || $request->getSession() === NULL) {
throw new AccessDeniedHttpException();
}
if (!$request->query->has('destination')) {
throw new HttpException(400, 'The original location is missing.');
}
$response = new LocalRedirectResponse($request->query->get('destination'));
$response->headers->setCookie(new Cookie(BigPipeStrategy::NOJS_COOKIE, TRUE));
$response->addCacheableDependency((new CacheableMetadata())->addCacheContexts(['cookies:' . BigPipeStrategy::NOJS_COOKIE, 'session.exists']));
return $response;
}
}
<?php
/**
* @file
* Contains \Drupal\big_pipe\EventSubscriber\HtmlResponseBigPipeSubscriber.
*/
namespace Drupal\big_pipe\EventSubscriber;
use Drupal\Core\Render\HtmlResponse;
use Drupal\big_pipe\Render\BigPipeInterface;
use Drupal\big_pipe\Render\BigPipeResponse;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Response subscriber to replace the HtmlResponse with a BigPipeResponse.
*
* @see \Drupal\big_pipe\Render\BigPipeInterface
*
* @todo Refactor once https://www.drupal.org/node/2577631 lands.
*/
class HtmlResponseBigPipeSubscriber implements EventSubscriberInterface {
/**
* Attribute name of the BigPipe response eligibility test result.
*
* @see onRespondEarly()
* @see onRespond()
*/
const ATTRIBUTE_ELIGIBLE = '_big_pipe_eligible';
/**
* The BigPipe service.
*
* @var \Drupal\big_pipe\Render\BigPipeInterface
*/
protected $bigPipe;
/**
* Constructs a HtmlResponseBigPipeSubscriber object.
*
* @param \Drupal\big_pipe\Render\BigPipeInterface $big_pipe
* The BigPipe service.
*/
public function __construct(BigPipeInterface $big_pipe) {
$this->bigPipe = $big_pipe;
}
/**
* Adds markers to the response necessary for the BigPipe render strategy.
*
* @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
* The event to process.
*/
public function onRespondEarly(FilterResponseEvent $event) {
// It does not make sense to have BigPipe responses for subrequests. BigPipe
// is never useful internally in Drupal, only externally towards end users.
$response = $event->getResponse();
$is_eligible = $event->isMasterRequest() && $response instanceof HtmlResponse;
$event->getRequest()->attributes->set(self::ATTRIBUTE_ELIGIBLE, $is_eligible);
if (!$is_eligible) {
return;
}
// Wrap the scripts_bottom placeholder with a marker before and after,
// because \Drupal\big_pipe\Render\BigPipe needs to be able to extract that
// markup if there are no-JS BigPipe placeholders.
// @see \Drupal\big_pipe\Render\BigPipe::sendPreBody()
$attachments = $response->getAttachments();
if (isset($attachments['html_response_attachment_placeholders']['scripts_bottom'])) {
$scripts_bottom_placeholder = $attachments['html_response_attachment_placeholders']['scripts_bottom'];
$content = $response->getContent();
$content = str_replace($scripts_bottom_placeholder, '<drupal-big-pipe-scripts-bottom-marker>' . $scripts_bottom_placeholder . '<drupal-big-pipe-scripts-bottom-marker>', $content);
$response->setContent($content);
}
}
/**
* Transforms a HtmlResponse to a BigPipeResponse.
*
* @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
* The event to process.
*/
public function onRespond(FilterResponseEvent $event) {
// Early return if this response was already found to not be eligible.
// @see onRespondEarly()
if (!$event->getRequest()->attributes->get(self::ATTRIBUTE_ELIGIBLE)) {
return;
}
$response = $event->getResponse();
$attachments = $response->getAttachments();
// If there are no no-JS BigPipe placeholders, unwrap the scripts_bottom
// markup.
// @see onRespondEarly()
// @see \Drupal\big_pipe\Render\BigPipe::sendPreBody()
if (empty($attachments['big_pipe_nojs_placeholders'])) {
$content = $response->getContent();
$content = str_replace('<drupal-big-pipe-scripts-bottom-marker>', '', $content);
$response->setContent($content);
}
// If there are neither BigPipe placeholders nor no-JS BigPipe placeholders,
// there isn't anything dynamic in this response, and we can return early:
// there is no point in sending this response using BigPipe.
if (empty($attachments['big_pipe_placeholders']) && empty($attachments['big_pipe_nojs_placeholders'])) {
return;
}
$big_pipe_response = new BigPipeResponse();
$big_pipe_response->setBigPipeService($this->bigPipe);
// Clone the HtmlResponse's data into the new BigPipeResponse.
$big_pipe_response->headers = clone $response->headers;
$big_pipe_response
->setStatusCode($response->getStatusCode())
->setContent($response->getContent())
->setAttachments($attachments)
->addCacheableDependency($response->getCacheableMetadata());
// A BigPipe response can never be cached, because it is intended for a
// single user.
// @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
$big_pipe_response->setPrivate();
// Inform surrogates how they should handle BigPipe responses:
// - "no-store" specifies that the response should not be stored in cache;
// it is only to be used for the original request
// - "content" identifies what processing surrogates should perform on the
// response before forwarding it. We send, "BigPipe/1.0", which surrogates
// should not process at all, and in fact, they should not even buffer it
// at all.
// @see http://www.w3.org/TR/edge-arch/
$big_pipe_response->headers->set('Surrogate-Control', 'no-store, content="BigPipe/1.0"');
// Add header to support streaming on NGINX + php-fpm (nginx >= 1.5.6).
$big_pipe_response->headers->set('X-Accel-Buffering', 'no');
$event->setResponse($big_pipe_response);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// Run after HtmlResponsePlaceholderStrategySubscriber (priority 5), i.e.
// after BigPipeStrategy has been applied, but before normal (priority 0)
// response subscribers have been applied, because by then it'll be too late
// to transform it into a BigPipeResponse.
$events[KernelEvents::RESPONSE][] = ['onRespondEarly', 3];
// Run as the last possible subscriber.
$events[KernelEvents::RESPONSE][] = ['onRespond', -10000];
return $events;
}
}
<?php
/**
* @file
* Contains \Drupal\big_pipe\EventSubscriber\NoBigPipeRouteAlterSubscriber.
*/
namespace Drupal\big_pipe\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Drupal\Core\Routing\RoutingEvents;
use Drupal\Core\Routing\RouteBuildEvent;
/**
* Sets the '_no_big_pipe' option on select routes.
*/
class NoBigPipeRouteAlterSubscriber implements EventSubscriberInterface {
/**
* Alters select routes to have the '_no_big_pipe' option.
*
* @param \Drupal\Core\Routing\RouteBuildEvent $event
* The event to process.
*/
public function onRoutingRouteAlterSetNoBigPipe(RouteBuildEvent $event) {
$no_big_pipe_routes = [
// The batch system uses a <meta> refresh to work without JavaScript.
'system.batch_page.html',
// When a user would install the BigPipe module using a browser and with
// JavaScript disabled, the first response contains the status messages
// for installing a module, but then the BigPipe no-JS redirect occurs,
// which then causes the user to not see those status messages.
// @see https://www.drupal.org/node/2469431#comment-10901944
'system.modules_list',
];
$route_collection = $event->getRouteCollection();
foreach ($no_big_pipe_routes as $excluded_route) {
if ($route = $route_collection->get($excluded_route)) {
$route->setOption('_no_big_pipe', TRUE);
}
}
}
/**
* {@inheritdoc}
*/
static function getSubscribedEvents() {
$events[RoutingEvents::ALTER][] = ['onRoutingRouteAlterSetNoBigPipe'];
return $events;
}
}
This diff is collapsed.
<?php
/**
* @file
* Contains \Drupal\big_pipe\Render\BigPipeInterface.
*/
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/json">[{"command":"settings","settings":{…}}, {"command":…}.
* - So, for every BigPipe placeholder, we send such a <script
* type="application/json"> 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/json">[{"command":"settings","settings":{…}}, {"command":…}
* 10. 2nd placeholder replacement: <script type="application/json">[{"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 string $content
* The HTML response content to send.
* @param array $attachments
* The HTML response's attachments.
*/
public function sendContent($content, array $attachments);
}
<?php
/**
* @file
* Contains \Drupal\big_pipe\Render\BigPipeMarkup.
*/
namespace Drupal\big_pipe\Render;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Render\MarkupTrait;
/**
* Defines an object that passes safe strings through BigPipe's render pipeline.
*
* This object should only be constructed with a known safe string. If there is
* any risk that the string contains user-entered data that has not been
* filtered first, it must not be used.
*
* @internal
* This object is marked as internal because it should only be used in the
* BigPipe render pipeline.
*
* @see \Drupal\Core\Render\Markup