diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module
index f6da17897e..3fdd843b20 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;
@@ -324,6 +325,11 @@ function shortcut_preprocess_page_title(&$variables) {
 
     $shortcut_set = shortcut_current_displayed_set();
 
+    // Add a list cache tag for shortcuts.
+    $cacheablity_metadata = CacheableMetadata::createFromRenderArray($variables);
+    $cacheablity_metadata->addCacheTags(\Drupal::entityTypeManager()->getDefinition('shortcut')->getListCacheTags());
+    $cacheablity_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 */
@@ -380,52 +386,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 0000000000..49244aa440
--- /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 0000000000..3c07ac28e4
--- /dev/null
+++ b/core/modules/shortcut/src/ShortcutLazyBuilders.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Drupal\shortcut;
+
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Security\TrustedCallbackInterface;
+use Drupal\Core\Url;
+
+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 71720d4442..3b32a6e966 100644
--- a/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php
+++ b/core/modules/shortcut/tests/src/Functional/ShortcutCacheTagsTest.php
@@ -3,8 +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\Entity\EntityCacheTagsTestBase;
+use Drupal\Tests\system\Functional\Cache\PageCacheTagsTestBase;
 use Drupal\user\Entity\Role;
 use Drupal\user\RoleInterface;
 
@@ -13,12 +14,12 @@
  *
  * @group shortcut
  */
-class ShortcutCacheTagsTest extends EntityCacheTagsTestBase {
+class ShortcutCacheTagsTest extends PageCacheTagsTestBase {
 
   /**
    * {@inheritdoc}
    */
-  public static $modules = ['shortcut'];
+  public static $modules = ['toolbar', 'shortcut', 'test_page_test', 'block'];
 
   /**
    * {@inheritdoc}
@@ -61,16 +62,98 @@ protected function createEntity() {
   public function testEntityCreation() {
     // Create a cache entry that is tagged with a shortcut set cache tag.
     $cache_tags = ['config:shortcut.set.default'];
-    \Drupal::cache('render')->set('foo', 'bar', CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
+    $render_cache = \Drupal::cache('render');
+    $render_cache->set('foo', 'bar', CacheBackendInterface::CACHE_PERMANENT, $cache_tags);
 
     // Verify a cache hit.
-    $this->verifyRenderCache('foo', $cache_tags);
+    $item = $render_cache->get('foo');
+    $this->assertEquals($cache_tags, $item->tags);
 
     // Now create a shortcut entity in that shortcut set.
     $this->createEntity();
 
     // Verify a cache miss.
-    $this->assertFalse(\Drupal::cache('render')->get('foo'), 'Creating a new shortcut invalidates the cache tag of the shortcut set.');
+    $this->assertFalse($render_cache->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() {
+    \Drupal::configFactory()
+      ->getEditable('stark.settings')
+      ->set('third_party_settings.shortcut.module_link', TRUE)
+      ->save(TRUE);
+
+    $this->drupalPlaceBlock('page_title_block');
+
+    $this->verifyPageCache(Url::fromRoute('test_page_test.test_page'), 'MISS');
+    $this->verifyPageCache(Url::fromRoute('test_page_test.test_page'), 'HIT');
+
+    // Add cron to the default shortcut set.
+    $this->drupalLogin($this->rootUser);
+    $this->drupalGet('admin/config/system/cron');
+    $this->clickLink('Add to Default shortcuts');
+
+    // Verify that users without the 'access shortcuts' permission can't see the
+    // shortcuts.
+    $this->drupalLogin($this->drupalCreateUser(['access toolbar']));
+    $this->assertNoLink('Shortcuts');
+    $this->verifyDynamicPageCache(Url::fromRoute('test_page_test.test_page'), 'MISS');
+    $this->verifyDynamicPageCache(Url::fromRoute('test_page_test.test_page'), 'HIT');
+
+    // Verify that users without the 'administer site configuration' permission
+    // can't see the cron shortcuts but can see shortcuts.
+    $this->drupalLogin($this->drupalCreateUser([
+      'access toolbar',
+      'access shortcuts',
+    ]));
+    $this->verifyDynamicPageCache(Url::fromRoute('test_page_test.test_page'), 'MISS');
+    $this->verifyDynamicPageCache(Url::fromRoute('test_page_test.test_page'), 'HIT');
+    $this->assertLink('Shortcuts');
+    $this->assertNoLink('Cron');
+
+    // Verify that users with the 'access shortcuts' permission can see the
+    // shortcuts.
+    $site_configuration_user = $this->drupalCreateUser([
+      'access toolbar',
+      'access shortcuts',
+      'administer site configuration',
+      'access administration pages',
+    ]);
+    $this->drupalLogin($site_configuration_user);
+    $this->verifyDynamicPageCache(Url::fromRoute('test_page_test.test_page'), 'MISS');
+    $this->verifyDynamicPageCache(Url::fromRoute('test_page_test.test_page'), 'HIT');
+    $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(Url::fromRoute('test_page_test.test_page'), '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(Url::fromRoute('test_page_test.test_page'), 'HIT');
+    $this->assertLink('Cron');
+    $this->assertLink('Alpaca');
+
+    // Delete the shortcut and assert that the link is gone.
+    $shortcut->delete();
+    $this->verifyDynamicPageCache(Url::fromRoute('test_page_test.test_page'), '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 8f7bf0aa1b..9342b329f2 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 92ef5531b1..29bd89a23b 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.
@@ -48,15 +47,50 @@ protected function verifyPageCache(Url $url, $hit_or_miss, $tags = FALSE) {
     $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), $hit_or_miss, $message);
 
     if ($hit_or_miss === 'HIT' && is_array($tags)) {
-      $absolute_url = $url->setAbsolute()->toString();
-      $cid_parts = [$absolute_url, ''];
-      $cid = implode(':', $cid_parts);
-      $cache_entry = \Drupal::cache('page')->get($cid);
-      sort($cache_entry->tags);
-      $tags = array_unique($tags);
-      sort($tags);
-      $this->assertIdentical($cache_entry->tags, $tags);
+      $this->verifyTags($url, 'page', $tags);
     }
   }
 
+  /**
+   * 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.
+   * @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.
+   */
+  protected function verifyDynamicPageCache(Url $url, $hit_or_miss, $tags = FALSE) {
+    $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);
+
+    if ($hit_or_miss === 'HIT' && is_array($tags)) {
+      $this->verifyTags($url, 'dynamic_page_cache', $tags);
+    }
+  }
+
+  /**
+   * Helper method to verify tags on a cache entry.
+   *
+   * @param \Drupal\Core\Url $url
+   *   The page for this URL will be loaded.
+   * @param string $bin
+   *   The cache bin used for the page cache.
+   * @param array $tags
+   *   An array of expected cache tags.
+   */
+  private function verifyTags(Url $url, $bin, array $tags) {
+    $absolute_url = $url->setAbsolute()->toString();
+    $cid_parts = [$absolute_url, ''];
+    $cid = implode(':', $cid_parts);
+    $cache_entry = \Drupal::cache($bin)->get($cid);
+    sort($cache_entry->tags);
+    $tags = array_unique($tags);
+    sort($tags);
+    $this->assertIdentical($cache_entry->tags, $tags);
+  }
+
 }
diff --git a/core/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php b/core/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php
index 8b564cca07..72a34fb854 100644
--- a/core/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php
+++ b/core/modules/toolbar/tests/src/Functional/ToolbarCacheContextsTest.php
@@ -6,6 +6,7 @@
 use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\user\UserInterface;
 
 /**
  * Tests the cache contexts for toolbar.
@@ -102,21 +103,24 @@ public function testToolbarCacheContextsCaller() {
     // 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.');
+    $this->assertToolbarCacheContextsForUser(['user.permissions'], $this->adminUser, 'Expected cache contexts found with shortcut module enabled.');
+    $this->assertToolbarCacheContextsForUser(['user'], $this->adminUser2, 'Expected cache contexts found with shortcut module enabled.');
   }
 
   /**
-   * Tests that cache contexts are applied for both users.
+   * Tests that cache contexts are applied for a specific user.
    *
    * @param string[] $cache_contexts
    *   Expected cache contexts for both users.
+   * @param UserInterface $user
+   *   The user for testing.
    * @param string $message
    *   (optional) A verbose message to output.
    *
    * @return
    *   TRUE if the assertion succeeded, FALSE otherwise.
    */
-  protected function assertToolbarCacheContexts(array $cache_contexts, $message = NULL) {
+  protected function assertToolbarCacheContextsForUser(array $cache_contexts, UserInterface $user, $message = NULL) {
     // Default cache contexts that should exist on all test cases.
     $default_cache_contexts = [
       'languages:language_interface',
@@ -125,17 +129,12 @@ protected function assertToolbarCacheContexts(array $cache_contexts, $message =
     ];
     $cache_contexts = Cache::mergeContexts($default_cache_contexts, $cache_contexts);
 
-    // Assert contexts for user1 which has only default permissions.
-    $this->drupalLogin($this->adminUser);
+    // Assert contexts for user.
+    $this->drupalLogin($user);
     $this->drupalGet('test-page');
     $return = $this->assertCacheContexts($cache_contexts);
     $this->drupalLogout();
 
-    // Assert contexts for user2 which has some additional permissions.
-    $this->drupalLogin($this->adminUser2);
-    $this->drupalGet('test-page');
-    $return = $return && $this->assertCacheContexts($cache_contexts);
-
     if ($return) {
       $this->pass($message);
     }
@@ -145,6 +144,22 @@ protected function assertToolbarCacheContexts(array $cache_contexts, $message =
     return $return;
   }
 
+  /**
+   * Tests that cache contexts are applied for both users.
+   *
+   * @param string[] $cache_contexts
+   *   Expected cache contexts for both users.
+   * @param string $message
+   *   (optional) A verbose message to output.
+   *
+   * @return
+   *   TRUE if the assertion succeeded, FALSE otherwise.
+   */
+  protected function assertToolbarCacheContexts(array $cache_contexts, $message = NULL) {
+    $this->assertToolbarCacheContextsForUser($cache_contexts, $this->adminUser, $message);
+    $this->assertToolbarCacheContextsForUser($cache_contexts, $this->adminUser2, $message);
+  }
+
   /**
    * Installs a given list of modules and rebuilds the cache.
    *
