diff --git a/core/core.services.yml b/core/core.services.yml
index 55d8d2ebc9843e32969d54c5a341bd9242a96d86..eb6a576d257e90205727f2caa45724f99302f1af 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -205,6 +205,7 @@ services:
       - { name: cache.context}
   cache_context.timezone:
     class: Drupal\Core\Cache\Context\TimeZoneCacheContext
+    arguments: ['@config.factory']
     tags:
       - { name: cache.context}
 
diff --git a/core/lib/Drupal/Core/Cache/Context/CacheContextOptimizableInterface.php b/core/lib/Drupal/Core/Cache/Context/CacheContextOptimizableInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..f7abf0944448192341761841aed2b432230f538e
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/Context/CacheContextOptimizableInterface.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\Core\Cache\Context;
+
+/**
+ * Allows cache contexts to influence how cache contexts are optimized.
+ */
+interface CacheContextOptimizableInterface {
+
+  /**
+   * Returns whether a cache context value has variations.
+   *
+   * For example, the timezone cache context can define itself as global
+   * if a site does not have configurable timezones and the language contexts
+   * are global on a site that is not multilingual.
+   *
+   * @return bool
+   *   TRUE if the context has more than one possible variations.
+   */
+  public function hasVariations(): bool;
+
+  /**
+   * Returns parent contexts for this context.
+   *
+   * By default, parents are derived based on the cache context name, user is
+   * a parent of user.permissions. This allows e.g. the user.roles context to
+   * expand on this and indicate that user.permissions is also a valid
+   * parent/replacement.
+   *
+   * Note that if implementing this, it is necessary to also include parents of
+   * the parent contexts, e.g. user.roles must provide both user.permissions and
+   * user.
+   *
+   * @return array
+   *   A list of parents cache contexts.
+   */
+  public function getParentContexts(): array;
+
+}
diff --git a/core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php b/core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php
index 91d1b3ab9e993acb105af695ba94b3fc364fa793..7a7b9979882acd63af0b3ed3d8bab056adc837d0 100644
--- a/core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php
+++ b/core/lib/Drupal/Core/Cache/Context/CacheContextsManager.php
@@ -172,6 +172,22 @@ public function optimizeTokens(array $context_tokens) {
         [$context_id, $parameter] = explode(':', $context_token);
       }
 
+      // Cache contexts can explicitly provide a list of parents or indicate
+      // that they are global.
+      // @todo Also support to optimize them away if a parent of those is
+      //   provided.
+      $service = $this->getService($context_id);
+      if ($service instanceof CacheContextOptimizableInterface) {
+        if (!$service->hasVariations()) {
+          continue;
+        }
+
+        if (array_intersect($context_tokens, $service->getParentContexts())) {
+          // If there is at least one parent context available, skip this one.
+          continue;
+        }
+      }
+
       // Context tokens without:
       // - a period means they don't have a parent
       // - a colon means they're not a specific value of a cache context
@@ -181,7 +197,7 @@ public function optimizeTokens(array $context_tokens) {
       }
       // Check cacheability. If the context defines a max-age of 0, then it
       // can not be optimized away. Pass the parameter along if we have one.
-      elseif ($this->getService($context_id)->getCacheableMetadata($parameter)->getCacheMaxAge() === 0) {
+      elseif ($service->getCacheableMetadata($parameter)->getCacheMaxAge() === 0) {
         $optimized_content_tokens[] = $context_token;
       }
       // The context token has a period or a colon. Iterate over all ancestor
diff --git a/core/lib/Drupal/Core/Cache/Context/TimeZoneCacheContext.php b/core/lib/Drupal/Core/Cache/Context/TimeZoneCacheContext.php
index 318817264cc7ecaf0704798717799c280c8359fa..b24fc09e326074248c6ce143825ba88101f666c4 100644
--- a/core/lib/Drupal/Core/Cache/Context/TimeZoneCacheContext.php
+++ b/core/lib/Drupal/Core/Cache/Context/TimeZoneCacheContext.php
@@ -3,6 +3,7 @@
 namespace Drupal\Core\Cache\Context;
 
 use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Config\ConfigFactoryInterface;
 
 /**
  * Defines the TimeZoneCacheContext service, for "per time zone" caching.
@@ -11,7 +12,24 @@
  *
  * @see \Drupal\Core\Session\AccountProxy::setAccount()
  */
-class TimeZoneCacheContext implements CacheContextInterface {
+class TimeZoneCacheContext implements CacheContextInterface, CacheContextOptimizableInterface {
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * Constructor for the TimeZoneCacheContext object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
+   *   The config factory.
+   */
+  public function __construct(ConfigFactoryInterface $configFactory) {
+    $this->configFactory = $configFactory;
+  }
 
   /**
    * {@inheritdoc}
@@ -33,7 +51,25 @@ public function getContext() {
    * {@inheritdoc}
    */
   public function getCacheableMetadata() {
-    return new CacheableMetadata();
+    $cacheability_metadata = new CacheableMetadata();
+    $cacheability_metadata->addCacheableDependency($this->configFactory->get('system.date'));
+    return $cacheability_metadata;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasVariations(): bool {
+    // The timezone context can not have different values if the site does not
+    // use configurable timezones.
+    return (bool) $this->configFactory->get('system.date')->get('timezone.user.configurable');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getParentContexts(): array {
+    return [];
   }
 
 }
diff --git a/core/modules/node/tests/src/Functional/NodeCacheTagsNoTimezoneTest.php b/core/modules/node/tests/src/Functional/NodeCacheTagsNoTimezoneTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..c92a32c0fddcd4701491403487acfbb17f1042fa
--- /dev/null
+++ b/core/modules/node/tests/src/Functional/NodeCacheTagsNoTimezoneTest.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\node\Functional;
+
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Tests the Node entity's cache tags.
+ *
+ * @group node
+ */
+class NodeCacheTagsNoTimezoneTest extends NodeCacheTagsTest {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    // Disable configurable timezones, which should result in the timezone cache
+    // context *not* being added.
+    $this->config('system.date')
+      ->set('timezone.user.configurable', FALSE)
+      ->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getAdditionalCacheContextsForEntity(EntityInterface $entity): array {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Each node must have an author.
+   */
+  protected function getAdditionalCacheTagsForEntity(EntityInterface $node): array {
+    // Because timezone is optimized away, the additional system.date cache tag
+    // is added.
+    return ['user:' . $node->getOwnerId(), 'user_view', 'config:system.date'];
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Cache/Context/CacheContextsManagerTest.php b/core/tests/Drupal/Tests/Core/Cache/Context/CacheContextsManagerTest.php
index f76e8cd647437b76f5439c538852311260360b5e..178cde88b7c55b21ac235c35364c9db7bf8d3720 100644
--- a/core/tests/Drupal/Tests/Core/Cache/Context/CacheContextsManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/Cache/Context/CacheContextsManagerTest.php
@@ -5,6 +5,7 @@
 namespace Drupal\Tests\Core\Cache\Context;
 
 use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Cache\Context\CacheContextOptimizableInterface;
 use Drupal\Core\Cache\Context\CacheContextsManager;
 use Drupal\Core\Cache\Context\CacheContextInterface;
 use Drupal\Core\Cache\Context\CalculatedCacheContextInterface;
@@ -57,6 +58,16 @@ public function testOptimizeTokens(array $context_tokens, array $optimized_conte
           Container::EXCEPTION_ON_INVALID_REFERENCE,
           new NoOptimizeCacheContext(),
         ],
+        [
+          'cache_context.x.non-standard',
+          Container::EXCEPTION_ON_INVALID_REFERENCE,
+          new NonStandardOptimizeCacheContext(),
+        ],
+        [
+          'cache_context.x.global',
+          Container::EXCEPTION_ON_INVALID_REFERENCE,
+          new GlobalOptimizeCacheContext(),
+        ],
       ]);
     $cache_contexts_manager = new CacheContextsManager($container, $this->getContextsFixture());
 
@@ -96,6 +107,24 @@ public static function providerTestOptimizeTokens() {
       [['a.b.c:foo', 'a'], ['a']],
       [['a.b.c:foo', 'a.b.c'], ['a.b.c']],
 
+      // Non-standard parents.
+      [['a.b', 'x.non-standard'], ['a.b']],
+      // Calculated cache contexts are supported as well but there is no
+      // argument granularity.
+      [['a.b', 'x.non-standard:argument'], ['a.b']],
+      [['a', 'a.b', 'x.non-standard'], ['a']],
+      // Should contexts with explicit parents still respect the default logic?
+      // Not doing that would allow to deprecate the somewhat strange max-age 0
+      // check in favor of returning an empty list?
+      [['x', 'x.non-standard'], ['x']],
+      [['a.b.c', 'x.non-standard'], ['a.b.c', 'x.non-standard']],
+      // @todo Support this as well?
+      // [['a', 'non-standard'], ['a']],
+
+      // Contexts that return TRUE in isGlobal() are always optimized away.
+      [['a', 'x.global'], ['a']],
+      [['x.global'], []],
+
       // max-age 0 is treated as non-optimizable.
       [['a.b.no-optimize', 'a.b', 'a'], ['a.b.no-optimize', 'a']],
     ];
@@ -333,3 +362,87 @@ public function getCacheableMetadata() {
   }
 
 }
+
+/**
+ * Optimizable context class with a non-default parent.
+ */
+class NonStandardOptimizeCacheContext implements CacheContextInterface, CacheContextOptimizableInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getLabel() {
+    return 'Non-Standard';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContext() {
+    return 'non_standard';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheableMetadata($parameter = NULL) {
+    return new CacheableMetadata();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasVariations(): bool {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getParentContexts(): array {
+    return ['a.b'];
+  }
+
+}
+
+/**
+ * A global cache context that can always be optimized away.
+ */
+class GlobalOptimizeCacheContext implements CacheContextInterface, CacheContextOptimizableInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getLabel() {
+    return 'Non-Standard';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContext() {
+    return 'non_standard';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheableMetadata($parameter = NULL) {
+    return new CacheableMetadata();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasVariations(): bool {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getParentContexts(): array {
+    return [];
+  }
+
+}