diff --git a/core/lib/Drupal/Core/Controller/CacheableTitleResolverInterface.php b/core/lib/Drupal/Core/Controller/CacheableTitleResolverInterface.php
new file mode 100644
index 0000000000..9edf5473ff
--- /dev/null
+++ b/core/lib/Drupal/Core/Controller/CacheableTitleResolverInterface.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\Core\Controller;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines a class which knows how to resolve titles in a cacheable way.
+ */
+interface CacheableTitleResolverInterface {
+
+  /**
+   * Returns a static or dynamic title for the route with cacheable metadata.
+   *
+   * If the returned title can contain HTML that should not be escaped it should
+   * return a render array, for example:
+   * @code
+   * ['#markup' => 'title', '#allowed_tags' => ['em']]
+   * @endcode
+   * If the method returns a string and it is not marked safe then it will be
+   * auto-escaped.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object passed to the title callback.
+   * @param \Symfony\Component\Routing\Route $route
+   *   The route information of the route to fetch the title.
+   * @param \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata
+   *   The cacheable metadata to attach the cacheable metadata of the title to.
+   *
+   * @return array|string|null
+   *   The cacheable title for the route.
+   */
+  public function getCacheableTitle(Request $request, Route $route, CacheableMetadata &$cacheable_metadata);
+
+}
diff --git a/core/lib/Drupal/Core/Controller/TitleResolver.php b/core/lib/Drupal/Core/Controller/TitleResolver.php
index 954835d754..8fd45f4d65 100644
--- a/core/lib/Drupal/Core/Controller/TitleResolver.php
+++ b/core/lib/Drupal/Core/Controller/TitleResolver.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Core\Controller;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Symfony\Component\HttpFoundation\Request;
@@ -10,7 +11,7 @@
 /**
  * Provides the default implementation of the title resolver interface.
  */
-class TitleResolver implements TitleResolverInterface {
+class TitleResolver implements TitleResolverInterface, CacheableTitleResolverInterface {
   use StringTranslationTrait;
 
   /**
@@ -37,12 +38,44 @@ public function __construct(ControllerResolverInterface $controller_resolver, Tr
    * {@inheritdoc}
    */
   public function getTitle(Request $request, Route $route) {
+    return $this->doGetTitle($request, $route);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheableTitle(Request $request, Route $route, CacheableMetadata &$cacheable_metadata) {
+    return $this->doGetTitle($request, $route, $cacheable_metadata);
+  }
+
+  /**
+   * Returns a static or dynamic title for the route.
+   *
+   * This supports both cacheable and non-cacheable titles.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object passed to the title callback.
+   * @param \Symfony\Component\Routing\Route $route
+   *   The route information of the route to fetch the title.
+   * @param \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata
+   *   The cacheable metadata to attach the cacheable metadata of the title to.
+   *
+   * @return array|string|null
+   *   The cacheable title for the route.
+   *
+   * @see \Drupal\Core\Controller\CacheableTitleResolverInterface::getCacheableTitle()
+   * @see \Drupal\Core\Controller\TitleResolverInterface::getTitle()
+   */
+  protected function doGetTitle(Request $request, Route $route, CacheableMetadata &$cacheable_metadata = NULL) {
     $route_title = NULL;
     // A dynamic title takes priority. Route::getDefault() returns NULL if the
     // named default is not set.  By testing the value directly, we also avoid
     // trying to use empty values.
     if ($callback = $route->getDefault('_title_callback')) {
       $callable = $this->controllerResolver->getControllerFromDefinition($callback);
+      if ($cacheable_metadata) {
+        $request->attributes->set('cacheable_metadata', $cacheable_metadata);
+      }
       $arguments = $this->controllerResolver->getArguments($request, $callable);
       $route_title = call_user_func_array($callable, $arguments);
     }
@@ -57,6 +90,9 @@ public function getTitle(Request $request, Route $route) {
           $args['@' . $key] = $value;
           $args['%' . $key] = $value;
         }
+        if ($cacheable_metadata) {
+          $cacheable_metadata->addCacheContexts(['route']);
+        }
       }
       if ($title_arguments = $route->getDefault('_title_arguments')) {
         $args = array_merge($args, (array) $title_arguments);
diff --git a/core/lib/Drupal/Core/Entity/Controller/EntityController.php b/core/lib/Drupal/Core/Entity/Controller/EntityController.php
index 46cc730b9e..1a6fa70493 100644
--- a/core/lib/Drupal/Core/Entity/Controller/EntityController.php
+++ b/core/lib/Drupal/Core/Entity/Controller/EntityController.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Core\Entity\Controller;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Entity\EntityDescriptionInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityRepositoryInterface;
@@ -220,12 +221,15 @@ public function addBundleTitle(RouteMatchInterface $route_match, $entity_type_id
    *   The route match.
    * @param \Drupal\Core\Entity\EntityInterface $_entity
    *   (optional) An entity, passed in directly from the request attributes.
+   * @param \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata
+   *   (optional) The cacheable metadata for the title callback.
    *
    * @return string|null
    *   The title for the entity view page, if an entity was found.
    */
-  public function title(RouteMatchInterface $route_match, EntityInterface $_entity = NULL) {
-    if ($entity = $this->doGetEntity($route_match, $_entity)) {
+  public function title(RouteMatchInterface $route_match, EntityInterface $_entity = NULL, CacheableMetadata $cacheable_metadata = NULL) {
+    if ($entity = $this->doGetEntity($route_match, $_entity, $cacheable_metadata)) {
+      $cacheable_metadata->addCacheableDependency($entity);
       return $entity->label();
     }
   }
@@ -278,7 +282,7 @@ public function deleteTitle(RouteMatchInterface $route_match, EntityInterface $_
    *   The entity, if it is passed in directly or if the first parameter of the
    *   active route is an entity; otherwise, NULL.
    */
-  protected function doGetEntity(RouteMatchInterface $route_match, EntityInterface $_entity = NULL) {
+  protected function doGetEntity(RouteMatchInterface $route_match, EntityInterface $_entity = NULL, CacheableMetadata &$cacheable_metadata = NULL) {
     if ($_entity) {
       $entity = $_entity;
     }
@@ -290,8 +294,12 @@ protected function doGetEntity(RouteMatchInterface $route_match, EntityInterface
           break;
         }
       }
+      if ($cacheable_metadata) {
+        $cacheable_metadata->addCacheContexts(['route']);
+      }
     }
     if (isset($entity)) {
+      // @todo Add cacheable metadata here.
       return $this->entityRepository->getTranslationFromContext($entity);
     }
   }
diff --git a/core/modules/node/src/Controller/NodeViewController.php b/core/modules/node/src/Controller/NodeViewController.php
index b8aa1e5a97..f0d9f1a5f3 100644
--- a/core/modules/node/src/Controller/NodeViewController.php
+++ b/core/modules/node/src/Controller/NodeViewController.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\node\Controller;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\Controller\EntityViewController;
 use Drupal\Core\Entity\EntityManagerInterface;
@@ -101,11 +102,15 @@ public function view(EntityInterface $node, $view_mode = 'full', $langcode = NUL
    *
    * @param \Drupal\Core\Entity\EntityInterface $node
    *   The current node.
+   * @param \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata
+   *   (optional) The cacheable metadata for the title callback.
    *
    * @return string
    *   The page title.
    */
-  public function title(EntityInterface $node) {
+  public function title(EntityInterface $node, CacheableMetadata $cacheable_metadata = NULL) {
+    $cacheable_metadata->addCacheableDependency($node);
+    // @todo Add cacheable metadata for ::getTranslationFromContext()
     return $this->entityManager->getTranslationFromContext($node)->label();
   }
 
diff --git a/core/modules/node/src/Tests/NodeTypeTest.php b/core/modules/node/src/Tests/NodeTypeTest.php
index 70d3ac40b5..bbc91121c8 100644
--- a/core/modules/node/src/Tests/NodeTypeTest.php
+++ b/core/modules/node/src/Tests/NodeTypeTest.php
@@ -99,11 +99,18 @@ public function testNodeTypeEditing() {
     $this->assertRaw('Title', 'Title field was found.');
     $this->assertRaw('Body', 'Body field was found.');
 
+    $front_page_path = Url::fromRoute('<front>')->toString();
+
     // Rename the title field.
     $edit = [
       'title_label' => 'Foo',
     ];
     $this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type'));
+    $this->assertBreadcrumb('admin/structure/types/manage/page/fields', [
+      $front_page_path => 'Home',
+      'admin/structure/types' => 'Content types',
+      'admin/structure/types/manage/page' => 'Basic page',
+    ]);
 
     $this->drupalGet('node/add/page');
     $this->assertRaw('Foo', 'New title label was displayed.');
@@ -115,6 +122,11 @@ public function testNodeTypeEditing() {
       'description' => 'Lorem ipsum.',
     ];
     $this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type'));
+    $this->assertBreadcrumb('admin/structure/types/manage/page/fields', [
+      $front_page_path => 'Home',
+      'admin/structure/types' => 'Content types',
+      'admin/structure/types/manage/page' => 'Bar',
+    ]);
 
     $this->drupalGet('node/add');
     $this->assertRaw('Bar', 'New name was displayed.');
@@ -138,7 +150,6 @@ public function testNodeTypeEditing() {
     $this->drupalPostForm('admin/structure/types/manage/page/fields/node.page.body/delete', [], t('Delete'));
     // Resave the settings for this type.
     $this->drupalPostForm('admin/structure/types/manage/page', [], t('Save content type'));
-    $front_page_path = Url::fromRoute('<front>')->toString();
     $this->assertBreadcrumb('admin/structure/types/manage/page/fields', [
       $front_page_path => 'Home',
       'admin/structure/types' => 'Content types',
diff --git a/core/modules/system/src/PathBasedBreadcrumbBuilder.php b/core/modules/system/src/PathBasedBreadcrumbBuilder.php
index ebc5df0b75..6639c020c6 100644
--- a/core/modules/system/src/PathBasedBreadcrumbBuilder.php
+++ b/core/modules/system/src/PathBasedBreadcrumbBuilder.php
@@ -6,7 +6,9 @@
 use Drupal\Core\Access\AccessManagerInterface;
 use Drupal\Core\Breadcrumb\Breadcrumb;
 use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Controller\CacheableTitleResolverInterface;
 use Drupal\Core\Controller\TitleResolverInterface;
 use Drupal\Core\Link;
 use Drupal\Core\ParamConverter\ParamNotConvertedException;
@@ -69,7 +71,7 @@ class PathBasedBreadcrumbBuilder implements BreadcrumbBuilderInterface {
   /**
    * The title resolver.
    *
-   * @var \Drupal\Core\Controller\TitleResolverInterface
+   * @var \Drupal\Core\Controller\CacheableTitleResolverInterface
    */
   protected $titleResolver;
 
@@ -116,7 +118,7 @@ class PathBasedBreadcrumbBuilder implements BreadcrumbBuilderInterface {
    * @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
    *   The path matcher service.
    */
-  public function __construct(RequestContext $context, AccessManagerInterface $access_manager, RequestMatcherInterface $router, InboundPathProcessorInterface $path_processor, ConfigFactoryInterface $config_factory, TitleResolverInterface $title_resolver, AccountInterface $current_user, CurrentPathStack $current_path, PathMatcherInterface $path_matcher = NULL) {
+  public function __construct(RequestContext $context, AccessManagerInterface $access_manager, RequestMatcherInterface $router, InboundPathProcessorInterface $path_processor, ConfigFactoryInterface $config_factory, CacheableTitleResolverInterface $title_resolver, AccountInterface $current_user, CurrentPathStack $current_path, PathMatcherInterface $path_matcher = NULL) {
     $this->context = $context;
     $this->accessManager = $access_manager;
     $this->router = $router;
@@ -174,7 +176,12 @@ public function build(RouteMatchInterface $route_match) {
         // the access result's cacheability metadata.
         $breadcrumb = $breadcrumb->addCacheableDependency($access);
         if ($access->isAllowed()) {
-          $title = $this->titleResolver->getTitle($route_request, $route_match->getRouteObject());
+          $cacheable_metadata = new CacheableMetadata();
+          $title = $this->titleResolver->getCacheableTitle($route_request, $route_match->getRouteObject(), $cacheable_metadata);
+          $breadcrumb
+            ->addCacheContexts($cacheable_metadata->getCacheContexts())
+            ->addCacheTags($cacheable_metadata->getCacheTags())
+            ->mergeCacheMaxAge($cacheable_metadata->getCacheMaxAge());
           if (!isset($title)) {
             // Fallback to using the raw path component as the title if the
             // route is missing a _title or _title_callback attribute.
diff --git a/core/modules/system/tests/src/Functional/Menu/BreadcrumbTest.php b/core/modules/system/tests/src/Functional/Menu/BreadcrumbTest.php
index 0e493e5d68..c908288b66 100644
--- a/core/modules/system/tests/src/Functional/Menu/BreadcrumbTest.php
+++ b/core/modules/system/tests/src/Functional/Menu/BreadcrumbTest.php
@@ -381,4 +381,34 @@ public function testBreadCrumbs() {
     $this->assertEscaped('<script>alert(123);</script>');
   }
 
+  /**
+   * Tests breadcrumb cacheability.
+   */
+  function testBreadcrumbCacheability() {
+    // Create an article.
+    $node = $this->drupalCreateNode([
+      'type' => 'article',
+      'title' => 'Article Foo',
+    ]);
+
+    // Edit the article's title and create a new revision.
+    $node->setTitle('Article Bar');
+    $node->setNewRevision(TRUE);
+    $node->save();
+
+    // Open the Revisions tab so the breadcrumb is cached.
+    $this->drupalGet('node/' . $node->id() . '/revisions');
+    $this->assertText('Article Bar');
+
+    // Edit the article's title again and create a new revision.
+    $node->setTitle('Article Baz');
+    $node->setNewRevision(TRUE);
+    $node->save();
+
+    // Open the Revisions tab and check the breadcrumb.
+    $this->drupalGet('node/' . $node->id() . '/revisions');
+    $this->assertNoText('Article Bar');
+    $this->assertText('Article Baz');
+  }
+
 }
