 README.md                                  |  5 +--
 big_pipe.module                            | 70 +++++++++++-------------------
 big_pipe.routing.yml                       |  9 ++++
 big_pipe.services.yml                      |  2 +-
 src/Controller/BigPipeController.php       | 59 +++++++++++++++++++++++++
 src/Render/Placeholder/BigPipeStrategy.php | 67 +++++++++++++++++++++-------
 6 files changed, 149 insertions(+), 63 deletions(-)

diff --git a/README.md b/README.md
index b200b74..83fdb58 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,6 @@
 
 Install like any other Drupal module.
 
-Note: None of the existing sessions will be able to use BigPipe. Log out and log
-  in to benefit. To fix that, see https://www.drupal.org/node/2603046.
-
 
 # Recommendations
 
@@ -49,6 +46,8 @@ vcl_fetch {
 }
 ```
 
+Note that the `big_pipe_nojs` cookie does *not* break caching. Varnish should let that cookie pass through.
+
 
 ## Other (reverse) proxies
 
diff --git a/big_pipe.module b/big_pipe.module
index f2615e6..77e6605 100644
--- a/big_pipe.module
+++ b/big_pipe.module
@@ -2,55 +2,37 @@
 
 /**
  * @file
- * Enables BigPipe for authenticated users; first send+render the cheap parts
- * of the page, then the expensive parts.
- *
- * BigPipe allows to send a page in chunks. First the main content is sent and
- * then uncacheable data that takes long to generate.
- */
-
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Render\Markup;
-
-/**
- * Implements hook_form_FORM_ID_alter().
+ * Adds BigPipe no-JS detection.
  */
-function big_pipe_form_user_login_form_alter(&$form, FormStateInterface $form_state, $form_id) {
-  // Check if the user has JavaScript enabled without adding JavaScript.
-  $form['big_pipe_has_js'] = array(
-    '#type' => 'hidden',
-    '#default_value' => '1',
-  );
 
-  $form['#after_build'][] = 'big_pipe_form_after_build';
-  $form['#submit'][] = 'big_pipe_form_set_js_check';
-}
+use Drupal\Core\Url;
 
 /**
- * After build handler for user_login_form().
- */
-function big_pipe_form_after_build($form, FormStateInterface $form_state) {
-  // This is tricky: we want Form API to 'default big_pipe_has_js' to '1' in
-  // case it is not sent. We also want to set the value of the HTML element
-  // to '0' and wrap it in a <noscript> tag.
-  // So in case the user has JavaScript disabled, the <noscript> is parsed and
-  // 'big_pipe_has_js' is sent with '0', else it is not sent and Form API falls
-  // back to the default value, which is '1'.
-  $form['big_pipe_has_js']['#value'] = '0';
-  $form['big_pipe_has_js']['#prefix'] = Markup::create('<noscript>');
-  $form['big_pipe_has_js']['#suffix'] = Markup::create('</noscript>');
-  return $form;
-}
-
-/**
- * Form submission handler for user_login_form().
+ * Implements hook_page_attachments().
  *
- * Remember whether the user has JavaScript enabled in this session.
+ * @see \Drupal\big_pipe\Controller\BigPipeController::setNoJsCookie()
  */
-function big_pipe_form_set_js_check($form, FormStateInterface $form_state) {
-  $current_user = \Drupal::currentUser();
-
-  if ($current_user->isAuthenticated()) {
-    $_SESSION['big_pipe_has_js'] = $form_state->getValue('big_pipe_has_js') == 1;
+function big_pipe_page_attachments(array &$page) {
+  // 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.
+  $page['#cache']['contexts'][] = 'cookies:' . session_name();
+  // Only do the no-JS detection while we don't know if there's no JS support:
+  // avoid endless redirect loops.
+  $page['#cache']['contexts'][] = 'cookies:big_pipe_nojs';
+  $cookies = \Drupal::request()->cookies;
+  if ($cookies->has(session_name()) && !$cookies->has('big_pipe_nojs')) {
+    $page['#attached']['html_head'][] = [
+      [
+        // Redirect through a 'Refresh' meta tag if JavaScript is disabled.
+        '#tag' => 'meta',
+        '#noscript' => TRUE,
+        '#attributes' => [
+          'http-equiv' => 'Refresh',
+          'content' => '0; URL=' . Url::fromRoute('big_pipe.nojs', [], ['query' => \Drupal::service('redirect.destination')->getAsArray()])->toString(),
+        ],
+      ],
+      'big_pipe_detect_nojs',
+    ];
   }
 }
diff --git a/big_pipe.routing.yml b/big_pipe.routing.yml
new file mode 100644
index 0000000..c7981dc
--- /dev/null
+++ b/big_pipe.routing.yml
@@ -0,0 +1,9 @@
+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'
diff --git a/big_pipe.services.yml b/big_pipe.services.yml
index d93b9c7..b8ff9d4 100644
--- a/big_pipe.services.yml
+++ b/big_pipe.services.yml
@@ -6,9 +6,9 @@ services:
     arguments: ['@big_pipe']
   placeholder_strategy.big_pipe:
     class: Drupal\big_pipe\Render\Placeholder\BigPipeStrategy
+    arguments: ['@session_configuration', '@request_stack']
     tags:
       - { name: placeholder_strategy, priority: 0 }
-    arguments: ['@current_user']
   big_pipe:
     class: Drupal\big_pipe\Render\BigPipe
     arguments: ['@renderer', '@session', '@request_stack', '@http_kernel', '@event_dispatcher']
diff --git a/src/Controller/BigPipeController.php b/src/Controller/BigPipeController.php
new file mode 100644
index 0000000..a20925a
--- /dev/null
+++ b/src/Controller/BigPipeController.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\big_pipe\Controller\BigPipeController.
+ */
+
+namespace Drupal\big_pipe\Controller;
+
+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 'big_pipe_nojs' cookie if there is a session.
+   *
+   * Must only be accessed when the browser does not support JavaScript, is
+   * accessed automatically when that's the case thanks to
+   * big_pipe_page_attachments().
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   *
+   * @return \Drupal\Core\Routing\LocalRedirectResponse
+   *   A response that sets the 'big_pipe_nojs' cookie and redirects back to the
+   *   original location.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
+   *   Thrown when the big_pipe_nojs cookie is already set (to prevent a
+   *   redirect loop) or when there is no session (in which case BigPipe is not
+   *   enabled anyway).
+   * @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) {
+    if ($request->cookies->has('big_pipe_nojs') || $request->getSession() === NULL) {
+      throw new AccessDeniedHttpException();
+    }
+
+    if (!$request->query->has('destination')) {
+      throw new HttpException(500, 'The original location is missing.');
+    }
+
+    $response = new LocalRedirectResponse($request->query->get('destination'));
+    $response->headers->setCookie(new Cookie('big_pipe_nojs', TRUE));
+    return $response;
+  }
+
+}
diff --git a/src/Render/Placeholder/BigPipeStrategy.php b/src/Render/Placeholder/BigPipeStrategy.php
index b672c6c..008db2c 100644
--- a/src/Render/Placeholder/BigPipeStrategy.php
+++ b/src/Render/Placeholder/BigPipeStrategy.php
@@ -10,11 +10,23 @@ namespace Drupal\big_pipe\Render\Placeholder;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface;
-use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Session\SessionConfigurationInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
  * Defines the BigPipe placeholder strategy, to send HTML in chunks.
  *
+ * First: the BigPipe placeholder strategy only activates if the current request
+ * is associated with a session. Without a session, it is assumed this response
+ * is not actually dynamic: if none of the placeholders show session-dependent
+ * information, then none of the placeholders are uncacheable or poorly
+ * cacheable, which means the Page Cache (for anonymous users) can deal with it.
+ * In other words: BigPipe works for all authenticated users and for anonymous
+ * users that have a session (typical example: a shopping cart).
+ *
+ * (This is the default, and other modules can subclass this placeholder
+ * strategy to have different rules for enabling BigPipe.)
+ *
  * The BigPipe placeholder strategy actually consists of two substrategies,
  * depending on whether the current session is in a browser with JavaScript
  * enabled or not:
@@ -52,31 +64,56 @@ use Drupal\Core\Session\AccountInterface;
 class BigPipeStrategy implements PlaceholderStrategyInterface {
 
   /**
-   * The current user.
+   * The session configuration.
    *
-   * @var \Drupal\Core\Session\AccountInterface
+   * @var \Drupal\Core\Session\SessionConfigurationInterface
    */
-  protected $currentUser;
+  protected $sessionConfiguration;
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
 
   /**
    * Constructs a new BigPipeStrategy class.
    *
-   * @param \Drupal\Core\Session\AccountInterface $current_user
-   *   The current user.
+   * @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
+   *   The session configuration.
+   * @param \Symfony\Component\HttpFoundation\RequestStack
+   *   The request stack.
    */
-  public function __construct(AccountInterface $current_user) {
-    $this->currentUser = $current_user;
+  public function __construct(SessionConfigurationInterface $session_configuration, RequestStack $request_stack) {
+    $this->sessionConfiguration = $session_configuration;
+    $this->requestStack = $request_stack;
   }
 
   /**
    * {@inheritdoc}
    */
   public function processPlaceholders(array $placeholders) {
-    // @todo Remove in https://www.drupal.org/node/2603046.
-    if (!$this->currentUser->isAuthenticated()) {
+    if (!$this->sessionConfiguration->hasSession($this->requestStack->getCurrentRequest())) {
       return [];
     }
 
+    return $this->doProcessPlaceholders($placeholders);
+  }
+
+  /**
+   * Transforms placeholders to BigPipe placeholders, either no-JS or JS.
+   *
+   * @param array $placeholders
+   *  The placeholders to process.
+   *
+   * @return array
+   *  The BigPipe placeholders.
+   */
+  protected function doProcessPlaceholders(array $placeholders) {
+    $non_html_placeholder_cache_contexts = ['cookies:' . session_name()];
+    $html_placeholder_cache_contexts = ['cookies:' . session_name(), 'cookies:big_pipe_nojs'];
+
     $overridden_placeholders = [];
     foreach ($placeholders as $placeholder => $placeholder_elements) {
       // BigPipe uses JavaScript and the DOM to find the placeholder to replace.
@@ -94,16 +131,18 @@ class BigPipeStrategy implements PlaceholderStrategyInterface {
       // @see \Drupal\Core\Form\FormBuilder::renderPlaceholderFormAction()
       if ($placeholder[0] !== '<' || $placeholder !== Html::normalize($placeholder)) {
         $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements);
+        $overridden_placeholders[$placeholder]['#cache']['contexts'] = $non_html_placeholder_cache_contexts;
       }
       else {
-        // If the current session doesn't have JavaScript, fall back to no-JS
-        // BigPipe.
-        if (empty($_SESSION['big_pipe_has_js'])) {
+        // If the current request/session doesn't have JavaScript, fall back to
+        // no-JS BigPipe.
+        if ($this->requestStack->getCurrentRequest()->cookies->has('big_pipe_nojs')) {
           $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements);
         }
         else {
           $overridden_placeholders[$placeholder] = static::createBigPipeJsPlaceholder($placeholder, $placeholder_elements);
         }
+        $overridden_placeholders[$placeholder]['#cache']['contexts'] = $html_placeholder_cache_contexts;
       }
     }
 
@@ -141,7 +180,6 @@ class BigPipeStrategy implements PlaceholderStrategyInterface {
     return [
       '#markup' => '<div data-big-pipe-selector="' . Html::escape($big_pipe_js_selector) . '"></div>',
       '#cache' => [
-        'contexts' => ['session'],
         'max-age' => 0,
       ],
       '#attached' => [
@@ -178,7 +216,6 @@ class BigPipeStrategy implements PlaceholderStrategyInterface {
     return [
       '#markup' => '<div data-big-pipe-selector-nojs="' . $html_placeholder . '"></div>',
       '#cache' => [
-        'contexts' => ['session'],
         'max-age' => 0,
       ],
       '#attached' => [
