diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module
index f6da17897e34ae3620fb7ad60cd8f3c2351cfc40..bbc1d8c120f17682623e4d8c707bdf781eefc8f8 100644
--- a/core/modules/shortcut/shortcut.module
+++ b/core/modules/shortcut/shortcut.module
@@ -8,6 +8,7 @@
 use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Url;
 use Drupal\shortcut\Entity\ShortcutSet;
@@ -306,10 +307,10 @@ function shortcut_preprocess_block(&$variables) {
  */
 function shortcut_preprocess_page_title(&$variables) {
   // Only display the shortcut link if the user has the ability to edit
-  // shortcuts and if the page's actual content is being shown (for example,
-  // we do not want to display it on "access denied" or "page not found"
-  // pages).
-  if (shortcut_set_edit_access()->isAllowed() && !\Drupal::request()->attributes->has('exception')) {
+  // shortcuts, the feature is enabled for the current theme and if the page's
+  // actual content is being shown (for example, we do not want to display it on
+  // "access denied" or "page not found" pages).
+  if (shortcut_set_edit_access()->isAllowed() && theme_get_setting('third_party_settings.shortcut.module_link') && !\Drupal::request()->attributes->has('exception')) {
     $link = Url::fromRouteMatch(\Drupal::routeMatch())->getInternalPath();
     $route_match = \Drupal::routeMatch();
 
@@ -324,6 +325,12 @@ function shortcut_preprocess_page_title(&$variables) {
 
     $shortcut_set = shortcut_current_displayed_set();
 
+    // Pages with the add or remove shortcut button need cache invalidation when
+    // a shortcut is added, edited, or removed.
+    $cacheability_metadata = CacheableMetadata::createFromRenderArray($variables);
+    $cacheability_metadata->addCacheTags(\Drupal::entityTypeManager()->getDefinition('shortcut')->getListCacheTags());
+    $cacheability_metadata->applyTo($variables);
+
     // Check if $link is already a shortcut and set $link_mode accordingly.
     $shortcuts = \Drupal::entityTypeManager()->getStorage('shortcut')->loadByProperties(['shortcut_set' => $shortcut_set->id()]);
     /** @var \Drupal\shortcut\ShortcutInterface $shortcut */
@@ -347,26 +354,24 @@ function shortcut_preprocess_page_title(&$variables) {
       $route_parameters = ['shortcut' => $shortcut_id];
     }
 
-    if (theme_get_setting('third_party_settings.shortcut.module_link')) {
-      $query += \Drupal::destination()->getAsArray();
-      $variables['title_suffix']['add_or_remove_shortcut'] = [
-        '#attached' => [
-          'library' => [
-            'shortcut/drupal.shortcut',
-          ],
+    $query += \Drupal::destination()->getAsArray();
+    $variables['title_suffix']['add_or_remove_shortcut'] = [
+      '#attached' => [
+        'library' => [
+          'shortcut/drupal.shortcut',
         ],
-        '#type' => 'link',
-        '#title' => new FormattableMarkup('<span class="shortcut-action__icon"></span><span class="shortcut-action__message">@text</span>', ['@text' => $link_text]),
-        '#url' => Url::fromRoute($route_name, $route_parameters),
-        '#options' => ['query' => $query],
-        '#attributes' => [
-          'class' => [
-            'shortcut-action',
-            'shortcut-action--' . $link_mode,
-          ],
+      ],
+      '#type' => 'link',
+      '#title' => new FormattableMarkup('<span class="shortcut-action__icon"></span><span class="shortcut-action__message">@text</span>', ['@text' => $link_text]),
+      '#url' => Url::fromRoute($route_name, $route_parameters),
+      '#options' => ['query' => $query],
+      '#attributes' => [
+        'class' => [
+          'shortcut-action',
+          'shortcut-action--' . $link_mode,
         ],
-      ];
-    }
+      ],
+    ];
   }
 }
 
@@ -380,52 +385,43 @@ function shortcut_toolbar() {
   $items['shortcuts'] = [
     '#cache' => [
       'contexts' => [
-        // Cacheable per user, because each user can have their own shortcut
-        // set, even if they cannot create or select a shortcut set, because
-        // an administrator may have assigned a non-default shortcut set.
-        'user',
+        'user.permissions',
       ],
     ],
   ];
 
   if ($user->hasPermission('access shortcuts')) {
-    $links = shortcut_renderable_links();
     $shortcut_set = shortcut_current_displayed_set();
-    \Drupal::service('renderer')->addCacheableDependency($items['shortcuts'], $shortcut_set);
-    $configure_link = NULL;
-    if (shortcut_set_edit_access($shortcut_set)->isAllowed()) {
-      $configure_link = [
+
+    $items['shortcuts'] += [
+      '#type' => 'toolbar_item',
+      'tab' => [
         '#type' => 'link',
-        '#title' => t('Edit shortcuts'),
-        '#url' => Url::fromRoute('entity.shortcut_set.customize_form', ['shortcut_set' => $shortcut_set->id()]),
-        '#options' => ['attributes' => ['class' => ['edit-shortcuts']]],
-      ];
-    }
-    if (!empty($links) || !empty($configure_link)) {
-      $items['shortcuts'] += [
-        '#type' => 'toolbar_item',
-        'tab' => [
-          '#type' => 'link',
-          '#title' => t('Shortcuts'),
-          '#url' => $shortcut_set->toUrl('collection'),
-          '#attributes' => [
-            'title' => t('Shortcuts'),
-            'class' => ['toolbar-icon', 'toolbar-icon-shortcut'],
-          ],
-        ],
-        'tray' => [
-          '#heading' => t('User-defined shortcuts'),
-          'shortcuts' => $links,
-          'configure' => $configure_link,
+        '#title' => t('Shortcuts'),
+        '#url' => $shortcut_set->toUrl('collection'),
+        '#attributes' => [
+          'title' => t('Shortcuts'),
+          'class' => ['toolbar-icon', 'toolbar-icon-shortcut'],
         ],
-        '#weight' => -10,
-        '#attached' => [
-          'library' => [
-            'shortcut/drupal.shortcut',
+      ],
+      'tray' => [
+        '#heading' => t('User-defined shortcuts'),
+        'children' => [
+          '#lazy_builder' => ['shortcut.lazy_builders:lazyLinks', []],
+          '#create_placeholder' => TRUE,
+          '#cache' => [
+            'keys' => ['shortcut_set_toolbar_links'],
+            'contexts' => ['user'],
           ],
         ],
-      ];
-    }
+      ],
+      '#weight' => -10,
+      '#attached' => [
+        'library' => [
+          'shortcut/drupal.shortcut',
+        ],
+      ],
+    ];
   }
 
   return $items;
diff --git a/core/modules/shortcut/shortcut.services.yml b/core/modules/shortcut/shortcut.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..49244aa440ef06e549ac177ad1e0fcb0491d7ac9
--- /dev/null
+++ b/core/modules/shortcut/shortcut.services.yml
@@ -0,0 +1,4 @@
+services:
+  shortcut.lazy_builders:
+    class: Drupal\shortcut\ShortcutLazyBuilders
+    arguments: ['@renderer']
diff --git a/core/modules/shortcut/src/ShortcutLazyBuilders.php b/core/modules/shortcut/src/ShortcutLazyBuilders.php
new file mode 100644
index 0000000000000000000000000000000000000000..e2672104c59fdfdde11d1a09e5523a3744740efa
--- /dev/null
+++ b/core/modules/shortcut/src/ShortcutLazyBuilders.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\shortcut;
+
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Security\TrustedCallbackInterface;
+use Drupal\Core\Url;
+
+/**
+ * Lazy builders for the shortcut module.
+ */
+class ShortcutLazyBuilders implements TrustedCallbackInterface {
+
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructs a new ShortcutLazyBuilders object.
+   *
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
+   */
+  public function __construct(RendererInterface $renderer) {
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function trustedCallbacks() {
+    return ['lazyLinks'];
+  }
+
+  /**
+   * #lazy_builder callback; builds shortcut toolbar links.
+   *
+   * @return array
+   *   A renderable array of shortcut links.
+   */
+  public function lazyLinks() {
+    $shortcut_set = shortcut_current_displayed_set();
+
+    $links = shortcut_renderable_links();
+
+    $configure_link = NULL;
+    if (shortcut_set_edit_access($shortcut_set)->isAllowed()) {
+      $configure_link = [
+        '#type' => 'link',
+        '#title' => t('Edit shortcuts'),
+        '#url' => Url::fromRoute('entity.shortcut_set.customize_form', ['shortcut_set' => $shortcut_set->id()]),
+        '#options' => ['attributes' => ['class' => ['edit-shortcuts']]],
+      ];
+    }
+
+    $build = [
+      'shortcuts' => $links,
+      'configure' => $configure_link,
+    ];
+    $this->renderer->addCacheableDependency($build, $shortcut_set);
+
+    return $build;
+  }
+
+}
diff --git a/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php b/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php
index 71720d4442e4b782bfa4ba54ae20a7e09dfb6cfb..52876a4e37ff71a370385165b1f1e9d21be505b8 100644
--- a/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php
+++ b/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php
@@ -3,7 +3,9 @@
 namespace Drupal\Tests\shortcut\Functional;
 
 use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Url;
 use Drupal\shortcut\Entity\Shortcut;
+use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
 use Drupal\Tests\system\Functional\Entity\EntityCacheTagsTestBase;
 use Drupal\user\Entity\Role;
 use Drupal\user\RoleInterface;
@@ -14,11 +16,12 @@
  * @group shortcut
  */
 class ShortcutCacheTagsTest extends EntityCacheTagsTestBase {
+  use AssertPageCacheContextsAndTagsTrait;
 
   /**
    * {@inheritdoc}
    */
-  public static $modules = ['shortcut'];
+  public static $modules = ['toolbar', 'shortcut', 'test_page_test', 'block'];
 
   /**
    * {@inheritdoc}
@@ -73,4 +76,121 @@ public function testEntityCreation() {
     $this->assertFalse(\Drupal::cache('render')->get('foo'), 'Creating a new shortcut invalidates the cache tag of the shortcut set.');
   }
 
+  /**
+   * Tests visibility and cacheability of shortcuts in the toolbar.
+   */
+  public function testToolbar() {
+    $this->drupalPlaceBlock('page_title_block', ['id' => 'title']);
+
+    $test_page_url = Url::fromRoute('test_page_test.test_page');
+    $this->verifyPageCache($test_page_url, 'MISS');
+    $this->verifyPageCache($test_page_url, 'HIT');
+
+    // Ensure that without enabling the shortcuts-in-page-title-link feature
+    // in the theme, the shortcut_list cache tag is not added to the page.
+    $this->drupalLogin($this->rootUser);
+    $this->drupalGet('admin/config/system/cron');
+    $expected_cache_tags = [
+      'block_view',
+      'config:block.block.title',
+      'config:block_list',
+      'config:shortcut.set.default',
+      'config:system.menu.admin',
+      'config:user.role.authenticated',
+      'rendered',
+      'user:' . $this->rootUser->id(),
+    ];
+    $this->assertCacheTags($expected_cache_tags);
+
+    \Drupal::configFactory()
+      ->getEditable('stark.settings')
+      ->set('third_party_settings.shortcut.module_link', TRUE)
+      ->save(TRUE);
+
+    // Add cron to the default shortcut set, now the shortcut list cache tag
+    // is expected.
+    $this->drupalGet('admin/config/system/cron');
+    $this->clickLink('Add to Default shortcuts');
+    $expected_cache_tags[] = 'config:shortcut_set_list';
+    $this->assertCacheTags($expected_cache_tags);
+
+    // Verify that users without the 'access shortcuts' permission can't see the
+    // shortcuts.
+    $this->drupalLogin($this->drupalCreateUser(['access toolbar']));
+    $this->assertNoLink('Shortcuts');
+    $this->verifyDynamicPageCache($test_page_url, 'MISS');
+    $this->verifyDynamicPageCache($test_page_url, 'HIT');
+
+    // Verify that users without the 'administer site configuration' permission
+    // can't see the cron shortcut but can see shortcuts toolbar tab.
+    $this->drupalLogin($this->drupalCreateUser([
+      'access toolbar',
+      'access shortcuts',
+    ]));
+    $this->verifyDynamicPageCache($test_page_url, 'MISS');
+    $this->verifyDynamicPageCache($test_page_url, 'HIT');
+    $this->assertLink('Shortcuts');
+    $this->assertNoLink('Cron');
+
+    // Create a role with access to shortcuts as well as the necessary
+    // permissions to see specific shortcuts.
+    $site_configuration_role = $this->drupalCreateRole([
+      'access toolbar',
+      'access shortcuts',
+      'administer site configuration',
+      'access administration pages',
+    ]);
+
+    // Create two different users with the same role to assert that the second
+    // user has a cache hit despite the user cache context, as
+    // the returned cache contexts include those from lazy-builder content.
+    $site_configuration_user1 = $this->drupalCreateUser();
+    $site_configuration_user1->addRole($site_configuration_role);
+    $site_configuration_user1->save();
+    $site_configuration_user2 = $this->drupalCreateUser();
+    $site_configuration_user2->addRole($site_configuration_role);
+    $site_configuration_user2->save();
+
+    $this->drupalLogin($site_configuration_user1);
+    $this->verifyDynamicPageCache($test_page_url, 'MISS');
+    $this->verifyDynamicPageCache($test_page_url, 'HIT');
+    $this->assertCacheContexts(['user', 'url.query_args:_wrapper_format']);
+    $this->assertLink('Shortcuts');
+    $this->assertLink('Cron');
+
+    $this->drupalLogin($site_configuration_user2);
+    $this->verifyDynamicPageCache($test_page_url, 'HIT');
+    $this->assertCacheContexts(['user', 'url.query_args:_wrapper_format']);
+    $this->assertLink('Shortcuts');
+    $this->assertLink('Cron');
+
+    // Add another shortcut.
+    $shortcut = Shortcut::create([
+      'shortcut_set' => 'default',
+      'title' => 'Llama',
+      'weight' => 0,
+      'link' => [['uri' => 'internal:/admin/config']],
+    ]);
+    $shortcut->save();
+
+    // The shortcuts are displayed in a lazy builder, so the page is still a
+    // cache HIT but shows the new shortcut immediately.
+    $this->verifyDynamicPageCache($test_page_url, 'HIT');
+    $this->assertLink('Cron');
+    $this->assertLink('Llama');
+
+    // Update the shortcut title and assert that it is updated.
+    $shortcut->set('title', 'Alpaca');
+    $shortcut->save();
+    $this->verifyDynamicPageCache($test_page_url, 'HIT');
+    $this->assertLink('Cron');
+    $this->assertLink('Alpaca');
+
+    // Delete the shortcut and assert that the link is gone.
+    $shortcut->delete();
+    $this->verifyDynamicPageCache($test_page_url, 'HIT');
+    $this->assertLink('Cron');
+    $this->assertNoLink('Alpaca');
+  }
+
 }
diff --git a/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php b/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php
index 8f7bf0aa1bc7ae51354980e71db9e7d19343ced1..9342b329f2395d2b00cd73a482db9b4651f99ec8 100644
--- a/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php
+++ b/core/modules/shortcut/tests/src/Functional/ShortcutLinksTest.php
@@ -355,9 +355,9 @@ public function testAccessShortcutsPermission() {
     $this->assertNoLink('Shortcuts', 'Shortcut link not found on page.');
 
     // Verify that users without the 'administer site configuration' permission
-    // can't see the cron shortcuts.
+    // can't see the cron shortcuts but can see shortcuts.
     $this->drupalLogin($this->drupalCreateUser(['access toolbar', 'access shortcuts']));
-    $this->assertNoLink('Shortcuts', 'Shortcut link not found on page.');
+    $this->assertLink('Shortcuts');
     $this->assertNoLink('Cron', 'Cron shortcut link not found on page.');
 
     // Verify that users with the 'access shortcuts' permission can see the
diff --git a/core/modules/system/tests/src/Functional/Cache/PageCacheTagsTestBase.php b/core/modules/system/tests/src/Functional/Cache/PageCacheTagsTestBase.php
index 92ef5531b10f8e72e20f0b69c00e690ee705c35b..5398e218de81b7ada7f3e029a87dd4d6d91338c7 100644
--- a/core/modules/system/tests/src/Functional/Cache/PageCacheTagsTestBase.php
+++ b/core/modules/system/tests/src/Functional/Cache/PageCacheTagsTestBase.php
@@ -37,7 +37,6 @@ protected function setUp() {
    *   The page for this URL will be loaded.
    * @param string $hit_or_miss
    *   'HIT' if a page cache hit is expected, 'MISS' otherwise.
-   *
    * @param array|false $tags
    *   When expecting a page cache hit, you may optionally specify an array of
    *   expected cache tags. While FALSE, the cache tags will not be verified.
@@ -59,4 +58,18 @@ protected function verifyPageCache(Url $url, $hit_or_miss, $tags = FALSE) {
     }
   }
 
+  /**
+   * Verify that when loading a given page, it's a page cache hit or miss.
+   *
+   * @param \Drupal\Core\Url $url
+   *   The page for this URL will be loaded.
+   * @param string $hit_or_miss
+   *   'HIT' if a page cache hit is expected, 'MISS' otherwise.
+   */
+  protected function verifyDynamicPageCache(Url $url, $hit_or_miss) {
+    $this->drupalGet($url);
+    $message = new FormattableMarkup('Dynamic page cache @hit_or_miss for %path.', ['@hit_or_miss' => $hit_or_miss, '%path' => $url->toString()]);
+    $this->assertSame($hit_or_miss, $this->getSession()->getResponseHeader('X-Drupal-Dynamic-Cache'), $message);
+  }
+
 }
diff --git a/core/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php b/core/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php
index 8b564cca07d6185b3fbeaac545710a5ca1476e0f..fbc8ee4a44e1d9f5c8fae883ad4163acb04f1140 100644
--- a/core/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php
+++ b/core/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php
@@ -98,11 +98,6 @@ public function testToolbarCacheContextsCaller() {
     $this->adminUser2 = $this->drupalCreateUser(array_merge($this->perms, ['access tour']));
     $this->assertToolbarCacheContexts(['user.permissions'], 'Expected cache contexts found with tour module enabled.');
     \Drupal::service('module_installer')->uninstall(['tour']);
-
-    // Test with shortcut module enabled.
-    $this->installExtraModules(['shortcut']);
-    $this->adminUser2 = $this->drupalCreateUser(array_merge($this->perms, ['access shortcuts', 'administer shortcuts']));
-    $this->assertToolbarCacheContexts(['user'], 'Expected cache contexts found with shortcut module enabled.');
   }
 
   /**