diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index 9dd3f56..3021cee 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -21,6 +21,11 @@
 use Drupal\Core\Lock\DatabaseLockBackend;
 use Drupal\Core\Lock\LockBackendInterface;
 use Drupal\user\Plugin\Core\Entity\User;
+use Drupal\Core\Cache\Cache;
+
+use Symfony\Component\HttpKernel\HttpCache\Esi;
+use Symfony\Component\HttpKernel\HttpCache\HttpCache;
+use Symfony\Component\HttpKernel\HttpCache\Store;
 
 /**
  * @file
@@ -1210,108 +1215,6 @@ function drupal_page_header() {
 }
 
 /**
- * Sets HTTP headers in preparation for a cached page response.
- *
- * The headers allow as much as possible in proxies and browsers without any
- * particular knowledge about the pages. Modules can override these headers
- * using drupal_add_http_header().
- *
- * If the request is conditional (using If-Modified-Since and If-None-Match),
- * and the conditions match those currently in the cache, a 304 Not Modified
- * response is sent.
- */
-function drupal_serve_page_from_cache(stdClass $cache) {
-  $config = config('system.performance');
-
-  // Negotiate whether to use compression.
-  $page_compression = $config->get('response.gzip') && extension_loaded('zlib');
-  $return_compressed = $page_compression && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE;
-
-  // Get headers. Keys are lower-case.
-  $boot_headers = drupal_get_http_header();
-
-  // Headers generated in this function, that may be replaced or unset using
-  // drupal_add_http_headers(). Keys are mixed-case.
-  $default_headers = array();
-
-  foreach ($cache->data['headers'] as $name => $value) {
-    // In the case of a 304 response, certain headers must be sent, and the
-    // remaining may not (see RFC 2616, section 10.3.5).
-    $name_lower = strtolower($name);
-    if (in_array($name_lower, array('content-location', 'expires', 'cache-control', 'vary')) && !isset($boot_headers[$name_lower])) {
-      drupal_add_http_header($name, $value);
-      unset($cache->data['headers'][$name]);
-    }
-  }
-
-  // If the client sent a session cookie, a cached copy will only be served
-  // to that one particular client due to Vary: Cookie. Thus, do not set
-  // max-age > 0, allowing the page to be cached by external proxies, when a
-  // session cookie is present unless the Vary header has been replaced.
-  $max_age = !isset($_COOKIE[session_name()]) || isset($boot_headers['vary']) ? $config->get('cache.page.max_age') : 0;
-  $default_headers['Cache-Control'] = 'public, max-age=' . $max_age;
-
-  // Entity tag should change if the output changes.
-  $etag = '"' . $cache->created . '-' . intval($return_compressed) . '"';
-  header('Etag: ' . $etag);
-
-  // See if the client has provided the required HTTP headers.
-  $if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : FALSE;
-  $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) : FALSE;
-
-  if ($if_modified_since && $if_none_match
-      && $if_none_match == $etag // etag must match
-      && $if_modified_since == $cache->created) {  // if-modified-since must match
-    header($_SERVER['SERVER_PROTOCOL'] . ' 304 Not Modified');
-    drupal_send_headers($default_headers);
-    return;
-  }
-
-  // Send the remaining headers.
-  foreach ($cache->data['headers'] as $name => $value) {
-    drupal_add_http_header($name, $value);
-  }
-
-  $default_headers['Last-Modified'] = gmdate(DATE_RFC1123, $cache->created);
-
-  // HTTP/1.0 proxies does not support the Vary header, so prevent any caching
-  // by sending an Expires date in the past. HTTP/1.1 clients ignores the
-  // Expires header if a Cache-Control: max-age= directive is specified (see RFC
-  // 2616, section 14.9.3).
-  $default_headers['Expires'] = 'Sun, 19 Nov 1978 05:00:00 GMT';
-
-  drupal_send_headers($default_headers);
-
-  // Allow HTTP proxies to cache pages for anonymous users without a session
-  // cookie. The Vary header is used to indicates the set of request-header
-  // fields that fully determines whether a cache is permitted to use the
-  // response to reply to a subsequent request for a given URL without
-  // revalidation.
-  if (!isset($boot_headers['vary']) && !settings()->get('omit_vary_cookie')) {
-    header('Vary: Cookie');
-  }
-
-  if ($page_compression) {
-    header('Vary: Accept-Encoding', FALSE);
-    // If page_compression is enabled, the cache contains gzipped data.
-    if ($return_compressed) {
-      // $cache->data['body'] is already gzip'ed, so make sure
-      // zlib.output_compression does not compress it once more.
-      ini_set('zlib.output_compression', '0');
-      header('Content-Encoding: gzip');
-    }
-    else {
-      // The client does not support compression, so unzip the data in the
-      // cache. Strip the gzip header and run uncompress.
-      $cache->data['body'] = gzinflate(substr(substr($cache->data['body'], 10), 0, -8));
-    }
-  }
-
-  // Print the page.
-  print $cache->data['body'];
-}
-
-/**
  * Defines the critical hooks that force modules to always be loaded.
  */
 function bootstrap_hooks() {
@@ -1800,7 +1703,6 @@ function drupal_bootstrap($phase = NULL, $new_phase = TRUE) {
   static $phases = array(
     DRUPAL_BOOTSTRAP_CONFIGURATION,
     DRUPAL_BOOTSTRAP_KERNEL,
-    DRUPAL_BOOTSTRAP_PAGE_CACHE,
     DRUPAL_BOOTSTRAP_DATABASE,
     DRUPAL_BOOTSTRAP_VARIABLES,
     DRUPAL_BOOTSTRAP_SESSION,
@@ -1841,10 +1743,6 @@ function drupal_bootstrap($phase = NULL, $new_phase = TRUE) {
           _drupal_bootstrap_kernel();
           break;
 
-        case DRUPAL_BOOTSTRAP_PAGE_CACHE:
-          _drupal_bootstrap_page_cache();
-          break;
-
         case DRUPAL_BOOTSTRAP_DATABASE:
           _drupal_bootstrap_database();
           break;
@@ -1911,6 +1809,11 @@ function drupal_handle_request($test_only = FALSE) {
   $kernel->boot();
   drupal_bootstrap(DRUPAL_BOOTSTRAP_CODE);
 
+  if (settings()->get('use_http_cache', TRUE)) {
+    $store_path = variable_get('file_public_path', conf_path() . '/files/http_cache');
+    $kernel = new HttpCache($kernel, new Store($store_path));
+  }
+
   // Create a request object from the HttpFoundation.
   $request = Request::createFromGlobals();
   $response = $kernel->handle($request)->prepare($request)->send();
@@ -1919,6 +1822,48 @@ function drupal_handle_request($test_only = FALSE) {
 }
 
 /**
+ * Instantiates and statically caches the correct class for a cache bin.
+ *
+ * By default, this returns an instance of the Drupal\Core\Cache\DatabaseBackend
+ * class.
+ *
+ * Classes implementing Drupal\Core\Cache\CacheBackendInterface can register
+ * themselves both as a default implementation and for specific bins.
+ *
+ * @param $bin
+ *   The cache bin for which the cache object should be returned, defaults to
+ *   'cache'.
+ *
+ * @return Drupal\Core\Cache\CacheBackendInterface
+ *   The cache object associated with the specified bin.
+ *
+ * @see Drupal\Core\Cache\CacheBackendInterface
+ */
+function cache($bin = 'cache') {
+  return Drupal::cache($bin);
+}
+
+/**
+ * Marks cache items from all bins with any of the specified tags as invalid.
+ *
+ * Many sites have more than one active cache backend, and each backend my use
+ * a different strategy for storing tags against cache items, and invalidating
+ * cache items associated with a given tag.
+ *
+ * When invalidating a given list of tags, we iterate over each cache backend,
+ * and call invalidateTags() on each.
+ *
+ * @param array $tags
+ *   The list of tags to invalidate cache items for.
+ *
+ * @deprecated 8.x
+ *   Use \Drupal\Core\Cache\Cache::invalidateTags().
+ */
+function cache_invalidate_tags(array $tags) {
+  Cache::invalidateTags($tags);
+}
+
+/**
  * Returns the time zone of the current user.
  */
 function drupal_get_user_timezone() {
@@ -2046,52 +1991,6 @@ function _drupal_bootstrap_kernel() {
 }
 
 /**
- * Attempts to serve a page from the cache.
- */
-function _drupal_bootstrap_page_cache() {
-  global $user;
-
-  // Allow specifying special cache handlers in settings.php, like
-  // using memcached or files for storing cache information.
-  require_once __DIR__ . '/cache.inc';
-  foreach (variable_get('cache_backends', array()) as $include) {
-    require_once DRUPAL_ROOT . '/' . $include;
-  }
-  // Check for a cache mode force from settings.php.
-  if (settings()->get('page_cache_without_database')) {
-    $cache_enabled = TRUE;
-  }
-  else {
-    drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES, FALSE);
-    $config = config('system.performance');
-    $cache_enabled = $config->get('cache.page.use_internal');
-  }
-  // If there is no session cookie and cache is enabled (or forced), try
-  // to serve a cached page.
-  if (!isset($_COOKIE[session_name()]) && $cache_enabled) {
-    // Make sure there is a user object because its timestamp will be checked.
-    $user = drupal_anonymous_user();
-    // Get the page from the cache.
-    $cache = drupal_page_get_cache();
-    // If there is a cached page, display it.
-    if (is_object($cache)) {
-      header('X-Drupal-Cache: HIT');
-      // Restore the metadata cached with the page.
-      _current_path($cache->data['path']);
-      drupal_set_title($cache->data['title'], PASS_THROUGH);
-      date_default_timezone_set(drupal_get_user_timezone());
-
-      drupal_serve_page_from_cache($cache);
-      // We are done.
-      exit;
-    }
-    else {
-      header('X-Drupal-Cache: MISS');
-    }
-  }
-}
-
-/**
  * In a test environment, get the test db prefix and set it in $databases.
  */
 function _drupal_initialize_db_test_prefix() {
diff --git a/core/includes/cache.inc b/core/includes/cache.inc
deleted file mode 100644
index edc47c7..0000000
--- a/core/includes/cache.inc
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php
-
-/**
- * @file
- * Functions and interfaces for cache handling.
- */
-
-use Drupal\Core\Cache\Cache;
-
-/**
- * Instantiates and statically caches the correct class for a cache bin.
- *
- * By default, this returns an instance of the Drupal\Core\Cache\DatabaseBackend
- * class.
- *
- * Classes implementing Drupal\Core\Cache\CacheBackendInterface can register
- * themselves both as a default implementation and for specific bins.
- *
- * @param $bin
- *   The cache bin for which the cache object should be returned, defaults to
- *   'cache'.
- *
- * @return Drupal\Core\Cache\CacheBackendInterface
- *   The cache object associated with the specified bin.
- *
- * @see Drupal\Core\Cache\CacheBackendInterface
- */
-function cache($bin = 'cache') {
-  return Drupal::cache($bin);
-}
-
-/**
- * Marks cache items from all bins with any of the specified tags as invalid.
- *
- * Many sites have more than one active cache backend, and each backend my use
- * a different strategy for storing tags against cache items, and invalidating
- * cache items associated with a given tag.
- *
- * When invalidating a given list of tags, we iterate over each cache backend,
- * and call invalidateTags() on each.
- *
- * @param array $tags
- *   The list of tags to invalidate cache items for.
- *
- * @deprecated 8.x
- *   Use \Drupal\Core\Cache\Cache::invalidateTags().
- */
-function cache_invalidate_tags(array $tags) {
-  Cache::invalidateTags($tags);
-}
diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc
index 9b95a47..225d9bf 100644
--- a/core/includes/install.core.inc
+++ b/core/includes/install.core.inc
@@ -421,8 +421,6 @@ function install_begin_request(&$install_state) {
   }
   $module_handler->load('system');
 
-  require_once __DIR__ . '/cache.inc';
-
   // Prepare for themed output. We need to run this at the beginning of the
   // page request to avoid a different theme accidentally getting set. (We also
   // need to run it even in the case of command-line installations, to prevent
diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index 163d5c8..653f4e1 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -56,6 +56,26 @@ public function onRespond(FilterResponseEvent $event) {
     // Set the Content-language header.
     $response->headers->set('Content-language', $this->languageManager->getLanguage(Language::TYPE_INTERFACE)->langcode);
 
+    // Max age of 0 means to disable all caching. That is rarely advisable
+    // except during development.
+    // Note: We have to do this one first, because as soon as we set a
+    // cache-related header (such as expires or last-modified, below) the
+    // cache defaults to private. Once we set the cache to public, though,
+    // future cache settings won't revert to that default. That allows a
+    // controller to return a response with custom headers on it already,
+    // including a private cache, while allowing the majority of pages to
+    // use the default aggressive cache settings.
+    $max_age = config('system.performance')->get('cache.page.max_age');
+    if ($max_age > 0) {
+      $this->setCacheHeaders($response, $event, $max_age);
+    }
+
+    // Set a default response time in the past, so that pages are uncachable
+    // unless the Cache-Control headers say otherwise. Those take priority.
+    if (!$response->headers->has('Expires')) {
+      $response->headers->set('Expires', 'Sun, 19 Nov 1978 05:00:00 GMT');
+    }
+
     // Because pages are highly dynamic, set the last-modified time to now
     // since the page is in fact being regenerated right now.
     // @todo Remove this and use a more intelligent default so that HTTP
@@ -92,16 +112,41 @@ public function onRespond(FilterResponseEvent $event) {
     //   use partial page caching more extensively.
     // Commit the user session, if needed.
     drupal_session_commit();
-    $max_age = config('system.performance')->get('cache.page.max_age');
-    if ($max_age > 0 && ($cache = drupal_page_set_cache($response->getContent()))) {
-      drupal_serve_page_from_cache($cache);
-      // drupal_serve_page_from_cache() already printed the response.
-      $response->setContent('');
-      $response->headers->remove('cache-control');
+
+  }
+
+  /**
+   * Sets appropriate cache headers on a response object.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   The response on which to set headers.
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The response event.
+   * @param int $max_age
+   *   The max age in seconds for which the response should be cached.
+   */
+  protected function setCacheHeaders($response, $event, $max_age) {
+    $response->isNotModified($event->getRequest());
+
+    // Set the Vary headers to encoding (mime type) and cookie. If they're
+    // already set, though, leave them as is.
+    if(!$response->hasVary()) {
+      $vary = array('Accept-Encoding');
+      if (!settings()->get('cache.page.omit_vary_cookie')) {
+        $vary[] = 'Cookie';
+      }
+      $response->setVary($vary);
     }
-    else {
-      $response->headers->set('Expires', 'Sun, 19 Nov 1978 05:00:00 GMT');
-      $response->headers->set('Cache-Control', 'no-cache, must-revalidate, post-check=0, pre-check=0');
+
+    // Default to public cache, meaning intermediaries between us and the
+    // browser may cache it.
+
+    if (!$response->headers->hasCacheControlDirective('private')) {
+      $response->setPublic();
+    }
+
+    if (!$response->getMaxAge()) {
+      $response->setMaxAge($max_age);
     }
   }
 
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index c9f0c22..03e9d5a 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -113,43 +113,24 @@ function toolbar_element_info() {
 }
 
 /**
- * Use Drupal's page cache for toolbar/subtrees/*, even for authenticated users.
+ * Access callback: Returns if the user has access to the rendered subtree requested by the hash.
  *
- * This gets invoked after full bootstrap, so must duplicate some of what's
- * done by _drupal_bootstrap_page_cache().
- *
- * @todo Replace this hack with something better integrated with DrupalKernel
- *   once Drupal's page caching itself is properly integrated.
+ * @see toolbar_menu().
  */
-function _toolbar_initialize_page_cache() {
-  $GLOBALS['conf']['system.performance']['cache']['page']['enabled'] = TRUE;
-  drupal_page_is_cacheable(TRUE);
-
-  // If we have a cache, serve it.
-  // @see _drupal_bootstrap_page_cache()
-  $cache = drupal_page_get_cache();
-  if (is_object($cache)) {
-    header('X-Drupal-Cache: HIT');
-    // Restore the metadata cached with the page.
-    $_GET['q'] = $cache->data['path'];
-    date_default_timezone_set(drupal_get_user_timezone());
-
-    drupal_serve_page_from_cache($cache);
-
-    // We are done.
-    exit;
-  }
-
-  // Otherwise, create a new page response (that will be cached).
-  header('X-Drupal-Cache: MISS');
+function _toolbar_subtrees_access($hash) {
+  return user_access('access toolbar') && ($hash == _toolbar_get_subtree_hash());
+}
 
-  // The Expires HTTP header is the heart of the client-side HTTP caching. The
-  // additional server-side page cache only takes effect when the client
-  // accesses the callback URL again (e.g., after clearing the browser cache or
-  // when force-reloading a Drupal page).
-  $max_age = 3600 * 24 * 365;
-  drupal_add_http_header('Expires', gmdate(DATE_RFC1123, REQUEST_TIME + $max_age));
-  drupal_add_http_header('Cache-Control', 'private, max-age=' . $max_age);
+/**
+ * Page callback: Returns the rendered subtree of each top-level toolbar link.
+ *
+ * @see toolbar_menu().
+ */
+function toolbar_subtrees_jsonp($hash) {
+  $subtrees = toolbar_get_rendered_subtrees();
+  $response = new JsonResponse($subtrees);
+  $response->setCallback('Drupal.toolbar.setSubtrees');
+  return $response;
 }
 
 /**
