diff --git a/core/core.services.yml b/core/core.services.yml
index 5ba01167a5b8e58ef9e1c7ee16b7817f8172bbce..9d40627214fc7ffdcbb601cf107b2b9d442932f2 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -62,6 +62,11 @@ services:
     arguments: ['@request_stack']
     tags:
       - { name: cache.context }
+  cache_context.url.path:
+    class: Drupal\Core\Cache\Context\PathCacheContext
+    arguments: ['@request_stack']
+    tags:
+      - { name: cache.context }
   cache_context.url.query_args:
     class: Drupal\Core\Cache\Context\QueryArgsCacheContext
     arguments: ['@request_stack']
diff --git a/core/lib/Drupal/Core/Breadcrumb/Breadcrumb.php b/core/lib/Drupal/Core/Breadcrumb/Breadcrumb.php
new file mode 100644
index 0000000000000000000000000000000000000000..14188070c8459b9c2a377937f24461849eaa6099
--- /dev/null
+++ b/core/lib/Drupal/Core/Breadcrumb/Breadcrumb.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Breadcrumb\Breadcrumb.
+ */
+
+namespace Drupal\Core\Breadcrumb;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Link;
+
+/**
+ * Used to return generated breadcrumbs with associated cacheability metadata.
+ *
+ * @todo implement RenderableInterface once https://www.drupal.org/node/2529560 lands.
+ */
+class Breadcrumb extends CacheableMetadata {
+
+  /**
+   * An ordered list of links for the breadcrumb.
+   *
+   * @var \Drupal\Core\Link[]
+   */
+  protected $links = [];
+
+  /**
+   * Gets the breadcrumb links.
+   *
+   * @return \Drupal\Core\Link[]
+   */
+  public function getLinks() {
+    return $this->links;
+  }
+
+  /**
+   * Sets the breadcrumb links.
+   *
+   * @param \Drupal\Core\Link[] $links
+   *   The breadcrumb links.
+   *
+   * @return $this
+   *
+   * @throws \LogicException
+   *   Thrown when setting breadcrumb links after they've already been set.
+   */
+  public function setLinks(array $links) {
+    if (!empty($this->links)) {
+      throw new \LogicException('Once breadcrumb links are set, only additional breadcrumb links can be added.');
+    }
+
+    $this->links = $links;
+
+    return $this;
+  }
+
+  /**
+   * Appends a link to the end of the ordered list of breadcrumb links.
+   *
+   * @param \Drupal\Core\Link $link
+   *   The link appended to the breadcrumb.
+   *
+   * @return $this
+   */
+  public function addLink(Link $link) {
+    $this->links[] = $link;
+
+    return $this;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php
index ebdfa5569a52bd901ceefc0a69e4dae06c32b8f1..e566f54fee3cbc7c1a217ebe95b5dfc6042f5a94 100644
--- a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php
+++ b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbBuilderInterface.php
@@ -32,9 +32,8 @@ public function applies(RouteMatchInterface $route_match);
    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
    *   The current route match.
    *
-   * @return \Drupal\Core\Link[]
-   *   An array of links for the breadcrumb. Returning an empty array will
-   *   suppress all breadcrumbs.
+   * @return \Drupal\Core\Breadcrumb\Breadcrumb
+   *   A breadcrumb.
    */
   public function build(RouteMatchInterface $route_match);
 
diff --git a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php
index 0015bf3cedcae4d82444acdbbacf6fa83033a7f0..3742363917e4b42775cef0fdc55f296dd2015afe 100644
--- a/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php
+++ b/core/lib/Drupal/Core/Breadcrumb/BreadcrumbManager.php
@@ -75,7 +75,7 @@ public function applies(RouteMatchInterface $route_match) {
    * {@inheritdoc}
    */
   public function build(RouteMatchInterface $route_match) {
-    $breadcrumb = array();
+    $breadcrumb = new Breadcrumb();
     $context = array('builder' => NULL);
     // Call the build method of registered breadcrumb builders,
     // until one of them returns an array.
@@ -85,11 +85,9 @@ public function build(RouteMatchInterface $route_match) {
         continue;
       }
 
-      $build = $builder->build($route_match);
+      $breadcrumb = $builder->build($route_match);
 
-      if (is_array($build)) {
-        // The builder returned an array of breadcrumb links.
-        $breadcrumb = $build;
+      if ($breadcrumb instanceof Breadcrumb) {
         $context['builder'] = $builder;
         break;
       }
@@ -99,7 +97,7 @@ public function build(RouteMatchInterface $route_match) {
     }
     // Allow modules to alter the breadcrumb.
     $this->moduleHandler->alter('system_breadcrumb', $breadcrumb, $route_match, $context);
-    // Fall back to an empty breadcrumb.
+
     return $breadcrumb;
   }
 
diff --git a/core/lib/Drupal/Core/Cache/Context/PathCacheContext.php b/core/lib/Drupal/Core/Cache/Context/PathCacheContext.php
new file mode 100644
index 0000000000000000000000000000000000000000..64a221a48857d3e347e4526eab33a90d7615b21e
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Context/PathCacheContext.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Cache\Context\PathCacheContext.
+ */
+
+namespace Drupal\Core\Cache\Context;
+
+use Drupal\Core\Cache\CacheableMetadata;
+
+/**
+ * Defines the PathCacheContext service, for "per URL path" caching.
+ *
+ * Cache context ID: 'url.path'.
+ *
+ * (This allows for caching relative URLs.)
+ *
+ * @see \Symfony\Component\HttpFoundation\Request::getBasePath()
+ * @see \Symfony\Component\HttpFoundation\Request::getPathInfo()
+ */
+class PathCacheContext extends RequestStackCacheContextBase implements CacheContextInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getLabel() {
+    return t('Path');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContext() {
+    $request = $this->requestStack->getCurrentRequest();
+    return $request->getBasePath() . $request->getPathInfo();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheableMetadata() {
+    return new CacheableMetadata();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Menu/menu.api.php b/core/lib/Drupal/Core/Menu/menu.api.php
index a2e9bb0e1e0453dc1e310db11edc046384451f49..e68b7951fc3c7b75b5314b36597c0a3a111cb0ea 100644
--- a/core/lib/Drupal/Core/Menu/menu.api.php
+++ b/core/lib/Drupal/Core/Menu/menu.api.php
@@ -562,12 +562,8 @@ function hook_contextual_links_plugins_alter(array &$contextual_links) {
 /**
  * Perform alterations to the breadcrumb built by the BreadcrumbManager.
  *
- * @param array $breadcrumb
- *   An array of breadcrumb link a tags, returned by the breadcrumb manager
- *   build method, for example
- *   @code
- *     array('<a href="/">Home</a>');
- *   @endcode
+ * @param \Drupal\Core\Breadcrumb\Breadcrumb $breadcrumb
+ *   A breadcrumb object returned by BreadcrumbBuilderInterface::build().
  * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
  *   The current route match.
  * @param array $context
@@ -578,9 +574,9 @@ function hook_contextual_links_plugins_alter(array &$contextual_links) {
  *
  * @ingroup menu
  */
-function hook_system_breadcrumb_alter(array &$breadcrumb, \Drupal\Core\Routing\RouteMatchInterface $route_match, array $context) {
+function hook_system_breadcrumb_alter(\Drupal\Core\Breadcrumb\Breadcrumb &$breadcrumb, \Drupal\Core\Routing\RouteMatchInterface $route_match, array $context) {
   // Add an item to the end of the breadcrumb.
-  $breadcrumb[] = Drupal::l(t('Text'), 'example_route_name');
+  $breadcrumb->addLink(Drupal::l(t('Text'), 'example_route_name'));
 }
 
 /**
diff --git a/core/modules/book/book.services.yml b/core/modules/book/book.services.yml
index 0a022a78f9d502d30d1f10e9820ee80859b2b19c..06affbb2777befd9d393091eb993eae804de808d 100644
--- a/core/modules/book/book.services.yml
+++ b/core/modules/book/book.services.yml
@@ -26,6 +26,8 @@ services:
   cache_context.route.book_navigation:
     class: Drupal\book\Cache\BookNavigationCacheContext
     arguments: ['@request_stack']
+    calls:
+      - [setContainer, ['@service_container']]
     tags:
       - { name: cache.context}
 
diff --git a/core/modules/book/src/BookBreadcrumbBuilder.php b/core/modules/book/src/BookBreadcrumbBuilder.php
index be0e63a866b7d9464b4dc2c2a417a72d4cfda426..b1ece44eba0a4a168e25d0d4bda8ae396c208bdc 100644
--- a/core/modules/book/src/BookBreadcrumbBuilder.php
+++ b/core/modules/book/src/BookBreadcrumbBuilder.php
@@ -8,6 +8,7 @@
 namespace Drupal\book;
 
 use Drupal\Core\Access\AccessManagerInterface;
+use Drupal\Core\Breadcrumb\Breadcrumb;
 use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Link;
@@ -72,6 +73,8 @@ public function applies(RouteMatchInterface $route_match) {
    */
   public function build(RouteMatchInterface $route_match) {
     $book_nids = array();
+    $breadcrumb = new Breadcrumb();
+
     $links = array(Link::createFromRoute($this->t('Home'), '<front>'));
     $book = $route_match->getParameter('node')->book;
     $depth = 1;
@@ -92,7 +95,9 @@ public function build(RouteMatchInterface $route_match) {
         $depth++;
       }
     }
-    return $links;
+    $breadcrumb->setLinks($links);
+    $breadcrumb->setCacheContexts(['route.book_navigation']);
+    return $breadcrumb;
   }
 
 }
diff --git a/core/modules/book/src/Tests/BookTest.php b/core/modules/book/src/Tests/BookTest.php
index bfd3ae31b5d9b6b4969d34b06ee8cf3a9a9394e4..b0235c4ab9950621ef6c23504e197adeef737161 100644
--- a/core/modules/book/src/Tests/BookTest.php
+++ b/core/modules/book/src/Tests/BookTest.php
@@ -24,7 +24,7 @@ class BookTest extends WebTestBase {
    *
    * @var array
    */
-  public static $modules = array('book', 'block', 'node_access_test');
+  public static $modules = array('book', 'block', 'node_access_test', 'book_test');
 
   /**
    * A book node.
@@ -109,6 +109,45 @@ function createBook() {
     return $nodes;
   }
 
+  /**
+   * Tests the book navigation cache context.
+   *
+   * @see \Drupal\book\Cache\BookNavigationCacheContext
+   */
+  public function testBookNavigationCacheContext() {
+    // Create a page node.
+    $this->drupalCreateContentType(['type' => 'page']);
+    $page = $this->drupalCreateNode();
+
+    // Create a book, consisting of book nodes.
+    $book_nodes = $this->createBook();
+
+    // Enable the debug output.
+    \Drupal::state()->set('book_test.debug_book_navigation_cache_context', TRUE);
+
+    $this->drupalLogin($this->bookAuthor);
+
+    // On non-node route.
+    $this->drupalGet('');
+    $this->assertRaw('[route.book_navigation]=book.none');
+
+    // On non-book node route.
+    $this->drupalGet($page->urlInfo());
+    $this->assertRaw('[route.book_navigation]=book.none');
+
+    // On book node route.
+    $this->drupalGet($book_nodes[0]->urlInfo());
+    $this->assertRaw('[route.book_navigation]=0|2|3');
+    $this->drupalGet($book_nodes[1]->urlInfo());
+    $this->assertRaw('[route.book_navigation]=0|2|3|4');
+    $this->drupalGet($book_nodes[2]->urlInfo());
+    $this->assertRaw('[route.book_navigation]=0|2|3|5');
+    $this->drupalGet($book_nodes[3]->urlInfo());
+    $this->assertRaw('[route.book_navigation]=0|2|6');
+    $this->drupalGet($book_nodes[4]->urlInfo());
+    $this->assertRaw('[route.book_navigation]=0|2|7');
+  }
+
   /**
    * Tests saving the book outline on an empty book.
    */
@@ -303,7 +342,7 @@ function createBookNode($book_nid, $parent = NULL) {
     static $number = 0; // Used to ensure that when sorted nodes stay in same order.
 
     $edit = array();
-    $edit['title[0][value]'] = $number . ' - SimpleTest test node ' . $this->randomMachineName(10);
+    $edit['title[0][value]'] = str_pad($number, 2, '0', STR_PAD_LEFT) . ' - SimpleTest test node ' . $this->randomMachineName(10);
     $edit['body[0][value]'] = 'SimpleTest test body ' . $this->randomMachineName(32) . ' ' . $this->randomMachineName(32);
     $edit['book[bid]'] = $book_nid;
 
diff --git a/core/modules/book/tests/modules/book_test.info.yml b/core/modules/book/tests/modules/book_test.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..33001479558ee819b9e5047dff6063f0d5a5fc2d
--- /dev/null
+++ b/core/modules/book/tests/modules/book_test.info.yml
@@ -0,0 +1,6 @@
+name: 'Book module tests'
+type: module
+description: 'Support module for book module testing.'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/core/modules/book/tests/modules/book_test.module b/core/modules/book/tests/modules/book_test.module
new file mode 100644
index 0000000000000000000000000000000000000000..2f868a45d69337df7f81bb5d8bae02fc4b390fa4
--- /dev/null
+++ b/core/modules/book/tests/modules/book_test.module
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * @file
+ * Test module for testing the book module.
+ *
+ * This module's functionality depends on the following state variables:
+ * - book_test.debug_book_navigation_cache_context: Used in NodeQueryAlterTest to enable the
+ *   node_access_all grant realm.
+ *
+ * @see \Drupal\book\Tests\BookTest::testBookNavigationCacheContext()
+ */
+
+/**
+ * Implements hook_page_attachments().
+ */
+function book_test_page_attachments(array &$page) {
+  if (\Drupal::state()->get('book_test.debug_book_navigation_cache_context', FALSE)) {
+    drupal_set_message(\Drupal::service('cache_contexts_manager')->convertTokensToKeys(['route.book_navigation'])->getKeys()[0]);
+  }
+}
diff --git a/core/modules/comment/src/CommentBreadcrumbBuilder.php b/core/modules/comment/src/CommentBreadcrumbBuilder.php
index 8bc2f251ad1af7e1de9a068df61a5b5bdcc8f634..873b569e16e1f8255a83508d9d5ed4984e1b4172 100644
--- a/core/modules/comment/src/CommentBreadcrumbBuilder.php
+++ b/core/modules/comment/src/CommentBreadcrumbBuilder.php
@@ -8,6 +8,7 @@
 namespace Drupal\comment;
 
 use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
+use Drupal\Core\Breadcrumb\Breadcrumb;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Link;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -47,16 +48,20 @@ public function applies(RouteMatchInterface $route_match) {
    * {@inheritdoc}
    */
   public function build(RouteMatchInterface $route_match) {
-    $breadcrumb = [Link::createFromRoute($this->t('Home'), '<front>')];
+    $breadcrumb = new Breadcrumb();
+    $breadcrumb->setCacheContexts(['route']);
+    $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
 
     $entity = $route_match->getParameter('entity');
-    $breadcrumb[] = new Link($entity->label(), $entity->urlInfo());
+    $breadcrumb->addLink(new Link($entity->label(), $entity->urlInfo()));
+    $breadcrumb->addCacheableDependency($entity);
 
     if (($pid = $route_match->getParameter('pid')) && ($comment = $this->storage->load($pid))) {
       /** @var \Drupal\comment\CommentInterface $comment */
+      $breadcrumb->addCacheableDependency($comment);
       // Display link to parent comment.
       // @todo Clean-up permalink in https://www.drupal.org/node/2198041
-      $breadcrumb[] = new Link($comment->getSubject(), $comment->urlInfo());
+      $breadcrumb->addLink(new Link($comment->getSubject(), $comment->urlInfo()));
     }
 
     return $breadcrumb;
diff --git a/core/modules/forum/src/Breadcrumb/ForumBreadcrumbBuilderBase.php b/core/modules/forum/src/Breadcrumb/ForumBreadcrumbBuilderBase.php
index f595ee83047fe300543236e00bc5024a42b0d71b..f5fa2a83ef12d31e03ac8a9e584256f5f53875aa 100644
--- a/core/modules/forum/src/Breadcrumb/ForumBreadcrumbBuilderBase.php
+++ b/core/modules/forum/src/Breadcrumb/ForumBreadcrumbBuilderBase.php
@@ -8,6 +8,7 @@
 namespace Drupal\forum\Breadcrumb;
 
 use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
+use Drupal\Core\Breadcrumb\Breadcrumb;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Link;
@@ -65,14 +66,18 @@ public function __construct(EntityManagerInterface $entity_manager, ConfigFactor
    * {@inheritdoc}
    */
   public function build(RouteMatchInterface $route_match) {
-    $breadcrumb[] = Link::createFromRoute($this->t('Home'), '<front>');
+    $breadcrumb = new Breadcrumb();
+    $breadcrumb->setCacheContexts(['route']);
+
+    $links[] = Link::createFromRoute($this->t('Home'), '<front>');
 
     $vocabulary = $this->entityManager
       ->getStorage('taxonomy_vocabulary')
       ->load($this->config->get('vocabulary'));
-    $breadcrumb[] = Link::createFromRoute($vocabulary->label(), 'forum.index');
+    $breadcrumb->addCacheableDependency($vocabulary);
+    $links[] = Link::createFromRoute($vocabulary->label(), 'forum.index');
 
-    return $breadcrumb;
+    return $breadcrumb->setLinks($links);
   }
 
 }
diff --git a/core/modules/forum/src/Breadcrumb/ForumListingBreadcrumbBuilder.php b/core/modules/forum/src/Breadcrumb/ForumListingBreadcrumbBuilder.php
index 9d63772c7c8d57bb56c65e0a31bc578047e36308..494af46a04341a03534d6421690bb55ca760ea89 100644
--- a/core/modules/forum/src/Breadcrumb/ForumListingBreadcrumbBuilder.php
+++ b/core/modules/forum/src/Breadcrumb/ForumListingBreadcrumbBuilder.php
@@ -27,19 +27,26 @@ public function applies(RouteMatchInterface $route_match) {
    */
   public function build(RouteMatchInterface $route_match) {
     $breadcrumb = parent::build($route_match);
+    $breadcrumb->addCacheContexts(['route']);
 
     // Add all parent forums to breadcrumbs.
-    $term_id = $route_match->getParameter('taxonomy_term')->id();
+    /** @var \Drupal\Taxonomy\TermInterface $term */
+    $term = $route_match->getParameter('taxonomy_term');
+    $term_id = $term->id();
+    $breadcrumb->addCacheableDependency($term);
+
     $parents = $this->forumManager->getParents($term_id);
     if ($parents) {
       foreach (array_reverse($parents) as $parent) {
         if ($parent->id() != $term_id) {
-          $breadcrumb[] = Link::createFromRoute($parent->label(), 'forum.page', array(
+          $breadcrumb->addCacheableDependency($parent);
+          $breadcrumb->addLink(Link::createFromRoute($parent->label(), 'forum.page', [
             'taxonomy_term' => $parent->id(),
-          ));
+          ]));
         }
       }
     }
+
     return $breadcrumb;
   }
 
diff --git a/core/modules/forum/src/Breadcrumb/ForumNodeBreadcrumbBuilder.php b/core/modules/forum/src/Breadcrumb/ForumNodeBreadcrumbBuilder.php
index 090f0ea6b77c57aee275a7809b49ef297ddeacf7..5d6e5922c9897b940b89fd332db6d0837ecf708d 100644
--- a/core/modules/forum/src/Breadcrumb/ForumNodeBreadcrumbBuilder.php
+++ b/core/modules/forum/src/Breadcrumb/ForumNodeBreadcrumbBuilder.php
@@ -29,18 +29,21 @@ public function applies(RouteMatchInterface $route_match) {
    */
   public function build(RouteMatchInterface $route_match) {
     $breadcrumb = parent::build($route_match);
+    $breadcrumb->addCacheContexts(['route']);
 
     $parents = $this->forumManager->getParents($route_match->getParameter('node')->forum_tid);
     if ($parents) {
       $parents = array_reverse($parents);
       foreach ($parents as $parent) {
-        $breadcrumb[] = Link::createFromRoute($parent->label(), 'forum.page',
+        $breadcrumb->addCacheableDependency($parent);
+        $breadcrumb->addLink(Link::createFromRoute($parent->label(), 'forum.page',
           array(
             'taxonomy_term' => $parent->id(),
           )
-        );
+        ));
       }
     }
+
     return $breadcrumb;
   }
 
diff --git a/core/modules/forum/tests/src/Unit/Breadcrumb/ForumBreadcrumbBuilderBaseTest.php b/core/modules/forum/tests/src/Unit/Breadcrumb/ForumBreadcrumbBuilderBaseTest.php
index da7ff11b299d8a0525a0277b2e79a6c0483ccfd0..f690e03e27ef9613a85b0e020ec1d8912adeebca 100644
--- a/core/modules/forum/tests/src/Unit/Breadcrumb/ForumBreadcrumbBuilderBaseTest.php
+++ b/core/modules/forum/tests/src/Unit/Breadcrumb/ForumBreadcrumbBuilderBaseTest.php
@@ -7,8 +7,10 @@
 
 namespace Drupal\Tests\forum\Unit\Breadcrumb;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Link;
 use Drupal\Tests\UnitTestCase;
+use Symfony\Component\DependencyInjection\Container;
 
 /**
  * @coversDefaultClass \Drupal\forum\Breadcrumb\ForumBreadcrumbBuilderBase
@@ -16,6 +18,22 @@
  */
 class ForumBreadcrumbBuilderBaseTest extends UnitTestCase {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $cache_contexts_manager->expects($this->any())
+      ->method('validate_tokens');
+    $container = new Container();
+    $container->set('cache_contexts_manager', $cache_contexts_manager);
+    \Drupal::setContainer($container);
+  }
+
   /**
    * Tests ForumBreadcrumbBuilderBase::__construct().
    *
@@ -74,16 +92,18 @@ public function testBuild() {
       ->disableOriginalConstructor()
       ->getMock();
 
-    $vocab_item = $this->getMock('Drupal\taxonomy\VocabularyInterface');
-    $vocab_item->expects($this->any())
-      ->method('label')
-      ->will($this->returnValue('Fora_is_the_plural_of_forum'));
+    $prophecy = $this->prophesize('Drupal\taxonomy\VocabularyInterface');
+    $prophecy->label()->willReturn('Fora_is_the_plural_of_forum');
+    $prophecy->id()->willReturn(5);
+    $prophecy->getCacheTags()->willReturn(['taxonomy_vocabulary:5']);
+    $prophecy->getCacheContexts()->willReturn([]);
+    $prophecy->getCacheMaxAge()->willReturn(Cache::PERMANENT);
 
     $vocab_storage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
     $vocab_storage->expects($this->any())
       ->method('load')
       ->will($this->returnValueMap(array(
-        array('forums', $vocab_item),
+        array('forums', $prophecy->reveal()),
       )));
 
     $entity_manager = $this->getMockBuilder('Drupal\Core\Entity\EntityManagerInterface')
@@ -128,7 +148,11 @@ public function testBuild() {
     );
 
     // And finally, the test.
-    $this->assertEquals($expected, $breadcrumb_builder->build($route_match));
+    $breadcrumb = $breadcrumb_builder->build($route_match);
+    $this->assertEquals($expected, $breadcrumb->getLinks());
+    $this->assertEquals(['route'], $breadcrumb->getCacheContexts());
+    $this->assertEquals(['taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
+    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
   }
 
 }
diff --git a/core/modules/forum/tests/src/Unit/Breadcrumb/ForumListingBreadcrumbBuilderTest.php b/core/modules/forum/tests/src/Unit/Breadcrumb/ForumListingBreadcrumbBuilderTest.php
index 95c670f2457fc4fe7b76676ab2828bc1fbece0f4..0d201ed6a6add1a42e864869f9c5716146079664 100644
--- a/core/modules/forum/tests/src/Unit/Breadcrumb/ForumListingBreadcrumbBuilderTest.php
+++ b/core/modules/forum/tests/src/Unit/Breadcrumb/ForumListingBreadcrumbBuilderTest.php
@@ -7,9 +7,11 @@
 
 namespace Drupal\Tests\forum\Unit\Breadcrumb;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Link;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\DependencyInjection\Container;
 
 /**
  * @coversDefaultClass \Drupal\forum\Breadcrumb\ForumListingBreadcrumbBuilder
@@ -17,6 +19,22 @@
  */
 class ForumListingBreadcrumbBuilderTest extends UnitTestCase {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $cache_contexts_manager->expects($this->any())
+      ->method('validate_tokens');
+    $container = new Container();
+    $container->set('cache_contexts_manager', $cache_contexts_manager);
+    \Drupal::setContainer($container);
+  }
+
   /**
    * Tests ForumListingBreadcrumbBuilder::applies().
    *
@@ -105,25 +123,21 @@ public function providerTestApplies() {
    */
   public function testBuild() {
     // Build all our dependencies, backwards.
-    $term1 = $this->getMockBuilder('Drupal\taxonomy\Entity\Term')
-      ->disableOriginalConstructor()
-      ->getMock();
-    $term1->expects($this->any())
-      ->method('label')
-      ->will($this->returnValue('Something'));
-    $term1->expects($this->any())
-      ->method('id')
-      ->will($this->returnValue(1));
-
-    $term2 = $this->getMockBuilder('Drupal\taxonomy\Entity\Term')
-      ->disableOriginalConstructor()
-      ->getMock();
-    $term2->expects($this->any())
-      ->method('label')
-      ->will($this->returnValue('Something else'));
-    $term2->expects($this->any())
-      ->method('id')
-      ->will($this->returnValue(2));
+    $prophecy = $this->prophesize('Drupal\taxonomy\Entity\Term');
+    $prophecy->label()->willReturn('Something');
+    $prophecy->id()->willReturn(1);
+    $prophecy->getCacheTags()->willReturn(['taxonomy_term:1']);
+    $prophecy->getCacheContexts()->willReturn([]);
+    $prophecy->getCacheMaxAge()->willReturn(Cache::PERMANENT);
+    $term1 = $prophecy->reveal();
+
+    $prophecy = $this->prophesize('Drupal\taxonomy\Entity\Term');
+    $prophecy->label()->willReturn('Something else');
+    $prophecy->id()->willReturn(2);
+    $prophecy->getCacheTags()->willReturn(['taxonomy_term:2']);
+    $prophecy->getCacheContexts()->willReturn([]);
+    $prophecy->getCacheMaxAge()->willReturn(Cache::PERMANENT);
+    $term2 = $prophecy->reveal();
 
     $forum_manager = $this->getMock('Drupal\forum\ForumManagerInterface');
     $forum_manager->expects($this->at(0))
@@ -134,15 +148,17 @@ public function testBuild() {
       ->will($this->returnValue(array($term1, $term2)));
 
     // The root forum.
-    $vocab_item = $this->getMock('Drupal\taxonomy\VocabularyInterface');
-    $vocab_item->expects($this->any())
-      ->method('label')
-      ->will($this->returnValue('Fora_is_the_plural_of_forum'));
+    $prophecy = $this->prophesize('Drupal\taxonomy\VocabularyInterface');
+    $prophecy->label()->willReturn('Fora_is_the_plural_of_forum');
+    $prophecy->id()->willReturn(5);
+    $prophecy->getCacheTags()->willReturn(['taxonomy_vocabulary:5']);
+    $prophecy->getCacheContexts()->willReturn([]);
+    $prophecy->getCacheMaxAge()->willReturn(Cache::PERMANENT);
     $vocab_storage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
     $vocab_storage->expects($this->any())
       ->method('load')
       ->will($this->returnValueMap(array(
-        array('forums', $vocab_item),
+        array('forums', $prophecy->reveal()),
       )));
 
     $entity_manager = $this->getMockBuilder('Drupal\Core\Entity\EntityManagerInterface')
@@ -176,13 +192,13 @@ public function testBuild() {
     $breadcrumb_builder->setStringTranslation($translation_manager);
 
     // The forum listing we need a breadcrumb back from.
-    $forum_listing = $this->getMockBuilder('Drupal\taxonomy\Entity\Term')
-      ->disableOriginalConstructor()
-      ->getMock();
-    $forum_listing->tid = 23;
-    $forum_listing->expects($this->any())
-      ->method('label')
-      ->will($this->returnValue('You_should_not_see_this'));
+    $prophecy = $this->prophesize('Drupal\taxonomy\Entity\Term');
+    $prophecy->label()->willReturn('You_should_not_see_this');
+    $prophecy->id()->willReturn(23);
+    $prophecy->getCacheTags()->willReturn(['taxonomy_term:23']);
+    $prophecy->getCacheContexts()->willReturn([]);
+    $prophecy->getCacheMaxAge()->willReturn(Cache::PERMANENT);
+    $forum_listing = $prophecy->reveal();
 
     // Our data set.
     $route_match = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
@@ -197,7 +213,11 @@ public function testBuild() {
       Link::createFromRoute('Fora_is_the_plural_of_forum', 'forum.index'),
       Link::createFromRoute('Something', 'forum.page', array('taxonomy_term' => 1)),
     );
-    $this->assertEquals($expected1, $breadcrumb_builder->build($route_match));
+    $breadcrumb = $breadcrumb_builder->build($route_match);
+    $this->assertEquals($expected1, $breadcrumb->getLinks());
+    $this->assertEquals(['route'], $breadcrumb->getCacheContexts());
+    $this->assertEquals(['taxonomy_term:1', 'taxonomy_term:23', 'taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
+    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
 
     // Second test.
     $expected2 = array(
@@ -206,7 +226,12 @@ public function testBuild() {
       Link::createFromRoute('Something else', 'forum.page', array('taxonomy_term' => 2)),
       Link::createFromRoute('Something', 'forum.page', array('taxonomy_term' => 1)),
     );
-    $this->assertEquals($expected2, $breadcrumb_builder->build($route_match));
+    $breadcrumb = $breadcrumb_builder->build($route_match);
+    $this->assertEquals($expected2, $breadcrumb->getLinks());
+    $this->assertEquals(['route'], $breadcrumb->getCacheContexts());
+    $this->assertEquals(['taxonomy_term:1', 'taxonomy_term:2', 'taxonomy_term:23', 'taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
+    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
+
   }
 
 }
diff --git a/core/modules/forum/tests/src/Unit/Breadcrumb/ForumNodeBreadcrumbBuilderTest.php b/core/modules/forum/tests/src/Unit/Breadcrumb/ForumNodeBreadcrumbBuilderTest.php
index ec5dec078585efb9f861835ccbcb05c5e58089c4..76851fd2d23a5ae55f58648f73095c9cebdeec5c 100644
--- a/core/modules/forum/tests/src/Unit/Breadcrumb/ForumNodeBreadcrumbBuilderTest.php
+++ b/core/modules/forum/tests/src/Unit/Breadcrumb/ForumNodeBreadcrumbBuilderTest.php
@@ -7,9 +7,10 @@
 
 namespace Drupal\Tests\forum\Unit\Breadcrumb;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Link;
 use Drupal\Tests\UnitTestCase;
-use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\DependencyInjection\Container;
 
 /**
  * @coversDefaultClass \Drupal\forum\Breadcrumb\ForumNodeBreadcrumbBuilder
@@ -17,6 +18,22 @@
  */
 class ForumNodeBreadcrumbBuilderTest extends UnitTestCase {
 
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $cache_contexts_manager->expects($this->any())
+      ->method('validate_tokens');
+    $container = new Container();
+    $container->set('cache_contexts_manager', $cache_contexts_manager);
+    \Drupal::setContainer($container);
+  }
+
   /**
    * Tests ForumNodeBreadcrumbBuilder::applies().
    *
@@ -112,25 +129,21 @@ public function providerTestApplies() {
    */
   public function testBuild() {
     // Build all our dependencies, backwards.
-    $term1 = $this->getMockBuilder('Drupal\Core\Entity\EntityInterface')
-      ->disableOriginalConstructor()
-      ->getMock();
-    $term1->expects($this->any())
-      ->method('label')
-      ->will($this->returnValue('Something'));
-    $term1->expects($this->any())
-      ->method('id')
-      ->will($this->returnValue(1));
-
-    $term2 = $this->getMockBuilder('Drupal\Core\Entity\EntityInterface')
-      ->disableOriginalConstructor()
-      ->getMock();
-    $term2->expects($this->any())
-      ->method('label')
-      ->will($this->returnValue('Something else'));
-    $term2->expects($this->any())
-      ->method('id')
-      ->will($this->returnValue(2));
+    $prophecy = $this->prophesize('Drupal\taxonomy\Entity\Term');
+    $prophecy->label()->willReturn('Something');
+    $prophecy->id()->willReturn(1);
+    $prophecy->getCacheTags()->willReturn(['taxonomy_term:1']);
+    $prophecy->getCacheContexts()->willReturn([]);
+    $prophecy->getCacheMaxAge()->willReturn(Cache::PERMANENT);
+    $term1 = $prophecy->reveal();
+
+    $prophecy = $this->prophesize('Drupal\taxonomy\Entity\Term');
+    $prophecy->label()->willReturn('Something else');
+    $prophecy->id()->willReturn(2);
+    $prophecy->getCacheTags()->willReturn(['taxonomy_term:2']);
+    $prophecy->getCacheContexts()->willReturn([]);
+    $prophecy->getCacheMaxAge()->willReturn(Cache::PERMANENT);
+    $term2 = $prophecy->reveal();
 
     $forum_manager = $this->getMockBuilder('Drupal\forum\ForumManagerInterface')
       ->disableOriginalConstructor()
@@ -142,15 +155,17 @@ public function testBuild() {
       ->method('getParents')
       ->will($this->returnValue(array($term1, $term2)));
 
-    $vocab_item = $this->getMock('Drupal\taxonomy\VocabularyInterface');
-    $vocab_item->expects($this->any())
-      ->method('label')
-      ->will($this->returnValue('Forums'));
+    $prophecy = $this->prophesize('Drupal\taxonomy\VocabularyInterface');
+    $prophecy->label()->willReturn('Forums');
+    $prophecy->id()->willReturn(5);
+    $prophecy->getCacheTags()->willReturn(['taxonomy_vocabulary:5']);
+    $prophecy->getCacheContexts()->willReturn([]);
+    $prophecy->getCacheMaxAge()->willReturn(Cache::PERMANENT);
     $vocab_storage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
     $vocab_storage->expects($this->any())
       ->method('load')
       ->will($this->returnValueMap(array(
-        array('forums', $vocab_item),
+        array('forums', $prophecy->reveal()),
       )));
 
     $entity_manager = $this->getMockBuilder('Drupal\Core\Entity\EntityManagerInterface')
@@ -203,7 +218,11 @@ public function testBuild() {
       Link::createFromRoute('Forums', 'forum.index'),
       Link::createFromRoute('Something', 'forum.page', array('taxonomy_term' => 1)),
     );
-    $this->assertEquals($expected1, $breadcrumb_builder->build($route_match));
+    $breadcrumb = $breadcrumb_builder->build($route_match);
+    $this->assertEquals($expected1, $breadcrumb->getLinks());
+    $this->assertEquals(['route'], $breadcrumb->getCacheContexts());
+    $this->assertEquals(['taxonomy_term:1', 'taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
+    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
 
     // Second test.
     $expected2 = array(
@@ -212,7 +231,11 @@ public function testBuild() {
       Link::createFromRoute('Something else', 'forum.page', array('taxonomy_term' => 2)),
       Link::createFromRoute('Something', 'forum.page', array('taxonomy_term' => 1)),
     );
-    $this->assertEquals($expected2, $breadcrumb_builder->build($route_match));
+    $breadcrumb = $breadcrumb_builder->build($route_match);
+    $this->assertEquals($expected2, $breadcrumb->getLinks());
+    $this->assertEquals(['route'], $breadcrumb->getCacheContexts());
+    $this->assertEquals(['taxonomy_term:1', 'taxonomy_term:2', 'taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
+    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
   }
 
 }
diff --git a/core/modules/menu_ui/menu_ui.module b/core/modules/menu_ui/menu_ui.module
index f26dade62d1c85e5475c899146cacc790a2d0678..9baf1e7ee120cd39a194dc09df8181dafca47e02 100644
--- a/core/modules/menu_ui/menu_ui.module
+++ b/core/modules/menu_ui/menu_ui.module
@@ -8,6 +8,7 @@
  * used for navigation.
  */
 
+use Drupal\Core\Breadcrumb\Breadcrumb;
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Block\BlockPluginInterface;
@@ -486,14 +487,14 @@ function menu_ui_preprocess_block(&$variables) {
 /**
  * Implements hook_system_breadcrumb_alter().
  */
-function menu_ui_system_breadcrumb_alter(array &$breadcrumb, RouteMatchInterface $route_match, array $context) {
+function menu_ui_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context) {
   // Custom breadcrumb behavior for editing menu links, we append a link to
   // the menu in which the link is found.
   if (($route_match->getRouteName() == 'menu_ui.link_edit') && $menu_link = $route_match->getParameter('menu_link_plugin')) {
     if (($menu_link instanceof MenuLinkInterface)) {
       // Add a link to the menu admin screen.
       $menu = Menu::load($menu_link->getMenuName());
-      $breadcrumb[] = Link::createFromRoute($menu->label(), 'entity.menu.edit_form', array('menu' => $menu->id()));
+      $breadcrumb->addLink(Link::createFromRoute($menu->label(), 'entity.menu.edit_form', ['menu' => $menu->id()]));
     }
   }
 }
diff --git a/core/modules/system/src/PathBasedBreadcrumbBuilder.php b/core/modules/system/src/PathBasedBreadcrumbBuilder.php
index c6e51af86309b6d069fcea746e4943bffd1b68e8..4acbee2673ad361ad088911c71b946ecbe54aec6 100644
--- a/core/modules/system/src/PathBasedBreadcrumbBuilder.php
+++ b/core/modules/system/src/PathBasedBreadcrumbBuilder.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Access\AccessManagerInterface;
+use Drupal\Core\Breadcrumb\Breadcrumb;
 use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Controller\TitleResolverInterface;
@@ -125,6 +126,7 @@ public function applies(RouteMatchInterface $route_match) {
    * {@inheritdoc}
    */
   public function build(RouteMatchInterface $route_match) {
+    $breadcrumb = new Breadcrumb();
     $links = array();
 
     // General path-based breadcrumbs. Use the actual request path, prior to
@@ -139,17 +141,21 @@ public function build(RouteMatchInterface $route_match) {
     // /user is just a redirect, so skip it.
     // @todo Find a better way to deal with /user.
     $exclude['/user'] = TRUE;
+    // Because this breadcrumb builder is entirely path-based, vary by the
+    // 'url.path' cache context.
+    $breadcrumb->setCacheContexts(['url.path']);
     while (count($path_elements) > 1) {
       array_pop($path_elements);
       // Copy the path elements for up-casting.
       $route_request = $this->getRequestForPath('/' . implode('/', $path_elements), $exclude);
       if ($route_request) {
         $route_match = RouteMatch::createFromRequest($route_request);
-        $access = $this->accessManager->check($route_match, $this->currentUser);
-        if ($access) {
+        $access = $this->accessManager->check($route_match, $this->currentUser, NULL, TRUE);
+        // The set of breadcrumb links depends on the access result, so merge
+        // the access result's cacheability metadata.
+        $breadcrumb = $breadcrumb->addCacheableDependency($access);
+        if ($access->isAllowed()) {
           $title = $this->titleResolver->getTitle($route_request, $route_match->getRouteObject());
-        }
-        if ($access) {
           if (!isset($title)) {
             // Fallback to using the raw path component as the title if the
             // route is missing a _title or _title_callback attribute.
@@ -165,7 +171,8 @@ public function build(RouteMatchInterface $route_match) {
       // Add the Home link, except for the front page.
       $links[] = Link::createFromRoute($this->t('Home'), '<front>');
     }
-    return array_reverse($links);
+
+    return $breadcrumb->setLinks(array_reverse($links));
   }
 
   /**
diff --git a/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php b/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php
index c7629f08ee089dcd0b17e35960976211f18a4cce..40da61619bd26934ca9b4ffd1dfd2e8ec77e7a91 100644
--- a/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php
+++ b/core/modules/system/src/Plugin/Block/SystemBreadcrumbBlock.php
@@ -77,20 +77,13 @@ public function build() {
     $breadcrumb = $this->breadcrumbManager->build($this->routeMatch);
     if (!empty($breadcrumb)) {
       // $breadcrumb is expected to be an array of rendered breadcrumb links.
-      return array(
+      $build = [
         '#theme' => 'breadcrumb',
-        '#links' => $breadcrumb,
-      );
+        '#links' => $breadcrumb->getLinks(),
+      ];
+      $breadcrumb->applyTo($build);
+      return $build;
     }
   }
 
-  /**
-   * {@inheritdoc}
-   *
-   * @todo Make cacheable in https://www.drupal.org/node/2483183
-   */
-  public function getCacheMaxAge() {
-    return 0;
-  }
-
 }
diff --git a/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php b/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php
index 0a00029f2cfc6de4c83431d3facc7e402948d349..bf106f6afefeef3810e2407e0e9e11002ada4978 100644
--- a/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php
+++ b/core/modules/system/tests/src/Unit/Breadcrumbs/PathBasedBreadcrumbBuilderTest.php
@@ -7,8 +7,10 @@
 
 namespace Drupal\Tests\system\Unit\Breadcrumbs;
 
-use Drupal\Core\Link;
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Link;
+use Drupal\Core\Access\AccessResultAllowed;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\Core\Url;
@@ -16,6 +18,7 @@
 use Drupal\system\PathBasedBreadcrumbBuilder;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\DependencyInjection\Container;
 use Symfony\Component\HttpFoundation\ParameterBag;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\Routing\RequestContext;
@@ -117,6 +120,15 @@ protected function setUp() {
     );
 
     $this->builder->setStringTranslation($this->getStringTranslationStub());
+
+    $cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $cache_contexts_manager->expects($this->any())
+      ->method('validate_tokens');
+    $container = new Container();
+    $container->set('cache_contexts_manager', $cache_contexts_manager);
+    \Drupal::setContainer($container);
   }
 
   /**
@@ -129,8 +141,11 @@ public function testBuildOnFrontpage() {
       ->method('getPathInfo')
       ->will($this->returnValue('/'));
 
-    $links = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface'));
-    $this->assertEquals(array(), $links);
+    $breadcrumb = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface'));
+    $this->assertEquals([], $breadcrumb->getLinks());
+    $this->assertEquals(['url.path'], $breadcrumb->getCacheContexts());
+    $this->assertEquals([], $breadcrumb->getCacheTags());
+    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
   }
 
   /**
@@ -143,8 +158,11 @@ public function testBuildWithOnePathElement() {
       ->method('getPathInfo')
       ->will($this->returnValue('/example'));
 
-    $links = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface'));
-    $this->assertEquals(array(0 => new Link('Home', new Url('<front>'))), $links);
+    $breadcrumb = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface'));
+    $this->assertEquals([0 => new Link('Home', new Url('<front>'))], $breadcrumb->getLinks());
+    $this->assertEquals(['url.path'], $breadcrumb->getCacheContexts());
+    $this->assertEquals([], $breadcrumb->getCacheTags());
+    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
   }
 
   /**
@@ -175,8 +193,11 @@ public function testBuildWithTwoPathElements() {
 
     $this->setupAccessManagerToAllow();
 
-    $links = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface'));
-    $this->assertEquals(array(0 => new Link('Home', new Url('<front>')), 1 => new Link('Example', new Url('example'))), $links);
+    $breadcrumb = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface'));
+    $this->assertEquals([0 => new Link('Home', new Url('<front>')), 1 => new Link('Example', new Url('example'))], $breadcrumb->getLinks());
+    $this->assertEquals(['url.path', 'user.permissions'], $breadcrumb->getCacheContexts());
+    $this->assertEquals([], $breadcrumb->getCacheTags());
+    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
   }
 
   /**
@@ -213,14 +234,21 @@ public function testBuildWithThreePathElements() {
         }
       }));
 
-    $this->setupAccessManagerToAllow();
-
-    $links = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface'));
-    $this->assertEquals(array(
+    $this->accessManager->expects($this->any())
+      ->method('check')
+      ->willReturnOnConsecutiveCalls(
+        AccessResult::allowed()->cachePerPermissions(),
+        AccessResult::allowed()->addCacheContexts(['bar'])->addCacheTags(['example'])
+      );
+    $breadcrumb = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface'));
+    $this->assertEquals([
       new Link('Home', new Url('<front>')),
       new Link('Example', new Url('example')),
       new Link('Bar', new Url('example_bar')),
-    ), $links);
+    ], $breadcrumb->getLinks());
+    $this->assertEquals(['bar', 'url.path', 'user.permissions'], $breadcrumb->getCacheContexts());
+    $this->assertEquals(['example'], $breadcrumb->getCacheTags());
+    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
   }
 
   /**
@@ -241,10 +269,13 @@ public function testBuildWithException($exception_class, $exception_argument) {
       ->method('matchRequest')
       ->will($this->throwException(new $exception_class($exception_argument)));
 
-    $links = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface'));
+    $breadcrumb = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface'));
 
     // No path matched, though at least the frontpage is displayed.
-    $this->assertEquals(array(0 => new Link('Home', new Url('<front>'))), $links);
+    $this->assertEquals([0 => new Link('Home', new Url('<front>'))], $breadcrumb->getLinks());
+    $this->assertEquals(['url.path'], $breadcrumb->getCacheContexts());
+    $this->assertEquals([], $breadcrumb->getCacheTags());
+    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
   }
 
   /**
@@ -282,10 +313,13 @@ public function testBuildWithNonProcessedPath() {
       ->method('matchRequest')
       ->will($this->returnValue(array()));
 
-    $links = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface'));
+    $breadcrumb = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface'));
 
     // No path matched, though at least the frontpage is displayed.
-    $this->assertEquals(array(0 => new Link('Home', new Url('<front>'))), $links);
+    $this->assertEquals([0 => new Link('Home', new Url('<front>'))], $breadcrumb->getLinks());
+    $this->assertEquals(['url.path'], $breadcrumb->getCacheContexts());
+    $this->assertEquals([], $breadcrumb->getCacheTags());
+    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
   }
 
   /**
@@ -329,8 +363,11 @@ public function testBuildWithUserPath() {
       ->with($this->anything(), $route_1)
       ->will($this->returnValue('Admin'));
 
-    $links = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface'));
-    $this->assertEquals(array(0 => new Link('Home', new Url('<front>')), 1 => new Link('Admin', new Url('user_page'))), $links);
+    $breadcrumb = $this->builder->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface'));
+    $this->assertEquals([0 => new Link('Home', new Url('<front>')), 1 => new Link('Admin', new Url('user_page'))], $breadcrumb->getLinks());
+    $this->assertEquals(['url.path', 'user.permissions'], $breadcrumb->getCacheContexts());
+    $this->assertEquals([], $breadcrumb->getCacheTags());
+    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
   }
 
   /**
@@ -339,7 +376,7 @@ public function testBuildWithUserPath() {
   public function setupAccessManagerToAllow() {
     $this->accessManager->expects($this->any())
       ->method('check')
-      ->willReturn(TRUE);
+      ->willReturn((new AccessResultAllowed())->cachePerPermissions());
   }
 
   protected function setupStubPathProcessor() {
diff --git a/core/modules/taxonomy/src/TermBreadcrumbBuilder.php b/core/modules/taxonomy/src/TermBreadcrumbBuilder.php
index c2e47a1d068744f751feb8164ffad0256ace8ac4..c2387c400fe48acff32a660cce8c8af3bd2c69f1 100644
--- a/core/modules/taxonomy/src/TermBreadcrumbBuilder.php
+++ b/core/modules/taxonomy/src/TermBreadcrumbBuilder.php
@@ -8,6 +8,7 @@
 namespace Drupal\taxonomy;
 
 use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
+use Drupal\Core\Breadcrumb\Breadcrumb;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Link;
 use Drupal\Core\Routing\RouteMatchInterface;
@@ -29,7 +30,7 @@ class TermBreadcrumbBuilder implements BreadcrumbBuilderInterface {
   /**
    * The taxonomy storage.
    *
-   * @var \Drupal\Core\Entity\EntityStorageInterface
+   * @var \Drupal\Taxonomy\TermStorageInterface
    */
   protected $termStorage;
 
@@ -56,18 +57,28 @@ public function applies(RouteMatchInterface $route_match) {
    * {@inheritdoc}
    */
   public function build(RouteMatchInterface $route_match) {
+    $breadcrumb = new Breadcrumb();
+    $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
     $term = $route_match->getParameter('taxonomy_term');
+    // Breadcrumb needs to have terms cacheable metadata as a cacheable
+    // dependency even though it is not shown in the breadcrumb because e.g. its
+    // parent might have changed.
+    $breadcrumb->addCacheableDependency($term);
     // @todo This overrides any other possible breadcrumb and is a pure
     //   hard-coded presumption. Make this behavior configurable per
     //   vocabulary or term.
-    $breadcrumb = array();
-    while ($parents = $this->termStorage->loadParents($term->id())) {
-      $term = array_shift($parents);
+    $parents = $this->termStorage->loadAllParents($term->id());
+    // Remove current term being accessed.
+    array_shift($parents);
+    foreach (array_reverse($parents) as $term) {
       $term = $this->entityManager->getTranslationFromContext($term);
-      $breadcrumb[] = Link::createFromRoute($term->getName(), 'entity.taxonomy_term.canonical', array('taxonomy_term' => $term->id()));
+      $breadcrumb->addCacheableDependency($term);
+      $breadcrumb->addLink(Link::createFromRoute($term->getName(), 'entity.taxonomy_term.canonical', array('taxonomy_term' => $term->id())));
     }
-    $breadcrumb[] = Link::createFromRoute($this->t('Home'), '<front>');
-    $breadcrumb = array_reverse($breadcrumb);
+
+    // This breadcrumb builder is based on a route parameter, and hence it
+    // depends on the 'route' cache context.
+    $breadcrumb->setCacheContexts(['route']);
 
     return $breadcrumb;
   }
diff --git a/core/tests/Drupal/Tests/Core/Breadcrumb/BreadcrumbManagerTest.php b/core/tests/Drupal/Tests/Core/Breadcrumb/BreadcrumbManagerTest.php
index a2cbbf0d5ae7a13f1ff502fde76b755e6952ee36..b91460bc4092836887b60cfc98532c25f02fbb3e 100644
--- a/core/tests/Drupal/Tests/Core/Breadcrumb/BreadcrumbManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/Breadcrumb/BreadcrumbManagerTest.php
@@ -7,7 +7,9 @@
 
 namespace Drupal\Tests\Core\Breadcrumb;
 
+use Drupal\Core\Breadcrumb\Breadcrumb;
 use Drupal\Core\Breadcrumb\BreadcrumbManager;
+use Drupal\Core\Cache\Cache;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -16,6 +18,13 @@
  */
 class BreadcrumbManagerTest extends UnitTestCase {
 
+  /**
+   * The breadcrumb object.
+   *
+   * @var \Drupal\Core\Breadcrumb\Breadcrumb
+   */
+  protected $breadcrumb;
+
   /**
    * The tested breadcrumb manager.
    *
@@ -36,14 +45,23 @@ class BreadcrumbManagerTest extends UnitTestCase {
   protected function setUp() {
     $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
     $this->breadcrumbManager = new BreadcrumbManager($this->moduleHandler);
+    $this->breadcrumb = new Breadcrumb();
   }
 
   /**
    * Tests the breadcrumb manager without any set breadcrumb.
    */
   public function testBuildWithoutBuilder() {
-    $result = $this->breadcrumbManager->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface'));
-    $this->assertEquals(array(), $result);
+    $route_match = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
+    $this->moduleHandler->expects($this->once())
+      ->method('alter')
+      ->with('system_breadcrumb', $this->breadcrumb, $route_match, ['builder' => NULL]);
+
+    $breadcrumb = $this->breadcrumbManager->build($this->getMock('Drupal\Core\Routing\RouteMatchInterface'));
+    $this->assertEquals([], $breadcrumb->getLinks());
+    $this->assertEquals([], $breadcrumb->getCacheContexts());
+    $this->assertEquals([], $breadcrumb->getCacheTags());
+    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
   }
 
   /**
@@ -51,7 +69,9 @@ public function testBuildWithoutBuilder() {
    */
   public function testBuildWithSingleBuilder() {
     $builder = $this->getMock('Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface');
-    $breadcrumb = array('<a href="/example">Test</a>');
+    $links = array('<a href="/example">Test</a>');
+    $this->breadcrumb->setLinks($links);
+    $this->breadcrumb->setCacheContexts(['foo'])->setCacheTags(['bar']);
 
     $builder->expects($this->once())
       ->method('applies')
@@ -59,17 +79,20 @@ public function testBuildWithSingleBuilder() {
 
     $builder->expects($this->once())
       ->method('build')
-      ->will($this->returnValue($breadcrumb));
+      ->willReturn($this->breadcrumb);
 
     $route_match = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
     $this->moduleHandler->expects($this->once())
       ->method('alter')
-      ->with('system_breadcrumb', $breadcrumb, $route_match, array('builder' => $builder));
+      ->with('system_breadcrumb', $this->breadcrumb, $route_match, array('builder' => $builder));
 
     $this->breadcrumbManager->addBuilder($builder, 0);
 
-    $result = $this->breadcrumbManager->build($route_match);
-    $this->assertEquals($breadcrumb, $result);
+    $breadcrumb = $this->breadcrumbManager->build($route_match);
+    $this->assertEquals($links, $breadcrumb->getLinks());
+    $this->assertEquals(['foo'], $breadcrumb->getCacheContexts());
+    $this->assertEquals(['bar'], $breadcrumb->getCacheTags());
+    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
   }
 
   /**
@@ -83,25 +106,30 @@ public function testBuildWithMultipleApplyingBuilders() {
       ->method('build');
 
     $builder2 = $this->getMock('Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface');
-    $breadcrumb2 = array('<a href="/example2">Test2</a>');
+    $links2 = array('<a href="/example2">Test2</a>');
+    $this->breadcrumb->setLinks($links2);
+    $this->breadcrumb->setCacheContexts(['baz'])->setCacheTags(['qux']);
     $builder2->expects($this->once())
       ->method('applies')
       ->will($this->returnValue(TRUE));
     $builder2->expects($this->once())
       ->method('build')
-      ->will($this->returnValue($breadcrumb2));
+      ->willReturn($this->breadcrumb);
 
     $route_match = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
 
     $this->moduleHandler->expects($this->once())
       ->method('alter')
-      ->with('system_breadcrumb', $breadcrumb2, $route_match, array('builder' => $builder2));
+      ->with('system_breadcrumb', $this->breadcrumb, $route_match, array('builder' => $builder2));
 
     $this->breadcrumbManager->addBuilder($builder1, 0);
     $this->breadcrumbManager->addBuilder($builder2, 10);
 
-    $result = $this->breadcrumbManager->build($route_match);
-    $this->assertEquals($breadcrumb2, $result);
+    $breadcrumb = $this->breadcrumbManager->build($route_match);
+    $this->assertEquals($links2, $breadcrumb->getLinks());
+    $this->assertEquals(['baz'], $breadcrumb->getCacheContexts());
+    $this->assertEquals(['qux'], $breadcrumb->getCacheTags());
+    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
   }
 
   /**
@@ -116,25 +144,30 @@ public function testBuildWithOneNotApplyingBuilders() {
       ->method('build');
 
     $builder2 = $this->getMock('Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface');
-    $breadcrumb2 = array('<a href="/example2">Test2</a>');
+    $links2 = ['<a href="/example2">Test2</a>'];
+    $this->breadcrumb->setLinks($links2);
+    $this->breadcrumb->setCacheContexts(['baz'])->setCacheTags(['qux']);
     $builder2->expects($this->once())
       ->method('applies')
       ->will($this->returnValue(TRUE));
     $builder2->expects($this->once())
       ->method('build')
-      ->will($this->returnValue($breadcrumb2));
+      ->willReturn($this->breadcrumb);
 
     $route_match = $this->getMock('Drupal\Core\Routing\RouteMatchInterface');
 
     $this->moduleHandler->expects($this->once())
       ->method('alter')
-      ->with('system_breadcrumb', $breadcrumb2, $route_match, array('builder' => $builder2));
+      ->with('system_breadcrumb', $this->breadcrumb, $route_match, array('builder' => $builder2));
 
     $this->breadcrumbManager->addBuilder($builder1, 10);
     $this->breadcrumbManager->addBuilder($builder2, 0);
 
-    $result = $this->breadcrumbManager->build($route_match);
-    $this->assertEquals($breadcrumb2, $result);
+    $breadcrumb = $this->breadcrumbManager->build($route_match);
+    $this->assertEquals($links2, $breadcrumb->getLinks());
+    $this->assertEquals(['baz'], $breadcrumb->getCacheContexts());
+    $this->assertEquals(['qux'], $breadcrumb->getCacheTags());
+    $this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/Breadcrumb/BreadcrumbTest.php b/core/tests/Drupal/Tests/Core/Breadcrumb/BreadcrumbTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..2399d0d3c56b05b45d2dd17a10ef1d9b6d2ffa0f
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Breadcrumb/BreadcrumbTest.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\Breadcrumb\BreadcrumbTest.
+ */
+
+namespace Drupal\Tests\Core\Breadcrumb;
+
+use Drupal\Core\Breadcrumb\Breadcrumb;
+use Drupal\Core\Link;
+use Drupal\Core\Url;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Breadcrumb\Breadcrumb
+ * @group Breadcrumb
+ */
+class BreadcrumbTest extends UnitTestCase {
+
+  /**
+   * @covers ::setLinks
+   * @expectedException \LogicException
+   * @expectedExceptionMessage Once breadcrumb links are set, only additional breadcrumb links can be added.
+   */
+  public function testSetLinks() {
+    $breadcrumb = new Breadcrumb();
+    $breadcrumb->setLinks([new Link('Home', Url::fromRoute('<front>'))]);
+    $breadcrumb->setLinks([new Link('None', Url::fromRoute('<none>'))]);
+  }
+
+}