From 5712a422051ccbe5337e9d71276d06acbf649957 Mon Sep 17 00:00:00 2001
From: Christian Kuhn <lolli@schwarzbu.ch>
Date: Sun, 3 Mar 2024 13:29:29 +0100
Subject: [PATCH] [TASK] Add FrontendTypoScriptFactory
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

In version 12, the introduction of the new TypoScript parser
was accompanied by the implementation of factories for
PageTsConfig and UserTsConfig.
A factory for Frontend TypoScript has not been added,
though: Frontend TypoScript creation ended up in
TSFE->getFromCache(). At this point, establishing a proper
factory was unfeasible due to the numerous dependencies of
TypoScript creation to TSFE internals.

With recent refactorings around TSFE, coupled with lots of
state now being represented as request attributes, it's now
possible to decompose getFromCache() and establish a
FrontendTypoScriptFactory.

getFromCache() is a complex beast: It influences Frontend
rendering performance a lot, and tries to trigger the least
amount of calculations, especially in 'fully cached pages'
context. This results in high required complexity due to
lots of state with diverse cross dependencies.

The method composes of three main steps:
1. Bootstrap TypoScript setting ("constants") and calculate
   setup condition verdicts. This creates required TypoScript
   related state needed to calculate the page cache identifier.
2. Access page cache, lock page rendering if needed, and see
   if a possible page cache content contains uncached ("_INT")
   sections.
3. Calculate at least setup "config." depending on given
   type/typeNum, but create full TypoScript setup if a cached
   page contains uncached sections or could not be retrieved
   from cache.

The patch extracts parts 1 and 3 to FrontendTypoScriptFactory.
Part 2 is moved into PrepareTypoScriptFrontendRendering
middleware.

This approach allowed these related refactorings:
* The release of rendering locks is now consolidated within the
  PrepareTypoScriptFrontendRendering middleware. This guarantees
  locks are released, even in scenarios where lower middleware
  components encounter errors. This addresses an issue where
  locks retained during crashes, leading to deadlock situations
  in subsequent requests.
* Dependencies to TSFE within ext:redirects RedirectService
  are reduced, and it no longer locks page rendering.
* The Extbase BackendConfigurationManager utilizes the new
  factory, eliminating the need for its own implementation.

The patch unlocks further refactorings: It especially allows
removing the cache related properties from TSFE by representing
them as request attributes. Subsequent patches will address this
task accordingly.

Resolves: #103410
Related: #97816
Related: #98914
Related: #102932
Releases: main
Change-Id: I7fd158cffeebe6b2c64e0e3595284b8780fb73cf
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83179
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Garvin Hicking <gh@faktor-e.de>
Tested-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Garvin Hicking <gh@faktor-e.de>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
---
 .../Classes/TypoScript/FrontendTypoScript.php | 138 ++++-
 .../TypoScript/FrontendTypoScriptFactory.php  | 473 ++++++++++++++++
 ...upConditionConstantSubstitutionVisitor.php |   4 +-
 typo3/sysext/core/Configuration/Services.yaml |  10 +
 .../BackendConfigurationManager.php           |  48 +-
 .../FrontendConfigurationManagerTest.php      |   6 +-
 .../ActionControllerArgumentTest.php          |   2 +-
 .../Functional/Mvc/Web/RequestBuilderTest.php |   2 +-
 .../Generic/Storage/Typo3DbBackendTest.php    |   2 +-
 .../Storage/Typo3DbQueryParserTest.php        |  40 +-
 .../Persistence/TranslationTest.php           |   2 +-
 .../Functional/Persistence/WorkspaceTest.php  |   4 +-
 .../FrontendConfigurationManagerTest.php      |  10 +-
 .../ViewHelpers/CObjectViewHelperTest.php     |   7 -
 .../ViewHelpers/FormViewHelperTest.php        |   2 +-
 .../ViewHelpers/Link/ActionViewHelperTest.php |   4 +-
 .../ViewHelpers/Link/PageViewHelperTest.php   |   4 +-
 .../ViewHelpers/Uri/ActionViewHelperTest.php  |   2 +-
 .../TypoScriptFrontendController.php          | 511 +-----------------
 ...houldUseCachedPageDataIfAvailableEvent.php |   6 +-
 .../frontend/Classes/Http/RequestHandler.php  |   1 -
 .../PrepareTypoScriptFrontendRendering.php    | 237 ++++++--
 .../frontend/Configuration/Services.yaml      |   5 +
 .../ContentObjectRendererTest.php             |   6 +-
 .../ContentObjectRendererTest.php             |  24 +-
 .../Tests/Unit/Http/RequestHandlerTest.php    |   8 +-
 .../DatabaseRecordLinkBuilderTest.php         |   2 +-
 .../indexed_search/Tests/Unit/IndexerTest.php |   2 +-
 .../Classes/Service/RedirectService.php       |  52 +-
 .../redirects/Configuration/Services.yaml     |   4 +
 .../Service/IntegrityServiceTest.php          |  11 +-
 .../Service/RedirectServiceTest.php           |  23 +-
 .../Unit/Service/RedirectServiceTest.php      |  47 +-
 .../Canonical/CanonicalGeneratorTest.php      |   2 +-
 34 files changed, 1020 insertions(+), 681 deletions(-)
 create mode 100644 typo3/sysext/core/Classes/TypoScript/FrontendTypoScriptFactory.php

diff --git a/typo3/sysext/core/Classes/TypoScript/FrontendTypoScript.php b/typo3/sysext/core/Classes/TypoScript/FrontendTypoScript.php
index aa3bcdd9ce48..3fd6fb3c1f5c 100644
--- a/typo3/sysext/core/Classes/TypoScript/FrontendTypoScript.php
+++ b/typo3/sysext/core/Classes/TypoScript/FrontendTypoScript.php
@@ -18,6 +18,7 @@ declare(strict_types=1);
 namespace TYPO3\CMS\Core\TypoScript;
 
 use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
 
 /**
  * This class contains the TypoScript set up by the PrepareTypoScriptFrontendRendering
@@ -27,19 +28,24 @@ use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
  */
 final class FrontendTypoScript
 {
-    private RootNode|null $setupTree = null;
-    private array|null $setupArray = null;
-    private RootNode $configTree;
-    private array $configArray;
-    private RootNode $pageTree;
-    private array $pageArray;
+    private ?RootInclude $setupIncludeTree = null;
+    private ?RootNode $setupTree = null;
+    private ?array $setupArray = null;
+    private ?RootNode $configTree = null;
+    private ?array $configArray = null;
+    private ?RootNode $pageTree = null;
+    private ?array $pageArray = null;
 
     public function __construct(
         private readonly RootNode $settingsTree,
+        private readonly array $settingsConditionList,
         private readonly array $flatSettings,
+        private readonly array $setupConditionList,
     ) {}
 
     /**
+     * The settings ("constants") AST.
+     *
      * @internal Internal for now until the AST API stabilized.
      */
     public function getSettingsTree(): RootNode
@@ -48,9 +54,19 @@ final class FrontendTypoScript
     }
 
     /**
-     * This is *always* set up by the middleware: Current settings (aka "TypoScript constants")
-     * are needed for page cache identifier calculation.
+     * List of settings conditions with verdicts. Used internally for
+     * page cache identifier calculation.
      *
+     * @internal
+     */
+    public function getSettingsConditionList(): array
+    {
+        return $this->settingsConditionList;
+    }
+
+    /**
+     * This is *always* set up by the middleware / factory: Current settings ("constants")
+     * are needed for page cache identifier calculation.
      * This is a "flattened" array of all settings, as example, consider these settings TypoScript:
      *
      * ```
@@ -74,6 +90,37 @@ final class FrontendTypoScript
         return $this->flatSettings;
     }
 
+    /**
+     * List of setup conditions with verdicts. Used internally for
+     * page cache identifier calculation.
+     *
+     * @internal
+     */
+    public function getSetupConditionList(): array
+    {
+        return $this->setupConditionList;
+    }
+
+    /**
+     * @internal
+     */
+    public function setSetupIncludeTree(RootInclude $setupIncludeTree): void
+    {
+        $this->setupIncludeTree = $setupIncludeTree;
+    }
+
+    /**
+     * A tree of all TypoScript setup includes. Used internally within
+     * FrontendTypoScriptFactory to suppress calculating the include tree
+     * twice.
+     *
+     * @internal
+     */
+    public function getSetupIncludeTree(): ?RootInclude
+    {
+        return $this->setupIncludeTree;
+    }
+
     /**
      * @internal
      */
@@ -84,13 +131,15 @@ final class FrontendTypoScript
 
     /**
      * When a page is retrieved from cache and does not contain COA_INT or USER_INT objects,
-     * Frontend TypoScript setup is not calculated, so the AST and the array are not set.
+     * Frontend TypoScript setup is not calculated, AST and the array representation aren't set.
      * Calling getSetupTree() or getSetupArray() will then throw an exception.
      *
      * To avoid the exception, consumers can call hasSetup() beforehand.
      *
      * Note casual content objects do not need to do this, since setup TypoScript is always
      * set up when content objects need to be calculated.
+     *
+     * @internal
      */
     public function hasSetup(): bool
     {
@@ -124,7 +173,7 @@ final class FrontendTypoScript
      * The full Frontend TypoScript array.
      *
      * This is always set up as soon as the Frontend rendering needs to actually render something and
-     * can not get the full content from page cache. This is the case when a page cache entry does
+     * can not get the *full* content from page cache. This is the case when a page cache entry does
      * not exist, or when the page contains COA_INT or USER_INT objects.
      */
     public function getSetupArray(): array
@@ -148,10 +197,26 @@ final class FrontendTypoScript
     }
 
     /**
-     * @internal
+     * The merged TypoScript 'config.'.
+     *
+     * This is the result of the "global" TypoScript 'config' section, merged with
+     * the 'config' section of the determined PAGE object which can override
+     * "global" 'config' per type / typeNum.
+     *
+     * This is *always* needed within casual Frontend rendering by FrontendTypoScriptFactory and
+     * has a dedicated cache layer to be quick to retrieve. It is needed even in fully cached pages
+     * context to for instance know if debug headers should be added ("config.debug=1") to a response.
+     *
+     * @internal Internal for now until the AST API stabilized.
      */
     public function getConfigTree(): RootNode
     {
+        if ($this->configTree === null) {
+            throw new \RuntimeException(
+                'Setup "config." not initialized. FrontendTypoScriptFactory->createSetupConfigOrFullSetup() not called?',
+                1710666154
+            );
+        }
         return $this->configTree;
     }
 
@@ -164,14 +229,16 @@ final class FrontendTypoScript
     }
 
     /**
-     * The merged TypoScript 'config'.
-     *
-     * This is the result of the "global" TypoScript 'config' section, merged with
-     * the 'config' section of the determined PAGE object which can override
-     * "global" 'config' per type / typeNum.
+     * Array representation of getConfigTree().
      */
     public function getConfigArray(): array
     {
+        if ($this->configArray === null) {
+            throw new \RuntimeException(
+                'Setup "config." not initialized. FrontendTypoScriptFactory->createSetupConfigOrFullSetup() not called?',
+                1710666123
+            );
+        }
         return $this->configArray;
     }
 
@@ -184,13 +251,35 @@ final class FrontendTypoScript
     }
 
     /**
+     * The determined PAGE object from main TypoScript 'setup' that depends
+     * on type / typeNum.
+     *
+     * This is used internally by RequestHandler for page generation.
+     * It is *not* set in full cached page scenarios without _INT object.
+     *
      * @internal
      */
     public function getPageTree(): RootNode
     {
+        if ($this->pageTree === null) {
+            throw new \RuntimeException(
+                'PAGE node has not been initialized. This happens in cached Frontend scope where full TypoScript' .
+                ' is not needed by the system, and if a PAGE object for given type could not be determined.' .
+                ' Test with hasPage().',
+                1710399966
+            );
+        }
         return $this->pageTree;
     }
 
+    /**
+     * @internal
+     */
+    public function hasPage(): bool
+    {
+        return $this->pageTree !== null;
+    }
+
     /**
      * @internal
      */
@@ -200,19 +289,20 @@ final class FrontendTypoScript
     }
 
     /**
-     * The determined PAGE object from main TypoScript 'setup' that depends
-     * on type / typeNum.
-     *
-     * This is used internally by RequestHandler for page generation.
-     * It is *not* set in full cached page scenarios without _INT object.
-     *
-     * *If* this in made non-internal, a method "hasPage()" should be added
-     * for extensions to verify if page is actually set.
+     * Array representation of getPageTree().
      *
      * @internal
      */
     public function getPageArray(): array
     {
+        if ($this->pageArray === null) {
+            throw new \RuntimeException(
+                'PAGE array has not been initialized. This happens in cached Frontend scope where full TypoScript' .
+                ' is not needed by the system, and if a PAGE object for given type could not be determined.' .
+                ' Test with hasPage().',
+                1710399967
+            );
+        }
         return $this->pageArray;
     }
 }
diff --git a/typo3/sysext/core/Classes/TypoScript/FrontendTypoScriptFactory.php b/typo3/sysext/core/Classes/TypoScript/FrontendTypoScriptFactory.php
new file mode 100644
index 000000000000..24fd0c8c3e05
--- /dev/null
+++ b/typo3/sysext/core/Classes/TypoScript/FrontendTypoScriptFactory.php
@@ -0,0 +1,473 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Core\TypoScript;
+
+use Psr\Container\ContainerInterface;
+use Psr\EventDispatcher\EventDispatcherInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
+use TYPO3\CMS\Core\Site\Entity\SiteInterface;
+use TYPO3\CMS\Core\TypoScript\AST\Merger\SetupConfigMerger;
+use TYPO3\CMS\Core\TypoScript\AST\Node\ChildNode;
+use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\RootInclude;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateTreeBuilder;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeAstBuilderVisitor;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionIncludeListAccumulatorVisitor;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionMatcherVisitor;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSetupConditionConstantSubstitutionVisitor;
+use TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer;
+use TYPO3\CMS\Frontend\Event\ModifyTypoScriptConfigEvent;
+use TYPO3\CMS\Frontend\Event\ModifyTypoScriptConstantsEvent;
+
+/**
+ * Create FrontendTypoScript with its details. This is typically used by a Frontend middleware
+ * to calculate the TypoScript needed to satisfy rendering details of the specific Request.
+ *
+ * @internal Methods signatures and detail implementations are still subject to change.
+ */
+final readonly class FrontendTypoScriptFactory
+{
+    public function __construct(
+        private ContainerInterface $container,
+        private EventDispatcherInterface $eventDispatcher,
+        private SysTemplateTreeBuilder $treeBuilder,
+        private LossyTokenizer $tokenizer,
+        private IncludeTreeTraverser $includeTreeTraverser,
+        private ConditionVerdictAwareIncludeTreeTraverser $includeTreeTraverserConditionVerdictAware,
+    ) {}
+
+    /**
+     * First step of TypoScript calculations.
+     * This is *always* called, even in FE fully cached pages context since the page
+     * cache entry depends on setup condition verdicts, which depends on settings.
+     *
+     * Returns the FrontendTypoScript object with these parameters set:
+     * * settingsTree: The full settings ("constants") AST
+     * * flatSettings: Flattened list of settings, derived from settings tree
+     * * settingsConditionList: Settings conditions with verdicts of this Request
+     * * setupConditionList: Setup conditions with verdicts of this Request
+     * * (sometimes) setupIncludeTree: The setup include tree *if* it had to be calculated
+     */
+    public function createSettingsAndSetupConditions(
+        SiteInterface $site,
+        array $sysTemplateRows,
+        array $expressionMatcherVariables,
+        ?PhpFrontend $typoScriptCache,
+    ): FrontendTypoScript {
+        $settingsDetails = $this->createSettings(
+            $site,
+            $sysTemplateRows,
+            $expressionMatcherVariables,
+            $typoScriptCache
+        );
+        $setupDetails = $this->createSetupConditionList(
+            $site,
+            $sysTemplateRows,
+            $expressionMatcherVariables,
+            $typoScriptCache,
+            $settingsDetails['flatSettings'],
+            $settingsDetails['settingsConditionList'],
+        );
+        $frontendTypoScript = new FrontendTypoScript(
+            $settingsDetails['settingsTree'],
+            $settingsDetails['settingsConditionList'],
+            $settingsDetails['flatSettings'],
+            $setupDetails['setupConditionList'],
+        );
+        if ($setupDetails['setupIncludeTree']) {
+            $frontendTypoScript->setSetupIncludeTree($setupDetails['setupIncludeTree']);
+        }
+        return $frontendTypoScript;
+    }
+
+    /**
+     * Calculate settings (formerly "constants").
+     *
+     * The page cache entry identifier depends on setup TypoScript: A single page with two different
+     * setup TypoScript AST will probably render different results, thus two page-cache entries.
+     * Setup TypoScript can be different when setup conditions match differently.
+     * Setup conditions can use settings "[{$foo} = 42]".
+     *
+     * All FE requests thus need the current list of settings, and settings can have conditions, too.
+     * We thus *always* need the current list of settings, even in fully cached pages context.
+     *
+     * The method calculates settings and uses caches as much as possible:
+     * * settingsTree: The full settings AST
+     * * flatSettings: Flattened list of settings, derived from settings AST
+     * * settingsConditionList: Settings conditions with verdicts of this Request
+     *
+     * @return array{settingsTree: RootNode, flatSettings: array, settingsConditionList: array}
+     */
+    private function createSettings(
+        SiteInterface $site,
+        array $sysTemplateRows,
+        array $expressionMatcherVariables,
+        ?PhpFrontend $typoScriptCache,
+    ): array {
+        $conditionTreeCacheIdentifier = 'settings-condition-tree-' . hash('xxh3', json_encode($sysTemplateRows, JSON_THROW_ON_ERROR));
+
+        if ($conditionTree = $typoScriptCache?->require($conditionTreeCacheIdentifier)) {
+            // Got the (flat) include tree of all settings conditions for this TypoScript combination from cache.
+            // Good. Traverse this list to calculate "current" condition verdicts. Hash this list together with a
+            // hash of the TypoScript sys_templates, and try to retrieve the full settings TypoScript AST from cache.
+            // Note: Working with the derived condition tree that *only* contains conditions, but not the full
+            // include tree is a trick: We only need the condition verdicts to know the AST cache identifier,
+            // and traversing the flat condition tree is quicker than traversing the entire settings include tree,
+            // since it only scales with the number of settings conditions and not with the full amount of TypoScript
+            // settings. The same trick is used for the setup AST cache later.
+            $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
+            $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
+            // It does not matter if we use IncludeTreeTraverser or ConditionVerdictAwareIncludeTreeTraverser here:
+            // Conditions list is flat, not nested. IncludeTreeTraverser has an if() less, so we use that one.
+            $this->includeTreeTraverser->traverse($conditionTree, [$conditionMatcherVisitor]);
+            $conditionList = $conditionMatcherVisitor->getConditionListWithVerdicts();
+            $settings = $typoScriptCache->require(
+                'settings-' . hash('xxh3', $conditionTreeCacheIdentifier . json_encode($conditionList, JSON_THROW_ON_ERROR))
+            );
+            if (is_array($settings)) {
+                return [
+                    'settingsTree' => $settings['ast'],
+                    'flatSettings' => $settings['flatSettings'],
+                    'settingsConditionList' => $conditionList,
+                ];
+            }
+        }
+
+        // We did not get settings from cache, or are not allowed to use cache. Build settings from scratch.
+        // We fetch the full settings include tree (from cache if possible), register the condition
+        // matcher and register the AST builder and traverse include tree to retrieve settings AST and derive
+        // 'flat settings' from it. Both are cached if allowed afterward for the above 'if' to kick in next time.
+        $includeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $this->tokenizer, $site, $typoScriptCache);
+        $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
+        $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
+        $visitors = [];
+        $visitors[] = $conditionMatcherVisitor;
+        $astBuilderVisitor = $this->container->get(IncludeTreeAstBuilderVisitor::class);
+        $visitors[] = $astBuilderVisitor;
+        // We must use ConditionVerdictAwareIncludeTreeTraverser here: This one does not walk into
+        // children for not matching conditions, which is important to create the correct AST.
+        $this->includeTreeTraverserConditionVerdictAware->traverse($includeTree, $visitors);
+        $tree = $astBuilderVisitor->getAst();
+        // @internal Dispatch an experimental event allowing listeners to still change the settings AST,
+        //           to for instance implement nested constants if really needed. Note this event may change
+        //           or vanish later without further notice.
+        $tree = $this->eventDispatcher->dispatch(new ModifyTypoScriptConstantsEvent($tree))->getConstantsAst();
+        $flatSettings = $tree->flatten();
+
+        // Prepare the full list of settings conditions in order to cache this list, avoiding the
+        // settings AST building next time. We need all conditions of the entire include tree, but the
+        // above ConditionVerdictAwareIncludeTreeTraverser did not find nested conditions if an upper
+        // condition did not match. We thus have to traverse include tree a second time with the
+        // IncludeTreeTraverser. This one does traverse into not matching conditions.
+        $visitors = [];
+        $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
+        $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
+        $visitors[] = $conditionMatcherVisitor;
+        $conditionTreeAccumulatorVisitor = null;
+        if (!$conditionTree && $typoScriptCache) {
+            // If the settingsConditionTree did not come from cache above and if we are allowed to cache,
+            // register the visitor that creates the settings condition include tree, to cache it.
+            $conditionTreeAccumulatorVisitor = $this->container->get(IncludeTreeConditionIncludeListAccumulatorVisitor::class);
+            $visitors[] = $conditionTreeAccumulatorVisitor;
+        }
+        $this->includeTreeTraverser->traverse($includeTree, $visitors);
+        $conditionList = $conditionMatcherVisitor->getConditionListWithVerdicts();
+
+        if ($conditionTreeAccumulatorVisitor) {
+            // Cache the flat condition include tree for next run.
+            $conditionTree = $conditionTreeAccumulatorVisitor->getConditionIncludes();
+            $typoScriptCache?->set(
+                $conditionTreeCacheIdentifier,
+                'return unserialize(\'' . addcslashes(serialize($conditionTree), '\'\\') . '\');'
+            );
+        }
+        $typoScriptCache?->set(
+            // Cache full AST and the derived 'flattened' variant for next run, which will kick in if
+            // the sys_templates and condition verdicts are identical with another Request.
+            'settings-' . hash('xxh3', $conditionTreeCacheIdentifier . json_encode($conditionList, JSON_THROW_ON_ERROR)),
+            'return unserialize(\'' . addcslashes(serialize(['ast' => $tree, 'flatSettings' => $flatSettings]), '\'\\') . '\');'
+        );
+
+        return [
+            'settingsTree' => $tree,
+            'flatSettings' => $flatSettings,
+            'settingsConditionList' => $conditionList,
+        ];
+    }
+
+    /**
+     * Calculate setup condition verdicts.
+     *
+     * With settings being done, the list of matching setup condition verdicts is calculated,
+     * which depend on settings. Setup conditions with their verdicts are part of the page
+     * cache identifier, they are *always* needed in the FE rendering chain.
+     *
+     * The cached variant uses a similar trick as with the settings calculation above: We
+     * calculate a flat tree of all conditions and cache this, so the traverser only needs
+     * to iterate the conditions to calculate there verdicts, but not the entire include
+     * tree next time.
+     *
+     * The method returns:
+     * * 'setupConditionList': Setup conditions with verdicts of this Request
+     * * (sometimes) setupIncludeTree: The setup include tree *if* it had to be calculated. Used internally
+     *                                 to suppress a second calculation in createSetupConfigOrFullSetup().
+     *
+     * @return array{setupConditionList: array, setupIncludeTree: RootInclude|null}
+     */
+    private function createSetupConditionList(
+        SiteInterface $site,
+        array $sysTemplateRows,
+        array $expressionMatcherVariables,
+        ?PhpFrontend $typoScriptCache,
+        array $flatSettings,
+        array $settingsConditionList,
+    ): array {
+        $conditionTreeCacheIdentifier = 'setup-condition-tree-'
+            . hash('xxh3', json_encode($sysTemplateRows, JSON_THROW_ON_ERROR) . json_encode($settingsConditionList, JSON_THROW_ON_ERROR));
+
+        if ($conditionTree = $typoScriptCache?->require($conditionTreeCacheIdentifier)) {
+            // We got the flat list of all setup conditions for this TypoScript combination from cache. Good. We traverse
+            // this list to calculate "current" condition verdicts, which we need as hash to be part of page cache identifier.
+            // We're done and return. Note 'setupIncludeTree' is *not* returned in this case since it is not needed and
+            // may or may not be needed later, depending on if we can get a page cache entry later and if it has _INT objects.
+            $visitors = [];
+            $conditionConstantSubstitutionVisitor = $this->container->get(IncludeTreeSetupConditionConstantSubstitutionVisitor::class);
+            $conditionConstantSubstitutionVisitor->setFlattenedConstants($flatSettings);
+            $visitors[] = $conditionConstantSubstitutionVisitor;
+            $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
+            $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
+            $visitors[] = $conditionMatcherVisitor;
+            // It does not matter if we use IncludeTreeTraverser or ConditionVerdictAwareIncludeTreeTraverser here:
+            // Condition list is flat, not nested. IncludeTreeTraverser has an if() less, so we use that one.
+            $this->includeTreeTraverser->traverse($conditionTree, $visitors);
+            return [
+                'setupConditionList' => $conditionMatcherVisitor->getConditionListWithVerdicts(),
+                'setupIncludeTree' => null,
+            ];
+        }
+
+        // We did not get setup condition list from cache, or are not allowed to use cache. We have to build setup
+        // condition list from scratch. This means we'll fetch the full setup include tree (from cache if possible),
+        // register the constant substitution visitor, the condition matcher and the condition accumulator visitor.
+        $includeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $this->tokenizer, $site, $typoScriptCache);
+        $visitors = [];
+        $conditionConstantSubstitutionVisitor = $this->container->get(IncludeTreeSetupConditionConstantSubstitutionVisitor::class);
+        $conditionConstantSubstitutionVisitor->setFlattenedConstants($flatSettings);
+        $visitors[] = $conditionConstantSubstitutionVisitor;
+        $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
+        $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
+        $visitors[] = $conditionMatcherVisitor;
+        $conditionTreeAccumulatorVisitor = $this->container->get(IncludeTreeConditionIncludeListAccumulatorVisitor::class);
+        $visitors[] = $conditionTreeAccumulatorVisitor;
+        // It is important to use IncludeTreeTraverser here: We need the condition verdicts of *all* conditions, and
+        // we want to accumulate all of them. The ConditionVerdictAwareIncludeTreeTraverser wouldn't walk into nested
+        // conditions if an upper one does not match, which defeats cache identifier calculations.
+        $this->includeTreeTraverser->traverse($includeTree, $visitors);
+
+        $typoScriptCache?->set(
+            $conditionTreeCacheIdentifier,
+            'return unserialize(\'' . addcslashes(serialize($conditionTreeAccumulatorVisitor->getConditionIncludes()), '\'\\') . '\');'
+        );
+
+        return [
+            'setupConditionList' => $conditionMatcherVisitor->getConditionListWithVerdicts(),
+            'setupIncludeTree' => $includeTree,
+        ];
+    }
+
+    /**
+     * Enrich the given FrontendTypoScript object with TypoScript 'setup' relevant data.
+     *
+     * The method is called in FE after an attempt to retrieve page content from cache has
+     * been done. There are three possible outcomes:
+     * * The page has been retrieved from cache and the content *does not* contain uncached "_INT" objects
+     * * The page has been retrieved from cache and the content *does* contain uncached "_INT" objects
+     * * The page could not be retrieved from cache
+     *
+     * If the page could not be retrieved from cache, or if the cached page content contains "_INT" objects,
+     * flag $needsFullSetup is given true, and the full TypoScript is calculated since at least parts of
+     * the page content has to be rendered, which then needs full TypoScript.
+     * If the page could be retrieved from cache, and contains no "_INT" objects, $needsFullSetup in false, the
+     * rendering chain only needs the "config." part of TypoScript to satisfy the remaining middlewares.
+     *
+     * The method implements these variants and tries to add as little overhead as possible.
+     *
+     * Returns the FrontendTypoScript object:
+     * * configTree: Always set. Global TypoScript 'config.' merged with overrides from given type/typeNum "page.config.".
+     * * configArray: Always set. Array representation of configTree.
+     * * setupTree: Not set if $needsFullSetup=false and configTree could be retrieved from cache. Full TypoScript setup.
+     * * setupArray: Not set if $needsFullSetup=false and configTree could be retrieved from cache.
+     *               Array representation of setupTree.
+     * * pageTree: Not set if $needsFullSetup=false and configTree could be retrieved from cache, or if no PAGE object
+     *             could be determined. The 'PAGE' object tree for given type/typeNum.
+     * * pageArray: Not set if $needsFullSetup=false and configTree could be retrieved from cache, or if no PAGE object
+     *              could be determined. Array representation of PageTree.
+     */
+    public function createSetupConfigOrFullSetup(
+        bool $needsFullSetup,
+        FrontendTypoScript $frontendTypoScript,
+        SiteInterface $site,
+        array $sysTemplateRows,
+        array $expressionMatcherVariables,
+        string $type,
+        ?PhpFrontend $typoScriptCache,
+        ?ServerRequestInterface $request,
+    ): FrontendTypoScript {
+        $setupTypoScriptCacheIdentifier = 'setup-' . hash(
+            'xxh3',
+            json_encode($sysTemplateRows, JSON_THROW_ON_ERROR)
+            . json_encode($frontendTypoScript->getSettingsConditionList(), JSON_THROW_ON_ERROR)
+            . json_encode($frontendTypoScript->getSetupConditionList(), JSON_THROW_ON_ERROR)
+        );
+        $setupConfigTypoScriptCacheIdentifier = 'setup-config-' . hash('xxh3', $setupTypoScriptCacheIdentifier . $type);
+
+        $gotSetupConfigFromCache = false;
+        if ($setupConfigTypoScriptCache = $typoScriptCache?->require($setupConfigTypoScriptCacheIdentifier)) {
+            $frontendTypoScript->setConfigTree($setupConfigTypoScriptCache['ast']);
+            $frontendTypoScript->setConfigArray($setupConfigTypoScriptCache['array']);
+            if (!$needsFullSetup) {
+                // Fully cached page context without _INT - only 'config' is needed. Return early.
+                return $frontendTypoScript;
+            }
+            $gotSetupConfigFromCache = true;
+        }
+
+        $setupRawConfigAst = null;
+        if (!$typoScriptCache || $needsFullSetup || !$gotSetupConfigFromCache) {
+            // If caching is not allowed, if no page cache entry could be loaded or if the page cache entry has _INT
+            // object, we need the full setup AST. Try to use a cache entry for setup AST, which especially up _INT
+            // parsing. In unavailable, calculate full setup AST and cache it if allowed.
+            $gotSetupFromCache = false;
+            if ($setupTypoScriptCache = $typoScriptCache?->require($setupTypoScriptCacheIdentifier)) {
+                // We need AST, and we got it from cache.
+                $frontendTypoScript->setSetupTree($setupTypoScriptCache['ast']);
+                $frontendTypoScript->setSetupArray($setupTypoScriptCache['array']);
+                $setupRawConfigAst = $setupTypoScriptCache['ast']->getChildByName('config');
+                $gotSetupFromCache = true;
+            }
+            if (!$typoScriptCache || !$gotSetupFromCache) {
+                // We need AST and couldn't get it from cache or are now allowed to. We thus need the full setup
+                // IncludeTree, which we can get from cache again if allowed, or is calculated a-new if not.
+                $setupIncludeTree = $frontendTypoScript->getSetupIncludeTree();
+                if (!$typoScriptCache || $setupIncludeTree === null) {
+                    // A previous method *may* have calculated setup include tree already. Calculate now if not.
+                    $setupIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $this->tokenizer, $site, $typoScriptCache);
+                }
+                $visitors = [];
+                $conditionConstantSubstitutionVisitor = $this->container->get(IncludeTreeSetupConditionConstantSubstitutionVisitor::class);
+                $conditionConstantSubstitutionVisitor->setFlattenedConstants($frontendTypoScript->getFlatSettings());
+                $visitors[] = $conditionConstantSubstitutionVisitor;
+                $conditionMatcherVisitor = $this->container->get(IncludeTreeConditionMatcherVisitor::class);
+                $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
+                $visitors[] = $conditionMatcherVisitor;
+                $astBuilderVisitor = $this->container->get(IncludeTreeAstBuilderVisitor::class);
+                $astBuilderVisitor->setFlatConstants($frontendTypoScript->getFlatSettings());
+                $visitors[] = $astBuilderVisitor;
+                $this->includeTreeTraverserConditionVerdictAware->traverse($setupIncludeTree, $visitors);
+                $setupAst = $astBuilderVisitor->getAst();
+                // @todo: It would be good to actively remove 'config' from AST and array here
+                //        to prevent people from using the unmerged variant. The same
+                //        is already done for the determined PAGE 'config' below. This works, but
+                //        is currently blocked by functional tests that assert details?
+                //        Also, we need to still cache with full 'config' to handle multiple types.
+                $setupRawConfigAst = $setupAst->getChildByName('config');
+                // $setupAst->removeChildByName('config');
+                $frontendTypoScript->setSetupTree($setupAst);
+                $frontendTypoScript->setSetupArray($setupAst->toArray());
+
+                // Write cache entry for AST and its array representation.
+                $typoScriptCache?->set(
+                    $setupTypoScriptCacheIdentifier,
+                    'return unserialize(\'' . addcslashes(serialize(['ast' => $setupAst, 'array' => $setupAst->toArray()]), '\'\\') . '\');'
+                );
+            }
+
+            $setupAst = $frontendTypoScript->getSetupTree();
+            $rawSetupPageNodeFromType = null;
+            $pageNodeFoundByType = false;
+            foreach ($setupAst->getNextChild() as $potentialPageNode) {
+                // Find the PAGE object that matches given type/typeNum
+                if ($potentialPageNode->getValue() === 'PAGE') {
+                    // @todo: We could potentially remove *all* PAGE objects from setup here. This prevents people
+                    //        from accessing other ones than the determined one in $frontendTypoScript->getSetupArray().
+                    $typeNumChild = $potentialPageNode->getChildByName('typeNum');
+                    if ($typeNumChild && $type === $typeNumChild->getValue()) {
+                        $rawSetupPageNodeFromType = $potentialPageNode;
+                        $pageNodeFoundByType = true;
+                        break;
+                    }
+                    if (!$typeNumChild && $type === '0') {
+                        // The first PAGE node that has no typeNum is considered '0' automatically.
+                        $rawSetupPageNodeFromType = $potentialPageNode;
+                        $pageNodeFoundByType = true;
+                        break;
+                    }
+                }
+            }
+            if (!$pageNodeFoundByType) {
+                $rawSetupPageNodeFromType = new RootNode();
+            }
+            $setupPageAst = new RootNode();
+            foreach ($rawSetupPageNodeFromType->getNextChild() as $child) {
+                $setupPageAst->addChild($child);
+            }
+
+            if (!$gotSetupConfigFromCache) {
+                // If we did not get merged 'config.' from cache above, create it now and cache it.
+                $mergedSetupConfigAst = (new SetupConfigMerger())->merge($setupRawConfigAst, $setupPageAst->getChildByName('config'));
+                if ($mergedSetupConfigAst->getChildByName('absRefPrefix') === null) {
+                    // Make sure config.absRefPrefix is set, fallback to 'auto'.
+                    $absRefPrefixNode = new ChildNode('absRefPrefix');
+                    $absRefPrefixNode->setValue('auto');
+                    $mergedSetupConfigAst->addChild($absRefPrefixNode);
+                }
+                if ($mergedSetupConfigAst->getChildByName('doctype') === null) {
+                    // Make sure config.doctype is set, fallback to 'html5'.
+                    $doctypeNode = new ChildNode('doctype');
+                    $doctypeNode->setValue('html5');
+                    $mergedSetupConfigAst->addChild($doctypeNode);
+                }
+                if ($request) {
+                    // Dispatch ModifyTypoScriptConfigEvent before config is cached and if Request is given.
+                    $mergedSetupConfigAst = $this->eventDispatcher
+                        ->dispatch(new ModifyTypoScriptConfigEvent($request, $setupAst, $mergedSetupConfigAst))->getConfigTree();
+                }
+                $frontendTypoScript->setConfigTree($mergedSetupConfigAst);
+                $setupConfigArray = $mergedSetupConfigAst->toArray();
+                $frontendTypoScript->setConfigArray($setupConfigArray);
+                $typoScriptCache?->set(
+                    $setupConfigTypoScriptCacheIdentifier,
+                    'return unserialize(\'' . addcslashes(serialize(['ast' => $mergedSetupConfigAst, 'array' => $setupConfigArray]), '\'\\') . '\');'
+                );
+            }
+
+            if ($pageNodeFoundByType) {
+                // Remove "page.config" to prevent people from working with the not merged variant.
+                // We do *not* set page if it could not be determined (important for hasPage() later
+                // to return an early "no PAGE for type found" Response.
+                $setupPageAst->removeChildByName('config');
+                $frontendTypoScript->setPageTree($setupPageAst);
+                $frontendTypoScript->setPageArray($setupPageAst->toArray());
+            }
+        }
+        return $frontendTypoScript;
+    }
+}
diff --git a/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeSetupConditionConstantSubstitutionVisitor.php b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeSetupConditionConstantSubstitutionVisitor.php
index 3d2c4e34c360..83cc53a579d5 100644
--- a/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeSetupConditionConstantSubstitutionVisitor.php
+++ b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeSetupConditionConstantSubstitutionVisitor.php
@@ -53,8 +53,8 @@ final class IncludeTreeSetupConditionConstantSubstitutionVisitor implements Incl
 
     /**
      * Do the magic, see tests for details.
-     * Implementation within 'visitBeforeChilden()' since this allows running *both* this
-     * visitor first, and then TreeVisitorConditionMatcher directly afterwards in the same
+     * Implementation within 'visitBeforeChildren()' since this allows running *both* this
+     * visitor first, and then IncludeTreeConditionMatcherVisitor directly afterward in the same
      * traverser cycle!
      */
     public function visitBeforeChildren(IncludeInterface $include, int $currentDepth): void
diff --git a/typo3/sysext/core/Configuration/Services.yaml b/typo3/sysext/core/Configuration/Services.yaml
index fe5314605e60..7d38fbae5174 100644
--- a/typo3/sysext/core/Configuration/Services.yaml
+++ b/typo3/sysext/core/Configuration/Services.yaml
@@ -283,11 +283,21 @@ services:
     # This Ast builder visitor creates state and should not be re-used
     shared: false
 
+  TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionIncludeListAccumulatorVisitor:
+    public: true
+    # This visitor creates state and should not be re-used
+    shared: false
+
   TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionMatcherVisitor:
     public: true
     # This visitor creates state and should not be re-used
     shared: false
 
+  TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSetupConditionConstantSubstitutionVisitor:
+    public: true
+    # This visitor creates state and should not be re-used
+    shared: false
+
   TYPO3\CMS\Core\TypoScript\Tokenizer\TokenizerInterface:
     alias: TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer
 
diff --git a/typo3/sysext/extbase/Classes/Configuration/BackendConfigurationManager.php b/typo3/sysext/extbase/Classes/Configuration/BackendConfigurationManager.php
index b4a6f425de48..dec4d7f844b7 100644
--- a/typo3/sysext/extbase/Classes/Configuration/BackendConfigurationManager.php
+++ b/typo3/sysext/extbase/Classes/Configuration/BackendConfigurationManager.php
@@ -31,13 +31,8 @@ use TYPO3\CMS\Core\SingletonInterface;
 use TYPO3\CMS\Core\Site\Entity\NullSite;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Type\Bitmask\Permission;
+use TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository;
-use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateTreeBuilder;
-use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser;
-use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeAstBuilderVisitor;
-use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionMatcherVisitor;
-use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSetupConditionConstantSubstitutionVisitor;
-use TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer;
 use TYPO3\CMS\Core\TypoScript\TypoScriptService;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -102,10 +97,8 @@ class BackendConfigurationManager implements SingletonInterface
         private readonly PhpFrontend $typoScriptCache,
         private readonly FrontendInterface $runtimeCache,
         private readonly SysTemplateRepository $sysTemplateRepository,
-        private readonly SysTemplateTreeBuilder $treeBuilder,
-        private readonly LossyTokenizer $lossyTokenizer,
-        private readonly ConditionVerdictAwareIncludeTreeTraverser $includeTreeTraverserConditionVerdictAware,
         private readonly SiteFinder $siteFinder,
+        private readonly FrontendTypoScriptFactory $frontendTypoScriptFactory,
     ) {}
 
     public function setRequest(ServerRequestInterface $request): void
@@ -205,6 +198,10 @@ class BackendConfigurationManager implements SingletonInterface
                 // Keep null / NullSite when no site could be determined for whatever reason.
             }
         }
+        if ($site === null) {
+            // If still no site object, have NullSite (usually pid 0).
+            $site = new NullSite();
+        }
 
         $rootLine = [];
         $sysTemplateFakeRow = [
@@ -236,10 +233,6 @@ class BackendConfigurationManager implements SingletonInterface
             $sysTemplateRows[] = $sysTemplateFakeRow;
         }
 
-        // We do cache tree and tokens, but don't cache full ast in this backend context for now:
-        // That's a possible improvement to further speed up extbase backend modules, but a bit of
-        // hassle. See the Frontend TypoScript calculation on how to do this.
-        $constantIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $this->lossyTokenizer, $site, $this->typoScriptCache);
         $expressionMatcherVariables = [
             'request' => $this->request,
             'pageId' => $pageId,
@@ -247,34 +240,11 @@ class BackendConfigurationManager implements SingletonInterface
             'fullRootLine' => $rootLine,
             'site' => $site,
         ];
-        $conditionMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class);
-        $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
-        $includeTreeTraverserConditionVerdictAwareVisitors = [];
-        $includeTreeTraverserConditionVerdictAwareVisitors[] = $conditionMatcherVisitor;
-        $constantAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeAstBuilderVisitor::class);
-        $includeTreeTraverserConditionVerdictAwareVisitors[] = $constantAstBuilderVisitor;
-        $this->includeTreeTraverserConditionVerdictAware->traverse($constantIncludeTree, $includeTreeTraverserConditionVerdictAwareVisitors);
-        $constantsAst = $constantAstBuilderVisitor->getAst();
-        $flatConstants = $constantsAst->flatten();
-
-        $setupIncludeTree = $this->treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $this->lossyTokenizer, $site, $this->typoScriptCache);
-        $includeTreeTraverserConditionVerdictAwareVisitors = [];
-        $setupConditionConstantSubstitutionVisitor = new IncludeTreeSetupConditionConstantSubstitutionVisitor();
-        $setupConditionConstantSubstitutionVisitor->setFlattenedConstants($flatConstants);
-        $includeTreeTraverserConditionVerdictAwareVisitors[] = $setupConditionConstantSubstitutionVisitor;
-        $setupMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class);
-        $setupMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
-        $includeTreeTraverserConditionVerdictAwareVisitors[] = $setupMatcherVisitor;
-        $setupAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeAstBuilderVisitor::class);
-        $setupAstBuilderVisitor->setFlatConstants($flatConstants);
-        $includeTreeTraverserConditionVerdictAwareVisitors[] = $setupAstBuilderVisitor;
-        $this->includeTreeTraverserConditionVerdictAware->traverse($setupIncludeTree, $includeTreeTraverserConditionVerdictAwareVisitors);
-        $setupAst = $setupAstBuilderVisitor->getAst();
-
-        $setupArray = $setupAst->toArray();
 
+        $typoScript = $this->frontendTypoScriptFactory->createSettingsAndSetupConditions($site, $sysTemplateRows, $expressionMatcherVariables, $this->typoScriptCache);
+        $typoScript = $this->frontendTypoScriptFactory->createSetupConfigOrFullSetup(true, $typoScript, $site, $sysTemplateRows, $expressionMatcherVariables, '0', $this->typoScriptCache, null);
+        $setupArray = $typoScript->getSetupArray();
         $this->runtimeCache->set($cacheIdentifier, $setupArray);
-
         return $setupArray;
     }
 
diff --git a/typo3/sysext/extbase/Tests/Functional/Configuration/FrontendConfigurationManagerTest.php b/typo3/sysext/extbase/Tests/Functional/Configuration/FrontendConfigurationManagerTest.php
index f2198087f811..b37038551089 100644
--- a/typo3/sysext/extbase/Tests/Functional/Configuration/FrontendConfigurationManagerTest.php
+++ b/typo3/sysext/extbase/Tests/Functional/Configuration/FrontendConfigurationManagerTest.php
@@ -56,7 +56,7 @@ final class FrontendConfigurationManagerTest extends FunctionalTestCase
     </data>
 </T3FlexForms>';
 
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray($typoScript);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())->withAttribute('frontend.typoscript', $frontendTypoScript);
 
@@ -75,7 +75,7 @@ final class FrontendConfigurationManagerTest extends FunctionalTestCase
         $contentObject = new ContentObjectRenderer();
         $contentObject->data = ['pi_flexform' => $flexForm];
         $request = (new ServerRequest())->withAttribute('currentContentObject', $contentObject);
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $request = $request->withAttribute('frontend.typoscript', $frontendTypoScript);
         $frontendConfigurationManager = $this->get(FrontendConfigurationManager::class);
@@ -356,7 +356,7 @@ final class FrontendConfigurationManagerTest extends FunctionalTestCase
         $contentObject = new ContentObjectRenderer();
         $contentObject->data = ['pi_flexform' => $flexForm];
         $request = (new ServerRequest())->withAttribute('currentContentObject', $contentObject);
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray($typoScript);
         $request = $request->withAttribute('frontend.typoscript', $frontendTypoScript);
         $frontendConfigurationManager = $this->get(FrontendConfigurationManager::class);
diff --git a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerArgumentTest.php b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerArgumentTest.php
index 8b35411d8fc3..955edaeec330 100644
--- a/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerArgumentTest.php
+++ b/typo3/sysext/extbase/Tests/Functional/Mvc/Controller/ActionControllerArgumentTest.php
@@ -191,7 +191,7 @@ final class ActionControllerArgumentTest extends FunctionalTestCase
 
     private function buildRequest(string $actionName, array $arguments = null): Request
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $serverRequest = (new ServerRequest())
             ->withAttribute('extbase', new ExtbaseRequestParameters())
diff --git a/typo3/sysext/extbase/Tests/Functional/Mvc/Web/RequestBuilderTest.php b/typo3/sysext/extbase/Tests/Functional/Mvc/Web/RequestBuilderTest.php
index 0b093a2731ad..e59be60434cd 100644
--- a/typo3/sysext/extbase/Tests/Functional/Mvc/Web/RequestBuilderTest.php
+++ b/typo3/sysext/extbase/Tests/Functional/Mvc/Web/RequestBuilderTest.php
@@ -678,7 +678,7 @@ final class RequestBuilderTest extends FunctionalTestCase
     {
         $pageArguments = new PageArguments(1, '0', ['tx_blog_example_blog' => ['action' => 'show']]);
 
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $mainRequest = $this->prepareServerRequest('https://example.com/');
         $mainRequest = $mainRequest
diff --git a/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbBackendTest.php b/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbBackendTest.php
index cadc67b8019a..bbe377817f92 100644
--- a/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbBackendTest.php
+++ b/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbBackendTest.php
@@ -55,7 +55,7 @@ final class Typo3DbBackendTest extends FunctionalTestCase
     public function getObjectDataByQueryChangesUidIfInPreview(): void
     {
         $this->importCSVDataSet(__DIR__ . '/Fixtures/Typo3DbBackendTestImport.csv');
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
diff --git a/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbQueryParserTest.php b/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbQueryParserTest.php
index 7280dec7c025..d98d9353f05e 100644
--- a/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbQueryParserTest.php
+++ b/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbQueryParserTest.php
@@ -48,7 +48,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
     #[Test]
     public function convertQueryToDoctrineQueryBuilderDoesNotAddAndWhereWithEmptyConstraint(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -67,7 +67,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
     #[Test]
     public function convertQueryToDoctrineQueryBuilderThrowsExceptionOnNotImplementedConstraint(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -90,7 +90,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
     #[Test]
     public function convertQueryToDoctrineQueryBuilderAddsSimpleAndWhere(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -112,7 +112,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
     #[Test]
     public function convertQueryToDoctrineQueryBuilderAddsNotConstraint(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -134,7 +134,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
     #[Test]
     public function convertQueryToDoctrineQueryBuilderAddsAndConstraint(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -160,7 +160,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
     #[Test]
     public function convertQueryToDoctrineQueryBuilderAddsOrConstraint(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -186,7 +186,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
     #[Test]
     public function languageStatementWorksForDefaultLanguage(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -204,7 +204,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
     #[Test]
     public function languageStatementWorksForNonDefaultLanguage(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -267,7 +267,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
     #[Test]
     public function addGetLanguageStatementWorksForForeignLanguageWithSubselectionTakesDeleteStatementIntoAccountIfNecessary(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -312,7 +312,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
     #[Test]
     public function orderStatementGenerationWorks(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -339,7 +339,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
         $this->expectException(UnsupportedOrderException::class);
         $this->expectExceptionCode(1242816074);
 
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -360,7 +360,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
     #[Test]
     public function orderStatementGenerationWorksWithMultipleOrderings(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -473,7 +473,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
     #[Test]
     public function expressionIsOmittedForIgnoreEnableFieldsAreAndDoNotIncludeDeletedInFrontendContext(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -497,7 +497,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
     #[Test]
     public function expressionIsGeneratedForIgnoreEnableFieldsAndDoNotIncludeDeletedInFrontendContext(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -521,7 +521,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
     #[Test]
     public function expressionIsGeneratedForIgnoreOnlyFeGroupAndDoNotIncludeDeletedInFrontendContext(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -547,7 +547,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
     #[Test]
     public function expressionIsGeneratedForDoNotIgnoreEnableFieldsAndDoNotIncludeDeletedInFrontendContext(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -573,7 +573,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
     public function respectEnableFieldsSettingGeneratesCorrectStatementWithOnlyEndTimeInFrontendContext(): void
     {
         $GLOBALS['TCA']['tx_blogexample_domain_model_blog']['ctrl']['enablecolumns']['endtime'] = 'endtime_column';
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -601,7 +601,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
         // simulate time for backend enable fields
         $GLOBALS['SIM_ACCESS_TIME'] = 1451779200;
         $GLOBALS['TCA']['tx_blogexample_domain_model_blog']['ctrl']['enablecolumns']['endtime'] = 'endtime_column';
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
@@ -622,7 +622,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
     #[Test]
     public function visibilityConstraintStatementGenerationThrowsExceptionIfTheQuerySettingsAreInconsistent(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -684,7 +684,7 @@ final class Typo3DbQueryParserTest extends FunctionalTestCase
         $GLOBALS['TCA']['tx_blogexample_domain_model_blog']['ctrl'] = [
             'rootLevel' => $rootLevel,
         ];
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
diff --git a/typo3/sysext/extbase/Tests/Functional/Persistence/TranslationTest.php b/typo3/sysext/extbase/Tests/Functional/Persistence/TranslationTest.php
index 9423e759d00e..91844e502de2 100644
--- a/typo3/sysext/extbase/Tests/Functional/Persistence/TranslationTest.php
+++ b/typo3/sysext/extbase/Tests/Functional/Persistence/TranslationTest.php
@@ -45,7 +45,7 @@ final class TranslationTest extends FunctionalTestCase
 
         $context = GeneralUtility::makeInstance(Context::class);
         $context->setAspect('language', new LanguageAspect(0, 0, LanguageAspect::OVERLAYS_OFF));
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
diff --git a/typo3/sysext/extbase/Tests/Functional/Persistence/WorkspaceTest.php b/typo3/sysext/extbase/Tests/Functional/Persistence/WorkspaceTest.php
index c1deed47c62a..8088b0ea69ad 100644
--- a/typo3/sysext/extbase/Tests/Functional/Persistence/WorkspaceTest.php
+++ b/typo3/sysext/extbase/Tests/Functional/Persistence/WorkspaceTest.php
@@ -58,7 +58,7 @@ final class WorkspaceTest extends FunctionalTestCase
         $context = new Context();
         $context->setAspect('workspace', new WorkspaceAspect($workspaceId));
         GeneralUtility::setSingletonInstance(Context::class, $context);
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
@@ -86,7 +86,7 @@ final class WorkspaceTest extends FunctionalTestCase
         $context->setAspect('backend.user', new UserAspect($backendUser));
         $context->setAspect('workspace', new WorkspaceAspect($workspaceId));
         GeneralUtility::setSingletonInstance(Context::class, $context);
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE)
diff --git a/typo3/sysext/extbase/Tests/Unit/Configuration/FrontendConfigurationManagerTest.php b/typo3/sysext/extbase/Tests/Unit/Configuration/FrontendConfigurationManagerTest.php
index ca30813fedc6..b2d72e6b9830 100644
--- a/typo3/sysext/extbase/Tests/Unit/Configuration/FrontendConfigurationManagerTest.php
+++ b/typo3/sysext/extbase/Tests/Unit/Configuration/FrontendConfigurationManagerTest.php
@@ -439,7 +439,7 @@ final class FrontendConfigurationManagerTest extends UnitTestCase
     #[Test]
     public function getTypoScriptSetupReturnsSetupFromRequest(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray(['foo' => 'bar']);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())->withAttribute('frontend.typoscript', $frontendTypoScript);
         self::assertEquals(['foo' => 'bar'], $this->frontendConfigurationManager->_call('getTypoScriptSetup'));
@@ -448,7 +448,7 @@ final class FrontendConfigurationManagerTest extends UnitTestCase
     #[Test]
     public function getPluginConfigurationReturnsEmptyArrayIfNoPluginConfigurationWasFound(): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray(['foo' => 'bar']);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())->withAttribute('frontend.typoscript', $frontendTypoScript);
         $expectedResult = [];
@@ -479,7 +479,7 @@ final class FrontendConfigurationManagerTest extends UnitTestCase
             ],
         ];
         $this->mockTypoScriptService->method('convertTypoScriptArrayToPlainArray')->with($testSettings)->willReturn($testSettingsConverted);
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray($testSetup);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())->withAttribute('frontend.typoscript', $frontendTypoScript);
         $expectedResult = [
@@ -510,7 +510,7 @@ final class FrontendConfigurationManagerTest extends UnitTestCase
             ],
         ];
         $this->mockTypoScriptService->method('convertTypoScriptArrayToPlainArray')->with($testSettings)->willReturn($testSettingsConverted);
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray($testSetup);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())->withAttribute('frontend.typoscript', $frontendTypoScript);
         $expectedResult = [
@@ -577,7 +577,7 @@ final class FrontendConfigurationManagerTest extends UnitTestCase
                 self::assertSame($settings, $arguments[0]);
                 return $arguments[1];
             });
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray($testSetup);
         $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest())->withAttribute('frontend.typoscript', $frontendTypoScript);
         $expectedResult = [
diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/CObjectViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/CObjectViewHelperTest.php
index 7dd600abd238..122e5ca601d1 100644
--- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/CObjectViewHelperTest.php
+++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/CObjectViewHelperTest.php
@@ -45,13 +45,6 @@ final class CObjectViewHelperTest extends FunctionalTestCase
         );
     }
 
-    protected function tearDown(): void
-    {
-        // @todo: When a FE sub request throws an exception, as some of the below test do, TSFE does NOT release locks properly!
-        $GLOBALS['TSFE']->releaseLocks();
-        parent::tearDown();
-    }
-
     #[Test]
     public function viewHelperAcceptsDataParameter(): void
     {
diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/FormViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/FormViewHelperTest.php
index 91143b603ca2..b92ce8c982e3 100644
--- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/FormViewHelperTest.php
+++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/FormViewHelperTest.php
@@ -244,7 +244,7 @@ final class FormViewHelperTest extends FunctionalTestCase
 
     protected function createRequest(): ServerRequestInterface
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupTree(new RootNode());
         $frontendTypoScript->setSetupArray([]);
         $frontendTypoScript->setConfigArray([]);
diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/ActionViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/ActionViewHelperTest.php
index 79851ed366e9..b00a67d148e7 100644
--- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/ActionViewHelperTest.php
+++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/ActionViewHelperTest.php
@@ -136,7 +136,7 @@ final class ActionViewHelperTest extends FunctionalTestCase
             'test',
             $this->buildSiteConfiguration(1, '/'),
         );
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setConfigArray($frontendTypoScriptConfigArray);
         $request = new ServerRequest();
         $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE);
@@ -236,7 +236,7 @@ final class ActionViewHelperTest extends FunctionalTestCase
             'test',
             $this->buildSiteConfiguration(1, '/'),
         );
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $frontendTypoScript->setConfigArray($frontendTypoScriptConfigArray);
         $extbaseRequestParameters = new ExtbaseRequestParameters();
diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/PageViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/PageViewHelperTest.php
index c5c73ea75a64..6148c85312a2 100644
--- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/PageViewHelperTest.php
+++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Link/PageViewHelperTest.php
@@ -242,7 +242,7 @@ final class PageViewHelperTest extends FunctionalTestCase
             'test',
             $this->buildSiteConfiguration(1, '/'),
         );
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $frontendTypoScript->setConfigArray($frontendTypoScriptConfigArray);
         $request = new ServerRequest('http://localhost/typo3/');
@@ -267,7 +267,7 @@ final class PageViewHelperTest extends FunctionalTestCase
             'test',
             $this->buildSiteConfiguration(1, '/'),
         );
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray($frontendTypoScriptSetupArray);
         $frontendTypoScript->setConfigArray([]);
         $request = new ServerRequest();
diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/ActionViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/ActionViewHelperTest.php
index 836331b47360..d102abdb0806 100644
--- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/ActionViewHelperTest.php
+++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Uri/ActionViewHelperTest.php
@@ -187,7 +187,7 @@ final class ActionViewHelperTest extends FunctionalTestCase
             'test',
             $this->buildSiteConfiguration(1, '/'),
         );
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $frontendTypoScript->setConfigArray([]);
         $extbaseRequestParameters = new ExtbaseRequestParameters();
diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
index f93bf5418c04..46e0d963c3bf 100644
--- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
+++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
@@ -24,49 +24,25 @@ use Psr\Log\LogLevel;
 use TYPO3\CMS\Backend\FrontendBackendUserAuthentication;
 use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
-use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Core\Environment;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Domain\Repository\PageRepository;
-use TYPO3\CMS\Core\Error\Http\StatusException;
-use TYPO3\CMS\Core\Http\PropagateResponseException;
 use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
 use TYPO3\CMS\Core\Localization\Locale;
 use TYPO3\CMS\Core\Localization\Locales;
-use TYPO3\CMS\Core\Locking\ResourceMutex;
 use TYPO3\CMS\Core\Page\AssetCollector;
 use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Core\PageTitle\PageTitleProviderManager;
-use TYPO3\CMS\Core\Routing\PageArguments;
 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Core\Type\DocType;
-use TYPO3\CMS\Core\TypoScript\AST\Merger\SetupConfigMerger;
-use TYPO3\CMS\Core\TypoScript\AST\Node\ChildNode;
-use TYPO3\CMS\Core\TypoScript\AST\Node\RootNode;
-use TYPO3\CMS\Core\TypoScript\FrontendTypoScript;
-use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateTreeBuilder;
-use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser;
-use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser;
-use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeAstBuilderVisitor;
-use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionIncludeListAccumulatorVisitor;
-use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeConditionMatcherVisitor;
-use TYPO3\CMS\Core\TypoScript\IncludeTree\Visitor\IncludeTreeSetupConditionConstantSubstitutionVisitor;
-use TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Frontend\Cache\CacheLifetimeCalculator;
 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
 use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent;
 use TYPO3\CMS\Frontend\Event\AfterCachedPageIsPersistedEvent;
-use TYPO3\CMS\Frontend\Event\BeforePageCacheIdentifierIsHashedEvent;
-use TYPO3\CMS\Frontend\Event\ModifyTypoScriptConfigEvent;
-use TYPO3\CMS\Frontend\Event\ModifyTypoScriptConstantsEvent;
-use TYPO3\CMS\Frontend\Event\ShouldUseCachedPageDataIfAvailableEvent;
-use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
-use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
 
 /**
  * Main controller class of the TypoScript based frontend.
@@ -147,14 +123,21 @@ class TypoScriptFrontendController implements LoggerAwareInterface
 
     /**
      * Set if cached content was fetched from the cache.
+     *
+     * @internal Used by a middleware. Will be removed.
      */
-    protected bool $pageContentWasLoadedFromCache = false;
+    public bool $pageContentWasLoadedFromCache = false;
 
     /**
      * Set to the expiry time of cached content
+     * @internal Used by a middleware. Will be removed.
+     */
+    public int $cacheExpires = 0;
+
+    /**
+     * @internal Used by a middleware. Will be removed.
      */
-    protected int $cacheExpires = 0;
-    private int $cacheGenerated = 0;
+    public int $cacheGenerated = 0;
 
     /**
      * This hash is unique to the page id, involved TS templates, TS condition verdicts, and
@@ -244,14 +227,13 @@ class TypoScriptFrontendController implements LoggerAwareInterface
 
     protected LanguageService $languageService;
 
-    /**
-     * @internal Internal locking. May move to a middleware soon.
-     */
-    public ?ResourceMutex $lock = null;
-
     protected ?PageRenderer $pageRenderer = null;
     protected FrontendInterface $pageCache;
-    protected array $pageCacheTags = [];
+
+    /**
+     * @internal Used by a middleware. Will be removed.
+     */
+    public array $pageCacheTags = [];
 
     /**
      * Content type HTTP header being sent in the request.
@@ -268,8 +250,9 @@ class TypoScriptFrontendController implements LoggerAwareInterface
     /**
      * If debug mode is enabled, this contains the information if a page is fetched from cache,
      * and sent as HTTP Response Header.
+     * @internal Used by a middleware. Will be removed.
      */
-    protected ?string $debugInformationHeader = null;
+    public ?string $debugInformationHeader = null;
 
     /**
      * @internal Extensions should usually not need to create own instances of TSFE
@@ -323,459 +306,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface
         $this->contentType = $contentType;
     }
 
-    /**
-     * Fetches the arguments that are relevant for creating the hash base from the given PageArguments object.
-     * Excluded parameters are not taken into account when calculating the hash base.
-     */
-    protected function getRelevantParametersForCachingFromPageArguments(PageArguments $pageArguments): array
-    {
-        $queryParams = $pageArguments->getDynamicArguments();
-        if (!empty($queryParams) && ($pageArguments->getArguments()['cHash'] ?? false)) {
-            $queryParams['id'] = $pageArguments->getPageId();
-            return GeneralUtility::makeInstance(CacheHashCalculator::class)
-                ->getRelevantParameters(HttpUtility::buildQueryString($queryParams));
-        }
-        return [];
-    }
-
-    /**
-     * This is a central and quite early method called by PrepareTypoScriptFrontendRendering middleware:
-     * This code is *always* executed for *every* frontend call if a general page rendering has to be done,
-     * if there is no early redirect or eid call or similar.
-     *
-     * The goal is to calculate dependencies up to a point to see if a possible page cache can be used,
-     * and to prepare TypoScript as far as really needed.
-     *
-     * @throws PropagateResponseException
-     * @throws StatusException
-     *
-     * @internal This method may vanish from TypoScriptFrontendController without further notice.
-     * @todo: This method is typically called by PrepareTypoScriptFrontendRendering middleware.
-     *        However, the RedirectService of (earlier) ext:redirects RedirectHandler middleware
-     *        calls this as well. We may want to put this code into some helper class, reduce class
-     *        state as much as possible and carry really needed state as request attributes around?!
-     */
-    public function getFromCache(ServerRequestInterface $request): FrontendTypoScript
-    {
-        $pageInformation = $request->getAttribute('frontend.page.information');
-        $rootLine = $pageInformation->getRootLine();
-        $localRootline = $pageInformation->getLocalRootLine();
-        $sysTemplateRows = $pageInformation->getSysTemplateRows();
-        $serializedSysTemplateRows = serialize($sysTemplateRows);
-        $site = $request->getAttribute('site');
-        $isCachingAllowed = $request->getAttribute('frontend.cache.instruction')->isCachingAllowed();
-
-        $tokenizer = new LossyTokenizer();
-        $treeBuilder = GeneralUtility::makeInstance(SysTemplateTreeBuilder::class);
-        $includeTreeTraverser = new IncludeTreeTraverser();
-        $includeTreeTraverserConditionVerdictAware = new ConditionVerdictAwareIncludeTreeTraverser();
-        $cacheManager = GeneralUtility::makeInstance(CacheManager::class);
-        /** @var PhpFrontend|null $typoscriptCache */
-        $typoscriptCache = null;
-        if ($isCachingAllowed) {
-            // disableCache() might have been called by earlier middlewares. This means we don't do fancy cache
-            // stuff, calculate full TypoScript and don't get() from nor set() to typoscript and page cache.
-            /** @var PhpFrontend|null $typoscriptCache */
-            $typoscriptCache = $cacheManager->getCache('typoscript');
-        }
-
-        $topDownRootLine = $rootLine;
-        ksort($topDownRootLine);
-        $expressionMatcherVariables = [
-            'request' => $request,
-            'pageId' => $pageInformation->getId(),
-            // @todo We're using the full page row here to provide all necessary fields (e.g. "backend_layout"),
-            //       which are currently not included in the rows, RootlineUtility provides by default. We might
-            //       want to switch to $pageInformation->getRootLine() as soon as it contains all fields.
-            'page' => $pageInformation->getPageRecord(),
-            'fullRootLine' => $topDownRootLine,
-            'localRootLine' => $localRootline,
-            'site' => $site,
-            'siteLanguage' => $request->getAttribute('language'),
-            'tsfe' => $this,
-        ];
-
-        // We *always* need the TypoScript constants, one way or the other: Setup conditions can use constants,
-        // so we need the constants to substitute their values within setup conditions.
-        $constantConditionIncludeListCacheIdentifier = 'constant-condition-include-list-' . sha1($serializedSysTemplateRows);
-        $constantConditionList = [];
-        $constantsAst = new RootNode();
-        $flatConstants = [];
-        $serializedConstantConditionList = '';
-        $gotConstantFromCache = false;
-        if ($isCachingAllowed && $constantConditionIncludeTree = $typoscriptCache->require($constantConditionIncludeListCacheIdentifier)) {
-            // We got the flat list of all constants conditions for this TypoScript combination from cache. Good. We traverse
-            // this list to calculate "current" condition verdicts. With a hash of this list together with a hash of the
-            // TypoScript sys_templates, we try to retrieve the full constants TypoScript from cache.
-            $conditionMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class);
-            $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
-            // It does not matter if we use IncludeTreeTraverser or ConditionVerdictAwareIncludeTreeTraverser here:
-            // Condition list is flat, not nested. IncludeTreeTraverser has an if() less, so we use that one.
-            $includeTreeTraverser->traverse($constantConditionIncludeTree, [$conditionMatcherVisitor]);
-            $constantConditionList = $conditionMatcherVisitor->getConditionListWithVerdicts();
-            // Needed for cache identifier calculations. Put into a variable here to not serialize multiple times.
-            $serializedConstantConditionList = serialize($constantConditionList);
-            $constantCacheEntryIdentifier = 'constant-' . sha1($serializedSysTemplateRows . $serializedConstantConditionList);
-            $constantsCacheEntry = $typoscriptCache->require($constantCacheEntryIdentifier);
-            if (is_array($constantsCacheEntry)) {
-                $constantsAst = $constantsCacheEntry['ast'];
-                $flatConstants = $constantsCacheEntry['flatConstants'];
-                $gotConstantFromCache = true;
-            }
-        }
-        if (!$isCachingAllowed || !$gotConstantFromCache) {
-            // We did not get constants from cache, or are not allowed to use cache. We have to build constants from scratch.
-            // This means we'll fetch the full constants include tree (from cache if possible), register the condition
-            // matcher and register the AST builder and traverse include tree to retrieve constants AST and calculate
-            // 'flat constants' from it. Both are cached if allowed afterwards for the 'if' above to kick in next time.
-            $constantIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('constants', $sysTemplateRows, $tokenizer, $site, $typoscriptCache);
-            $conditionMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class);
-            $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
-            $includeTreeTraverserConditionVerdictAwareVisitors = [];
-            $includeTreeTraverserConditionVerdictAwareVisitors[] = $conditionMatcherVisitor;
-            $constantAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeAstBuilderVisitor::class);
-            $includeTreeTraverserConditionVerdictAwareVisitors[] = $constantAstBuilderVisitor;
-            // We must use ConditionVerdictAwareIncludeTreeTraverser here: This one does not walk into
-            // children for not matching conditions, which is important to create the correct AST.
-            $includeTreeTraverserConditionVerdictAware->traverse($constantIncludeTree, $includeTreeTraverserConditionVerdictAwareVisitors);
-            $constantsAst = $constantAstBuilderVisitor->getAst();
-            // @internal Dispatch an experimental event allowing listeners to still change the constants AST,
-            //           to for instance implement nested constants if really needed. Note this event may change
-            //           or vanish later without further notice.
-            $constantsAst = GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch(new ModifyTypoScriptConstantsEvent($constantsAst))->getConstantsAst();
-            $flatConstants = $constantsAst->flatten();
-            if ($isCachingAllowed) {
-                // We are allowed to cache and can create both the full list of conditions, plus the constant AST and flat constant
-                // list cache entry. To do that, we need all (!) conditions, but the above ConditionVerdictAwareIncludeTreeTraverser
-                // did not find nested conditions if an upper condition did not match. We thus have to traverse include tree a
-                // second time with the IncludeTreeTraverser that does traverse into not matching conditions as well.
-                $includeTreeTraverserVisitors = [];
-                $conditionMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class);
-                $conditionMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
-                $includeTreeTraverserVisitors[] = $conditionMatcherVisitor;
-                $constantConditionIncludeListAccumulatorVisitor = new IncludeTreeConditionIncludeListAccumulatorVisitor();
-                $includeTreeTraverserVisitors[] = $constantConditionIncludeListAccumulatorVisitor;
-                $includeTreeTraverser->traverse($constantIncludeTree, $includeTreeTraverserVisitors);
-                $constantConditionList = $conditionMatcherVisitor->getConditionListWithVerdicts();
-                // Needed for cache identifier calculations. Put into a variable here to not serialize multiple times.
-                $serializedConstantConditionList = serialize($constantConditionList);
-                $typoscriptCache->set($constantConditionIncludeListCacheIdentifier, 'return unserialize(\'' . addcslashes(serialize($constantConditionIncludeListAccumulatorVisitor->getConditionIncludes()), '\'\\') . '\');');
-                $constantCacheEntryIdentifier = 'constant-' . sha1($serializedSysTemplateRows . $serializedConstantConditionList);
-                $typoscriptCache->set($constantCacheEntryIdentifier, 'return unserialize(\'' . addcslashes(serialize(['ast' => $constantsAst, 'flatConstants' => $flatConstants]), '\'\\') . '\');');
-            }
-        }
-
-        $frontendTypoScript = new FrontendTypoScript($constantsAst, $flatConstants);
-
-        // Next step: We have constants and fetch the setup include tree now. We then calculate setup condition verdicts
-        // and set the constants to allow substitution of constants within conditions. Next, we traverse include tree
-        // to calculate conditions verdicts and gather them along the way. A hash of these conditions with their verdicts
-        // is then part of the page cache identifier hash: When a condition on a page creates a different result, the hash
-        // is different from an existing page cache entry and a new one is created later.
-        $setupConditionIncludeListCacheIdentifier = 'setup-condition-include-list-' . sha1($serializedSysTemplateRows . $serializedConstantConditionList);
-        $setupConditionList = [];
-        $gotSetupConditionsFromCache = false;
-        if ($isCachingAllowed && $setupConditionIncludeTree = $typoscriptCache->require($setupConditionIncludeListCacheIdentifier)) {
-            // We got the flat list of all setup conditions for this TypoScript combination from cache. Good. We traverse
-            // this list to calculate "current" condition verdicts, which we need as hash to be part of page cache identifier.
-            $includeTreeTraverserVisitors = [];
-            $setupConditionConstantSubstitutionVisitor = new IncludeTreeSetupConditionConstantSubstitutionVisitor();
-            $setupConditionConstantSubstitutionVisitor->setFlattenedConstants($flatConstants);
-            $includeTreeTraverserVisitors[] = $setupConditionConstantSubstitutionVisitor;
-            $setupMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class);
-            $setupMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
-            $includeTreeTraverserVisitors[] = $setupMatcherVisitor;
-            // It does not matter if we use IncludeTreeTraverser or ConditionVerdictAwareIncludeTreeTraverser here:
-            // Condition list is flat, not nested. IncludeTreeTraverser has an if() less, so we use that one.
-            $includeTreeTraverser->traverse($setupConditionIncludeTree, $includeTreeTraverserVisitors);
-            $setupConditionList = $setupMatcherVisitor->getConditionListWithVerdicts();
-            $gotSetupConditionsFromCache = true;
-        }
-        $setupIncludeTree = null;
-        if (!$isCachingAllowed || !$gotSetupConditionsFromCache) {
-            // We did not get setup condition list from cache, or are not allowed to use cache. We have to build setup
-            // condition list from scratch. This means we'll fetch the full setup include tree (from cache if possible),
-            // register the constant substitution visitor, and register condition matcher and register the condition
-            // accumulator visitor.
-            $setupIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $tokenizer, $site, $typoscriptCache);
-            $includeTreeTraverserVisitors = [];
-            $setupConditionConstantSubstitutionVisitor = new IncludeTreeSetupConditionConstantSubstitutionVisitor();
-            $setupConditionConstantSubstitutionVisitor->setFlattenedConstants($flatConstants);
-            $includeTreeTraverserVisitors[] = $setupConditionConstantSubstitutionVisitor;
-            $setupMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class);
-            $setupMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
-            $includeTreeTraverserVisitors[] = $setupMatcherVisitor;
-            $setupConditionIncludeListAccumulatorVisitor = new IncludeTreeConditionIncludeListAccumulatorVisitor();
-            $includeTreeTraverserVisitors[] = $setupConditionIncludeListAccumulatorVisitor;
-            // It is important we use IncludeTreeTraverser here: We to have the condition verdicts of *all* conditions, plus
-            // want to accumulate all of them. The ConditionVerdictAwareIncludeTreeTraverser wouldn't walk into nested
-            // conditions if an upper one does not match.
-            $includeTreeTraverser->traverse($setupIncludeTree, $includeTreeTraverserVisitors);
-            $setupConditionList = $setupMatcherVisitor->getConditionListWithVerdicts();
-            $typoscriptCache?->set($setupConditionIncludeListCacheIdentifier, 'return unserialize(\'' . addcslashes(serialize($setupConditionIncludeListAccumulatorVisitor->getConditionIncludes()), '\'\\') . '\');');
-        }
-
-        // We now gathered everything to calculate the page cache identifier: It depends on sys_template rows, the calculated
-        // constant condition verdicts, the setup condition verdicts, plus various not TypoScript related details like
-        // obviously the page id.
-        $this->lock = GeneralUtility::makeInstance(ResourceMutex::class);
-        $this->newHash = $this->createHashBase($request, $sysTemplateRows, $constantConditionList, $setupConditionList);
-        if ($isCachingAllowed) {
-            if ($this->shouldAcquireCacheData($request)) {
-                // Try to get a page cache row.
-                $pageCacheRow = $this->pageCache->get($this->newHash);
-                if (!is_array($pageCacheRow)) {
-                    // Nothing in the cache, we acquire an exclusive lock now.
-                    // There are two scenarios when locking: We're either the first process acquiring this lock. This means we'll
-                    // "immediately" get it and can continue with page rendering. Or, another process acquired the lock already. In
-                    // this case, the below call will wait until the lock is released again. The other process then probably wrote
-                    // a page cache entry, which we can use.
-                    // To handle the second case - if our process had to wait for another one creating the content for us - we
-                    // simply query the page cache again to see if there is a page cache now.
-                    $hadToWaitForLock = $this->lock->acquireLock('pages', $this->newHash);
-                    // From this point on we're the only one working on that page.
-                    if ($hadToWaitForLock) {
-                        // Query the cache again to see if the data is there meanwhile: We did not get the lock
-                        // immediately, chances are high the other process created a page cache for us.
-                        // There is a small chance the other process actually pageCache->set() the content,
-                        // but pageCache->get() still returns false, for instance when a database returned "done"
-                        // for the INSERT, but SELECT still does not return the new row - may happen in multi-head
-                        // DB instances, and with some other distributed cache backends as well. The worst that
-                        // can happen here is the page generation is done too often, which we accept as trade-off.
-                        $pageCacheRow = $this->pageCache->get($this->newHash);
-                        if (is_array($pageCacheRow)) {
-                            // We have the content, some other process did the work for us, release our lock again.
-                            $this->releaseLocks();
-                        }
-                    }
-                    // We keep the lock set, because we are the ones generating the page now and filling the cache.
-                    // This indicates that we have to release the lock later in releaseLocks()!
-                }
-                if (is_array($pageCacheRow)) {
-                    $this->config['INTincScript'] = $pageCacheRow['INTincScript'];
-                    $this->config['INTincScript_ext'] = $pageCacheRow['INTincScript_ext'];
-                    $this->config['pageTitleCache'] = $pageCacheRow['pageTitleCache'];
-                    $this->content = $pageCacheRow['content'];
-                    $this->contentType = $pageCacheRow['contentType'];
-                    $this->cacheExpires = $pageCacheRow['expires'];
-                    $this->pageCacheTags = $pageCacheRow['cacheTags'];
-                    $this->cacheGenerated = $pageCacheRow['tstamp'];
-                    $this->pageContentWasLoadedFromCache = true;
-                }
-            } else {
-                // User forced page cache rebuilding. Get a lock for the page content so other processes can't interfere.
-                $this->lock->acquireLock('pages', $this->newHash);
-            }
-        } else {
-            // Caching is not allowed. We'll rebuild the page. Lock this.
-            $this->lock->acquireLock('pages', $this->newHash);
-        }
-
-        $setupRawConfigAst = null;
-        if (!$isCachingAllowed || !$this->pageContentWasLoadedFromCache || $this->isINTincScript()) {
-            // We don't need the full setup AST in many cached scenarios. However, if caching is not allowed, if no page
-            // cache entry could be loaded or if the page cache entry has _INT object, then we still need the full setup AST.
-            // If there is "just" an _INT object, we can use a possible cache entry for the setup AST, which speeds up _INT
-            // parsing quite a bit. In other cases we calculate full setup AST and cache it if allowed.
-            $setupTypoScriptCacheIdentifier = 'setup-' . sha1($serializedSysTemplateRows . $serializedConstantConditionList . serialize($setupConditionList));
-            $gotSetupFromCache = false;
-            if ($isCachingAllowed) {
-                // We need AST, but we are allowed to potentially get it from cache.
-                if ($setupTypoScriptCache = $typoscriptCache->require($setupTypoScriptCacheIdentifier)) {
-                    $frontendTypoScript->setSetupTree($setupTypoScriptCache['ast']);
-                    $frontendTypoScript->setSetupArray($setupTypoScriptCache['array']);
-                    $setupRawConfigAst = $setupTypoScriptCache['ast']->getChildByName('config');
-                    $gotSetupFromCache = true;
-                }
-            }
-            if (!$isCachingAllowed || !$gotSetupFromCache) {
-                // We need AST and couldn't get it from cache or are now allowed to. We thus need the full setup
-                // IncludeTree, which we can get from cache again if allowed, or is calculated a-new if not.
-                if (!$isCachingAllowed || $setupIncludeTree === null) {
-                    $setupIncludeTree = $treeBuilder->getTreeBySysTemplateRowsAndSite('setup', $sysTemplateRows, $tokenizer, $site, $typoscriptCache);
-                }
-                $includeTreeTraverserConditionVerdictAwareVisitors = [];
-                $setupConditionConstantSubstitutionVisitor = new IncludeTreeSetupConditionConstantSubstitutionVisitor();
-                $setupConditionConstantSubstitutionVisitor->setFlattenedConstants($flatConstants);
-                $includeTreeTraverserConditionVerdictAwareVisitors[] = $setupConditionConstantSubstitutionVisitor;
-                $setupMatcherVisitor = GeneralUtility::makeInstance(IncludeTreeConditionMatcherVisitor::class);
-                $setupMatcherVisitor->initializeExpressionMatcherWithVariables($expressionMatcherVariables);
-                $includeTreeTraverserConditionVerdictAwareVisitors[] = $setupMatcherVisitor;
-                $setupAstBuilderVisitor = GeneralUtility::makeInstance(IncludeTreeAstBuilderVisitor::class);
-                $setupAstBuilderVisitor->setFlatConstants($flatConstants);
-                $includeTreeTraverserConditionVerdictAwareVisitors[] = $setupAstBuilderVisitor;
-                $includeTreeTraverserConditionVerdictAware->traverse($setupIncludeTree, $includeTreeTraverserConditionVerdictAwareVisitors);
-                $setupAst = $setupAstBuilderVisitor->getAst();
-                // @todo: It would be good to actively remove 'config' from AST and array here
-                //        to prevent people from using the unmerged variant. The same
-                //        is already done for the determined PAGE 'config' below. This works, but
-                //        is currently blocked by functional tests that assert details?
-                $setupRawConfigAst = $setupAst->getChildByName('config');
-                // $setupAst->removeChildByName('config');
-                $frontendTypoScript->setSetupTree($setupAst);
-                $frontendTypoScript->setSetupArray($setupAst->toArray());
-
-                if ($isCachingAllowed) {
-                    // Write cache entry for AST and its array representation, we're allowed to do it.
-                    $typoscriptCache->set($setupTypoScriptCacheIdentifier, 'return unserialize(\'' . addcslashes(serialize(['ast' => $setupAst, 'array' => $setupAst->toArray()]), '\'\\') . '\');');
-                }
-            }
-        }
-
-        /** @var PageArguments $pageArguments */
-        $pageArguments = $request->getAttribute('routing');
-        $type = $pageArguments->getPageType();
-
-        $gotSetupConfigFromCache = false;
-        $setupConfigTypoScriptCacheIdentifier = 'setup-config-' . sha1($serializedSysTemplateRows . $serializedConstantConditionList . serialize($setupConditionList) . $type);
-        if ($isCachingAllowed) {
-            if ($setupConfigTypoScriptCache = $typoscriptCache->require($setupConfigTypoScriptCacheIdentifier)) {
-                $frontendTypoScript->setConfigTree($setupConfigTypoScriptCache['ast']);
-                $frontendTypoScript->setConfigArray($setupConfigTypoScriptCache['array']);
-                $gotSetupConfigFromCache = true;
-            }
-        }
-
-        if (!$isCachingAllowed || !$this->pageContentWasLoadedFromCache || !$gotSetupConfigFromCache || $this->isINTincScript()) {
-            $setupAst = $frontendTypoScript->getSetupTree();
-            $rawSetupPageNodeFromType = null;
-            foreach ($setupAst->getNextChild() as $potentialPageNode) {
-                if ($potentialPageNode->getValue() === 'PAGE') {
-                    // @todo: We could potentially remove *all* PAGE objects from setup here. This prevents people
-                    //        from accessing other ones than the determined one in $frontendTypoScript->getSetupArray().
-                    $typeNumChild = $potentialPageNode->getChildByName('typeNum');
-                    if ($typeNumChild && $type === $typeNumChild->getValue()) {
-                        $rawSetupPageNodeFromType = $potentialPageNode;
-                        break;
-                    }
-                    if (!$typeNumChild && $type === '0') {
-                        // The first PAGE node that has no typeNum is considered '0' automatically.
-                        $rawSetupPageNodeFromType = $potentialPageNode;
-                        break;
-                    }
-                }
-            }
-            if (!$rawSetupPageNodeFromType) {
-                $this->logger->error('No page configured for type={type}. There is no TypoScript object of type PAGE with typeNum={type}.', ['type' => $type]);
-                $response = GeneralUtility::makeInstance(ErrorController::class)->internalErrorAction(
-                    $request,
-                    'No page configured for type=' . $type . '.',
-                    ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED]
-                );
-                throw new PropagateResponseException($response, 1533931374);
-            }
-            $setupPageAst = new RootNode();
-            foreach ($rawSetupPageNodeFromType->getNextChild() as $child) {
-                $setupPageAst->addChild($child);
-            }
-            if (!$gotSetupConfigFromCache) {
-                $mergedSetupConfigAst = (new SetupConfigMerger())->merge($setupRawConfigAst, $setupPageAst->getChildByName('config'));
-
-                if ($mergedSetupConfigAst->getChildByName('absRefPrefix') === null) {
-                    // Make sure config.absRefPrefix is set, fallback to 'auto'.
-                    $absRefPrefixNode = new ChildNode('absRefPrefix');
-                    $absRefPrefixNode->setValue('auto');
-                    $mergedSetupConfigAst->addChild($absRefPrefixNode);
-                }
-                if ($mergedSetupConfigAst->getChildByName('doctype') === null) {
-                    // Make sure config.doctype is set, fallback to 'html5'.
-                    $doctypeNode = new ChildNode('doctype');
-                    $doctypeNode->setValue('html5');
-                    $mergedSetupConfigAst->addChild($doctypeNode);
-                }
-                $mergedSetupConfigAst = GeneralUtility::makeInstance(EventDispatcherInterface::class)
-                    ->dispatch(new ModifyTypoScriptConfigEvent($request, $setupAst, $mergedSetupConfigAst))->getConfigTree();
-                $frontendTypoScript->setConfigTree($mergedSetupConfigAst);
-                $setupConfigArray = $mergedSetupConfigAst->toArray();
-                $frontendTypoScript->setConfigArray($setupConfigArray);
-                if ($isCachingAllowed) {
-                    $typoscriptCache->set($setupConfigTypoScriptCacheIdentifier, 'return unserialize(\'' . addcslashes(serialize(['ast' => $mergedSetupConfigAst, 'array' => $setupConfigArray]), '\'\\') . '\');');
-                }
-            }
-            if (!$isCachingAllowed || !$this->pageContentWasLoadedFromCache || $this->isINTincScript()) {
-                // Remove "page.config" to prevent people from working with the not merged variant.
-                $setupPageAst->removeChildByName('config');
-                $frontendTypoScript->setPageTree($setupPageAst);
-                $frontendTypoScript->setPageArray($setupPageAst->toArray());
-            }
-        }
-
-        $setupConfigAst = $frontendTypoScript->getConfigTree();
-
-        if ($this->pageContentWasLoadedFromCache
-            && ($setupConfigAst->getChildByName('debug')?->getValue() || !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']))
-        ) {
-            // Prepare X-TYPO3-Debug-Cache HTTP header
-            $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'];
-            $timeFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
-            $this->debugInformationHeader = 'Cached page generated ' . date($dateFormat . ' ' . $timeFormat, $this->cacheGenerated)
-                . '. Expires ' . date($dateFormat . ' ' . $timeFormat, $this->cacheExpires);
-        }
-
-        if ($setupConfigAst->getChildByName('no_cache')?->getValue()) {
-            // Disable cache if config.no_cache is set!
-            $cacheInstruction = $request->getAttribute('frontend.cache.instruction');
-            $cacheInstruction->disableCache('EXT:frontend: Disabled cache due to TypoScript "config.no_cache = 1"');
-        }
-
-        return $frontendTypoScript;
-    }
-
-    /**
-     * Detecting if shift-reload has been clicked.
-     * This option will have no effect if re-generation of page happens by other reasons (for instance that the page is not in cache yet).
-     * Also, a backend user MUST be logged in for the shift-reload to be detected due to DoS-attack-security reasons.
-     *
-     * @return bool If shift-reload in client browser has been clicked, disable getting cached page and regenerate the page content.
-     */
-    protected function shouldAcquireCacheData(ServerRequestInterface $request): bool
-    {
-        // Trigger event for possible by-pass of requiring of page cache.
-        $event = new ShouldUseCachedPageDataIfAvailableEvent($request, $this, $request->getAttribute('frontend.cache.instruction')->isCachingAllowed());
-        GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch($event);
-        return $event->shouldUseCachedPageData();
-    }
-
-    /**
-     * This creates a hash used as page cache entry identifier and as page generation lock.
-     * When multiple requests try to render the same page that will result in the same page cache entry,
-     * this lock allows creation by one request which typically puts the result into page cache, while
-     * the other requests wait until this finished and re-use the result.
-     *
-     * This hash is unique to the TS template and constant and setup condition verdict,
-     * the variables ->id, ->type, list of frontend user groups, mount points and cHash array.
-     *
-     * @return string Page cache entry identifier also used as page generation lock
-     */
-    protected function createHashBase(ServerRequestInterface $request, array $sysTemplateRows, array $constantConditionList, array $setupConditionList): string
-    {
-        $pageInformation = $request->getAttribute('frontend.page.information');
-        $pageId = $pageInformation->getId();
-        $site = $request->getAttribute('site');
-        $pageArguments = $request->getAttribute('routing');
-        $pageCacheIdentifierParameters = [
-            'id' => $pageId,
-            'type' => $pageArguments->getPageType(),
-            'groupIds' => implode(',', $this->context->getAspect('frontend.user')->getGroupIds()),
-            'MP' => $pageInformation->getMountPoint(),
-            'site' => $site->getIdentifier(),
-            // Ensure the language base is used for the hash base calculation as well, otherwise TypoScript and page-related rendering
-            // is not cached properly as we don't have any language-specific conditions anymore
-            'siteBase' => (string)$request->getAttribute('language', $site->getDefaultLanguage())->getBase(),
-            // additional variation trigger for static routes
-            'staticRouteArguments' => $pageArguments->getStaticArguments(),
-            // dynamic route arguments (if route was resolved)
-            'dynamicArguments' => $this->getRelevantParametersForCachingFromPageArguments($pageArguments),
-            'sysTemplateRows' => $sysTemplateRows,
-            'constantConditionList' => $constantConditionList,
-            'setupConditionList' => $setupConditionList,
-        ];
-        $pageCacheIdentifierParameters = GeneralUtility::makeInstance(EventDispatcherInterface::class)
-            ->dispatch(new BeforePageCacheIdentifierIsHashedEvent($request, $pageCacheIdentifierParameters))
-            ->getPageCacheIdentifierParameters();
-        return $pageId . '_' . sha1(serialize($pageCacheIdentifierParameters));
-    }
-
     /**
      * Returns TRUE if the page content should be generated.
      *
@@ -1467,15 +997,12 @@ class TypoScriptFrontendController implements LoggerAwareInterface
     }
 
     /**
-     * Release the page specific lock.
-     *
-     * @throws \InvalidArgumentException
-     * @throws \RuntimeException
      * @internal
+     * @todo: Remove when not called in TF anymore.
      */
     public function releaseLocks(): void
     {
-        $this->lock?->releaseLock('pages');
+        // noop
     }
 
     /**
diff --git a/typo3/sysext/frontend/Classes/Event/ShouldUseCachedPageDataIfAvailableEvent.php b/typo3/sysext/frontend/Classes/Event/ShouldUseCachedPageDataIfAvailableEvent.php
index b5602b3d7f2d..6b31e7e790f2 100644
--- a/typo3/sysext/frontend/Classes/Event/ShouldUseCachedPageDataIfAvailableEvent.php
+++ b/typo3/sysext/frontend/Classes/Event/ShouldUseCachedPageDataIfAvailableEvent.php
@@ -28,13 +28,15 @@ final class ShouldUseCachedPageDataIfAvailableEvent
 {
     public function __construct(
         private readonly ServerRequestInterface $request,
-        private readonly TypoScriptFrontendController $controller,
         private bool $shouldUseCachedPageData
     ) {}
 
+    /**
+     * @todo: deprecate
+     */
     public function getController(): TypoScriptFrontendController
     {
-        return $this->controller;
+        return $this->request->getAttribute('frontend.controller');
     }
 
     public function getRequest(): ServerRequestInterface
diff --git a/typo3/sysext/frontend/Classes/Http/RequestHandler.php b/typo3/sysext/frontend/Classes/Http/RequestHandler.php
index 397cf9cb27d7..3d9379501625 100644
--- a/typo3/sysext/frontend/Classes/Http/RequestHandler.php
+++ b/typo3/sysext/frontend/Classes/Http/RequestHandler.php
@@ -158,7 +158,6 @@ class RequestHandler implements RequestHandlerInterface
             $controller->generatePage_postProcessing($request);
             $this->timeTracker->pull();
         }
-        $controller->releaseLocks();
 
         // Render non-cached page parts by replacing placeholders which are taken from cache or added during page generation
         if ($controller->isINTincScript()) {
diff --git a/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php b/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php
index fd654d806e82..636de88afdfc 100644
--- a/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php
+++ b/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php
@@ -22,56 +22,229 @@ use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\MiddlewareInterface;
 use Psr\Http\Server\RequestHandlerInterface;
-use TYPO3\CMS\Core\TimeTracker\TimeTracker;
+use Psr\Log\LoggerInterface;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
+use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
+use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Locking\ResourceMutex;
+use TYPO3\CMS\Core\TypoScript\FrontendTypoScript;
+use TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\HttpUtility;
+use TYPO3\CMS\Frontend\Controller\ErrorController;
 use TYPO3\CMS\Frontend\Event\AfterTypoScriptDeterminedEvent;
+use TYPO3\CMS\Frontend\Event\BeforePageCacheIdentifierIsHashedEvent;
+use TYPO3\CMS\Frontend\Event\ShouldUseCachedPageDataIfAvailableEvent;
+use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
+use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
 
 /**
- * Initialization of TypoScriptFrontendController
- *
- * Do all necessary preparation steps for rendering
+ * Initialize TypoScript, get page content from cache if possible, lock
+ * rendering if needed and create more TypoScript data if needed.
  *
  * @internal this middleware might get removed later.
  */
-final class PrepareTypoScriptFrontendRendering implements MiddlewareInterface
+final readonly class PrepareTypoScriptFrontendRendering implements MiddlewareInterface
 {
     public function __construct(
-        private readonly EventDispatcherInterface $eventDispatcher,
-        private readonly TimeTracker $timeTracker
+        private EventDispatcherInterface $eventDispatcher,
+        private FrontendTypoScriptFactory $frontendTypoScriptFactory,
+        private PhpFrontend $typoScriptCache,
+        private FrontendInterface $pageCache,
+        private ResourceMutex $lock,
+        private Context $context,
+        private LoggerInterface $logger,
+        private ErrorController $errorController,
     ) {}
 
-    /**
-     * Initialize TypoScriptFrontendController to the point right before rendering of the page is triggered
-     */
     public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
     {
-        $controller = $request->getAttribute('frontend.controller');
+        $site = $request->getAttribute('site');
+        $sysTemplateRows = $request->getAttribute('frontend.page.information')->getSysTemplateRows();
+        $isCachingAllowed = $request->getAttribute('frontend.cache.instruction')->isCachingAllowed();
+
+        // Create FrontendTypoScript with essential info for page cache identifier
+        $conditionMatcherVariables = $this->prepareConditionMatcherVariables($request);
+        $frontendTypoScript = $this->frontendTypoScriptFactory->createSettingsAndSetupConditions(
+            $site,
+            $sysTemplateRows,
+            $conditionMatcherVariables,
+            $isCachingAllowed ? $this->typoScriptCache : null,
+        );
 
-        // as long as TSFE throws errors with the global object, this needs to be set, but
-        // should be removed later-on once TypoScript Condition Matcher is built with the current request object.
-        $GLOBALS['TYPO3_REQUEST'] = $request;
+        $isUsingPageCacheAllowed = $this->eventDispatcher
+            ->dispatch(new ShouldUseCachedPageDataIfAvailableEvent($request, $isCachingAllowed))
+            ->shouldUseCachedPageData();
+        $pageCacheIdentifier = $this->createPageCacheIdentifier($request, $frontendTypoScript);
 
-        $this->timeTracker->push('Get Page from cache');
-        // Get from cache. Locks may be acquired here. After this, we should have a valid config-array ready.
-        $frontendTypoScript = $controller->getFromCache($request);
-        $this->eventDispatcher->dispatch(new AfterTypoScriptDeterminedEvent($frontendTypoScript));
-        $request = $request->withAttribute('frontend.typoscript', $frontendTypoScript);
-        $this->timeTracker->pull();
+        $pageCacheRow = null;
+        if (!$isUsingPageCacheAllowed) {
+            // Caching is not allowed. We'll rebuild the page. Lock this.
+            $this->lock->acquireLock('pages', $pageCacheIdentifier);
+        } else {
+            // Try to get a page cache row.
+            $pageCacheRow = $this->pageCache->get($pageCacheIdentifier);
+            if (!is_array($pageCacheRow)) {
+                // Nothing in the cache, we acquire an exclusive lock now.
+                // There are two scenarios when locking: We're either the first process acquiring this lock. This means we'll
+                // "immediately" get it and can continue with page rendering. Or, another process acquired the lock already. In
+                // this case, the below call will wait until the lock is released again. The other process then probably wrote
+                // a page cache entry, which we can use.
+                // To handle the second case - if our process had to wait for another one creating the content for us - we
+                // simply query the page cache again to see if there is a page cache now.
+                $hadToWaitForLock = $this->lock->acquireLock('pages', $pageCacheIdentifier);
+                // From this point on we're the only one working on that page.
+                if ($hadToWaitForLock) {
+                    // Query the cache again to see if the data is there meanwhile: We did not get the lock
+                    // immediately, chances are high the other process created a page cache for us.
+                    // There is a small chance the other process actually pageCache->set() the content,
+                    // but pageCache->get() still returns false, for instance when a database returned "done"
+                    // for the INSERT, but SELECT still does not return the new row - may happen in multi-head
+                    // DB instances, and with some other distributed cache backends as well. The worst that
+                    // can happen here is the page generation is done too often, which we accept as trade-off.
+                    $pageCacheRow = $this->pageCache->get($pageCacheIdentifier);
+                    if (is_array($pageCacheRow)) {
+                        // We have the content, some other process did the work for us, release our lock again.
+                        $this->lock->releaseLock('pages');
+                    }
+                }
+                // Keep the lock set, because we are the ones generating the page now and filling the cache.
+            }
+        }
 
-        // b/w compat
-        $controller->config['config'] = $request->getAttribute('frontend.typoscript')->getConfigArray();
-        // Set new request which now has the frontend.typoscript attribute
-        $GLOBALS['TYPO3_REQUEST'] = $request;
+        $controller = $request->getAttribute('frontend.controller');
+        $controller->newHash = $pageCacheIdentifier;
+        $pageContentWasLoadedFromCache = false;
+        if (is_array($pageCacheRow)) {
+            $controller->config['INTincScript'] = $pageCacheRow['INTincScript'];
+            $controller->config['INTincScript_ext'] = $pageCacheRow['INTincScript_ext'];
+            $controller->config['pageTitleCache'] = $pageCacheRow['pageTitleCache'];
+            $controller->content = $pageCacheRow['content'];
+            $controller->setContentType($pageCacheRow['contentType']);
+            $controller->cacheExpires = $pageCacheRow['expires'];
+            $controller->pageCacheTags = $pageCacheRow['cacheTags'];
+            $controller->cacheGenerated = $pageCacheRow['tstamp'];
+            $controller->pageContentWasLoadedFromCache = true;
+            $pageContentWasLoadedFromCache = true;
+        }
+
+        try {
+            $needsFullSetup = !$pageContentWasLoadedFromCache || $controller->isINTincScript();
+            $pageType = $request->getAttribute('routing')->getPageType();
+            $frontendTypoScript = $this->frontendTypoScriptFactory->createSetupConfigOrFullSetup(
+                $needsFullSetup,
+                $frontendTypoScript,
+                $site,
+                $sysTemplateRows,
+                $conditionMatcherVariables,
+                $pageType,
+                $isCachingAllowed ? $this->typoScriptCache : null,
+                $request,
+            );
+            if ($needsFullSetup && !$frontendTypoScript->hasPage()) {
+                $this->logger->error('No page configured for type={type}. There is no TypoScript object of type PAGE with typeNum={type}.', ['type' => $pageType]);
+                return $this->errorController->internalErrorAction(
+                    $request,
+                    'No page configured for type=' . $pageType . '.',
+                    ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED]
+                );
+            }
+            $setupConfigAst = $frontendTypoScript->getConfigTree();
+            if ($pageContentWasLoadedFromCache && ($setupConfigAst->getChildByName('debug')?->getValue() || !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']))) {
+                // Prepare X-TYPO3-Debug-Cache HTTP header
+                $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'];
+                $timeFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
+                $controller->debugInformationHeader = 'Cached page generated ' . date($dateFormat . ' ' . $timeFormat, $controller->cacheGenerated)
+                    . '. Expires ' . date($dateFormat . ' ' . $timeFormat, $controller->cacheExpires);
+            }
+            if ($setupConfigAst->getChildByName('no_cache')?->getValue()) {
+                // Disable cache if config.no_cache is set!
+                $cacheInstruction = $request->getAttribute('frontend.cache.instruction');
+                $cacheInstruction->disableCache('EXT:frontend: Disabled cache due to TypoScript "config.no_cache = 1"');
+            }
+            $this->eventDispatcher->dispatch(new AfterTypoScriptDeterminedEvent($frontendTypoScript));
+            $request = $request->withAttribute('frontend.typoscript', $frontendTypoScript);
 
-        $response = $handler->handle($request);
+            // b/w compat
+            $controller->config['config'] = $frontendTypoScript->getConfigArray();
+            $GLOBALS['TYPO3_REQUEST'] = $request;
 
-        /**
-         * Release TSFE locks. They have been acquired in the above call to controller->getFromCache().
-         * TSFE locks are usually released by the RequestHandler 'final' middleware.
-         * However, when some middlewares returns early (e.g. Shortcut and MountPointRedirect,
-         * which both skip inner middlewares), or due to Exceptions, locks still need to be released explicitly.
-         */
-        $controller->releaseLocks();
+            $response = $handler->handle($request);
+        } finally {
+            // Whatever happens in a below middleware, this finally is called, even when exceptions
+            // are raised by a lower middleware. This ensures locks are released no matter what.
+            $this->lock->releaseLock('pages');
+        }
 
         return $response;
     }
+
+    /**
+     * Data available in TypoScript "condition" matching.
+     */
+    private function prepareConditionMatcherVariables(ServerRequestInterface $request): array
+    {
+        $pageInformation = $request->getAttribute('frontend.page.information');
+        $topDownRootLine = $pageInformation->getRootLine();
+        $localRootline = $pageInformation->getLocalRootLine();
+        ksort($topDownRootLine);
+        return [
+            'request' => $request,
+            'pageId' => $pageInformation->getId(),
+            'page' => $pageInformation->getPageRecord(),
+            'fullRootLine' => $topDownRootLine,
+            'localRootLine' => $localRootline,
+            'site' => $request->getAttribute('site'),
+            'siteLanguage' => $request->getAttribute('language'),
+            'tsfe' => $request->getAttribute('frontend.controller'),
+        ];
+    }
+
+    /**
+     * This creates a hash used as page cache entry identifier and as page generation lock.
+     * When multiple requests try to render the same page that will result in the same page cache entry,
+     * this lock allows creation by one request which typically puts the result into page cache, while
+     * the other requests wait until this finished and re-use the result.
+     */
+    private function createPageCacheIdentifier(ServerRequestInterface $request, FrontendTypoScript $frontendTypoScript): string
+    {
+        $pageInformation = $request->getAttribute('frontend.page.information');
+        $pageId = $pageInformation->getId();
+        $pageArguments = $request->getAttribute('routing');
+        $site = $request->getAttribute('site');
+
+        $dynamicArguments = [];
+        $queryParams = $pageArguments->getDynamicArguments();
+        if (!empty($queryParams) && ($pageArguments->getArguments()['cHash'] ?? false)) {
+            // Fetch arguments relevant for creating the page cache identifier from the PageArguments object.
+            // Excluded parameters are not taken into account when calculating the hash base.
+            $queryParams['id'] = $pageArguments->getPageId();
+            // @todo: Make CacheHashCalculator and CacheHashConfiguration stateless and get it injected.
+            $dynamicArguments = GeneralUtility::makeInstance(CacheHashCalculator::class)
+                ->getRelevantParameters(HttpUtility::buildQueryString($queryParams));
+        }
+
+        $pageCacheIdentifierParameters = [
+            'id' => $pageId,
+            'type' => $pageArguments->getPageType(),
+            'groupIds' => implode(',', $this->context->getAspect('frontend.user')->getGroupIds()),
+            'MP' => $pageInformation->getMountPoint(),
+            'site' => $site->getIdentifier(),
+            // Ensure the language base is used for the hash base calculation as well, otherwise TypoScript and page-related rendering
+            // is not cached properly as we don't have any language-specific conditions anymore
+            'siteBase' => (string)$request->getAttribute('language', $site->getDefaultLanguage())->getBase(),
+            // additional variation trigger for static routes
+            'staticRouteArguments' => $pageArguments->getStaticArguments(),
+            // dynamic route arguments (if route was resolved)
+            'dynamicArguments' => $dynamicArguments,
+            'sysTemplateRows' => $pageInformation->getSysTemplateRows(),
+            'constantConditionList' => $frontendTypoScript->getSettingsConditionList(),
+            'setupConditionList' => $frontendTypoScript->getSetupConditionList(),
+        ];
+        $pageCacheIdentifierParameters = $this->eventDispatcher
+            ->dispatch(new BeforePageCacheIdentifierIsHashedEvent($request, $pageCacheIdentifierParameters))
+            ->getPageCacheIdentifierParameters();
+
+        return $pageId . '_' . hash('xxh3', serialize($pageCacheIdentifierParameters));
+    }
 }
diff --git a/typo3/sysext/frontend/Configuration/Services.yaml b/typo3/sysext/frontend/Configuration/Services.yaml
index c8ee38a0eedd..52305edbb6a7 100644
--- a/typo3/sysext/frontend/Configuration/Services.yaml
+++ b/typo3/sysext/frontend/Configuration/Services.yaml
@@ -15,6 +15,11 @@ services:
     arguments:
       $cache: '@cache.assets'
 
+  TYPO3\CMS\Frontend\Middleware\PrepareTypoScriptFrontendRendering:
+    arguments:
+      $pageCache: '@cache.pages'
+      $typoScriptCache: '@cache.typoscript'
+
   TYPO3\CMS\Frontend\ContentObject\ContentDataProcessor:
     public: true
 
diff --git a/typo3/sysext/frontend/Tests/Functional/ContentObject/ContentObjectRendererTest.php b/typo3/sysext/frontend/Tests/Functional/ContentObject/ContentObjectRendererTest.php
index 22c1db10780b..14549e716d7a 100644
--- a/typo3/sysext/frontend/Tests/Functional/ContentObject/ContentObjectRendererTest.php
+++ b/typo3/sysext/frontend/Tests/Functional/ContentObject/ContentObjectRendererTest.php
@@ -639,7 +639,7 @@ final class ContentObjectRendererTest extends FunctionalTestCase
     #[Test]
     public function typolinkReturnsCorrectLinkForSpamEncryptedEmails(array $tsfeConfig, string $linkText, string $parameter, string $expected): void
     {
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setConfigArray($tsfeConfig);
         $request = (new ServerRequest())->withAttribute('frontend.typoscript', $frontendTypoScript);
         $subject = new ContentObjectRenderer();
@@ -874,7 +874,7 @@ final class ContentObjectRendererTest extends FunctionalTestCase
     {
         $tsfe = $this->getMockBuilder(TypoScriptFrontendController::class)->disableOriginalConstructor()->getMock();
         $subject = new ContentObjectRenderer($tsfe);
-        $typoScript = new FrontendTypoScript(new RootNode(), []);
+        $typoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $typoScript->setConfigArray([]);
         $request = $this->getPreparedRequest()->withAttribute('frontend.typoscript', $typoScript);
         $subject->setRequest($request);
@@ -1163,7 +1163,7 @@ And another one';
         $typoScriptFrontendController = GeneralUtility::makeInstance(TypoScriptFrontendController::class);
         $subject = GeneralUtility::makeInstance(ContentObjectRenderer::class, $typoScriptFrontendController);
         $subject->setCurrentFile($fileReference);
-        $typoScript = new FrontendTypoScript(new RootNode(), []);
+        $typoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $typoScript->setConfigArray([]);
         $request = $this->getPreparedRequest()->withAttribute('frontend.typoscript', $typoScript);
         $subject->setRequest($request);
diff --git a/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php b/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php
index 343b2f4b8b1d..745fe16cc51f 100644
--- a/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php
+++ b/typo3/sysext/frontend/Tests/Unit/ContentObject/ContentObjectRendererTest.php
@@ -1885,7 +1885,7 @@ final class ContentObjectRendererTest extends UnitTestCase
     {
         $this->expectException(\LogicException::class);
         $this->expectExceptionCode(1414513947);
-        $typoScript = new FrontendTypoScript(new RootNode(), []);
+        $typoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $typoScript->setConfigArray([]);
         $request = (new ServerRequest())->withAttribute('frontend.typoscript', $typoScript);
         $this->subject->setRequest($request);
@@ -1907,7 +1907,7 @@ final class ContentObjectRendererTest extends UnitTestCase
             Environment::getPublicPath() . '/index.php',
             Environment::isWindows() ? 'WINDOWS' : 'UNIX'
         );
-        $typoScript = new FrontendTypoScript(new RootNode(), []);
+        $typoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $typoScript->setConfigArray([]);
         $request = (new ServerRequest())->withAttribute('frontend.typoscript', $typoScript);
         $this->subject->setRequest($request);
@@ -1922,7 +1922,7 @@ final class ContentObjectRendererTest extends UnitTestCase
         $configuration = [
             'exceptionHandler' => '1',
         ];
-        $typoScript = new FrontendTypoScript(new RootNode(), []);
+        $typoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $typoScript->setConfigArray([]);
         $request = (new ServerRequest())->withAttribute('frontend.typoscript', $typoScript);
         $this->subject->setRequest($request);
@@ -1933,7 +1933,7 @@ final class ContentObjectRendererTest extends UnitTestCase
     public function renderingContentObjectDoesNotThrowExceptionIfExceptionHandlerIsConfiguredGlobally(): void
     {
         $contentObjectFixture = $this->createContentObjectThrowingExceptionFixture($this->subject);
-        $typoScript = new FrontendTypoScript(new RootNode(), []);
+        $typoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $typoScript->setConfigArray([
             'contentObjectExceptionHandler' => '1',
         ]);
@@ -1948,7 +1948,7 @@ final class ContentObjectRendererTest extends UnitTestCase
         $this->expectException(\LogicException::class);
         $this->expectExceptionCode(1414513947);
         $contentObjectFixture = $this->createContentObjectThrowingExceptionFixture($this->subject, false);
-        $typoScript = new FrontendTypoScript(new RootNode(), []);
+        $typoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $typoScript->setConfigArray([
             'contentObjectExceptionHandler' => '1',
         ]);
@@ -1964,7 +1964,7 @@ final class ContentObjectRendererTest extends UnitTestCase
     public function renderedErrorMessageCanBeCustomized(): void
     {
         $contentObjectFixture = $this->createContentObjectThrowingExceptionFixture($this->subject);
-        $typoScript = new FrontendTypoScript(new RootNode(), []);
+        $typoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $typoScript->setConfigArray([]);
         $request = (new ServerRequest())->withAttribute('frontend.typoscript', $typoScript);
         $this->subject->setRequest($request);
@@ -1981,7 +1981,7 @@ final class ContentObjectRendererTest extends UnitTestCase
     public function localConfigurationOverridesGlobalConfiguration(): void
     {
         $contentObjectFixture = $this->createContentObjectThrowingExceptionFixture($this->subject);
-        $typoScript = new FrontendTypoScript(new RootNode(), []);
+        $typoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $typoScript->setConfigArray([
             'contentObjectExceptionHandler.' => [
                 'errorMessage' => 'Global message for testing',
@@ -2011,7 +2011,7 @@ final class ContentObjectRendererTest extends UnitTestCase
                 'ignoreCodes.' => ['10.' => '1414513947'],
             ],
         ];
-        $typoScript = new FrontendTypoScript(new RootNode(), []);
+        $typoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $typoScript->setConfigArray([]);
         $request = (new ServerRequest())->withAttribute('frontend.typoscript', $typoScript);
         $this->subject->setRequest($request);
@@ -2093,7 +2093,7 @@ final class ContentObjectRendererTest extends UnitTestCase
             $linkFactory->method('create')->willReturn($linkResult);
             GeneralUtility::addInstance(LinkFactory::class, $linkFactory);
         }
-        $typoScript = new FrontendTypoScript(new RootNode(), []);
+        $typoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $typoScript->setConfigArray([]);
         $request = (new ServerRequest())->withAttribute('frontend.typoscript', $typoScript);
         $this->subject->setRequest($request);
@@ -2365,7 +2365,7 @@ final class ContentObjectRendererTest extends UnitTestCase
     #[Test]
     public function parseFuncParsesNestedTagsProperly(string $value, array $configuration, string $expectedResult): void
     {
-        $typoScript = new FrontendTypoScript(new RootNode(), []);
+        $typoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $typoScript->setConfigArray([]);
         $request = (new ServerRequest())->withAttribute('frontend.typoscript', $typoScript);
         $this->subject->setRequest($request);
@@ -5933,7 +5933,7 @@ final class ContentObjectRendererTest extends UnitTestCase
         int $times,
         string $will
     ): void {
-        $typoScript = new FrontendTypoScript(new RootNode(), []);
+        $typoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $typoScript->setConfigArray([
             'disablePrefixComment' => $disable,
         ]);
@@ -7327,7 +7327,7 @@ final class ContentObjectRendererTest extends UnitTestCase
             "lib.bar.value = bar\n";
         $lineStream = (new LossyTokenizer())->tokenize($typoScriptString);
         $typoScriptAst = (new AstBuilder(new NoopEventDispatcher()))->build($lineStream, new RootNode());
-        $typoScriptAttribute = new FrontendTypoScript(new RootNode(), []);
+        $typoScriptAttribute = new FrontendTypoScript(new RootNode(), [], [], []);
         $typoScriptAttribute->setSetupTree($typoScriptAst);
         $typoScriptAttribute->setSetupArray($typoScriptAst->toArray());
         $request = (new ServerRequest())->withAttribute('frontend.typoscript', $typoScriptAttribute);
diff --git a/typo3/sysext/frontend/Tests/Unit/Http/RequestHandlerTest.php b/typo3/sysext/frontend/Tests/Unit/Http/RequestHandlerTest.php
index b1f183b89b2c..62c6d1ce234a 100644
--- a/typo3/sysext/frontend/Tests/Unit/Http/RequestHandlerTest.php
+++ b/typo3/sysext/frontend/Tests/Unit/Http/RequestHandlerTest.php
@@ -134,7 +134,7 @@ final class RequestHandlerTest extends UnitTestCase
 
         $pageRendererMock = $this->getMockBuilder(PageRenderer::class)->disableOriginalConstructor()->getMock();
         $pageRendererMock->method('getDocType')->willReturn(DocType::html5);
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $frontendTypoScript->setPageArray([
             'meta.' => [
@@ -295,7 +295,7 @@ final class RequestHandlerTest extends UnitTestCase
         $pageRendererMock = $this->getMockBuilder(PageRenderer::class)->disableOriginalConstructor()->getMock();
         $pageRendererMock->method('getDocType')->willReturn(DocType::html5);
         $pageRendererMock->expects(self::once())->method('setMetaTag')->with($expectedTags['type'], $expectedTags['name'], $expectedTags['content'], [], false);
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $frontendTypoScript->setPageArray([
             'meta.' => $typoScript,
@@ -342,7 +342,7 @@ final class RequestHandlerTest extends UnitTestCase
         $pageRendererMock = $this->getMockBuilder(PageRenderer::class)->disableOriginalConstructor()->getMock();
         $pageRendererMock->method('getDocType')->willReturn(DocType::html5);
         $pageRendererMock->expects(self::never())->method('setMetaTag');
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $frontendTypoScript->setPageArray([
             'meta.' => [
@@ -459,7 +459,7 @@ final class RequestHandlerTest extends UnitTestCase
                 self::assertSame($expectedArgs[3], $subProperties);
                 self::assertSame($expectedArgs[4], $replace);
             });
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray([]);
         $frontendTypoScript->setPageArray([
             'meta.' => $typoScript,
diff --git a/typo3/sysext/frontend/Tests/Unit/Typolink/DatabaseRecordLinkBuilderTest.php b/typo3/sysext/frontend/Tests/Unit/Typolink/DatabaseRecordLinkBuilderTest.php
index e681c8a246d5..a116d585d39a 100644
--- a/typo3/sysext/frontend/Tests/Unit/Typolink/DatabaseRecordLinkBuilderTest.php
+++ b/typo3/sysext/frontend/Tests/Unit/Typolink/DatabaseRecordLinkBuilderTest.php
@@ -157,7 +157,7 @@ final class DatabaseRecordLinkBuilderTest extends UnitTestCase
         $frontendControllerMock = $this->createMock(TypoScriptFrontendController::class);
         $pageRepositoryMock = $this->createMock(PageRepository::class);
         $contentObjectRendererMock = $this->createMock(ContentObjectRenderer::class);
-        $frontendTypoScript = new FrontendTypoScript(new RootNode(), []);
+        $frontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $frontendTypoScript->setSetupArray($typoScriptConfig);
         $request = (new ServerRequest())->withAttribute('frontend.typoscript', $frontendTypoScript);
         $contentObjectRendererMock->method('getRequest')->willReturn($request);
diff --git a/typo3/sysext/indexed_search/Tests/Unit/IndexerTest.php b/typo3/sysext/indexed_search/Tests/Unit/IndexerTest.php
index 34c24d924f84..1225fb2134ba 100644
--- a/typo3/sysext/indexed_search/Tests/Unit/IndexerTest.php
+++ b/typo3/sysext/indexed_search/Tests/Unit/IndexerTest.php
@@ -76,7 +76,7 @@ final class IndexerTest extends UnitTestCase
     {
         $absRefPrefix = '/' . StringUtility::getUniqueId();
         $html = 'test <a href="' . $absRefPrefix . 'index.php">test</a> test';
-        $typoScript = new FrontendTypoScript(new RootNode(), []);
+        $typoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $typoScript->setConfigArray([
             'absRefPrefix' => $absRefPrefix,
         ]);
diff --git a/typo3/sysext/redirects/Classes/Service/RedirectService.php b/typo3/sysext/redirects/Classes/Service/RedirectService.php
index 88de1806ce0d..bb431a4545af 100644
--- a/typo3/sysext/redirects/Classes/Service/RedirectService.php
+++ b/typo3/sysext/redirects/Classes/Service/RedirectService.php
@@ -20,8 +20,8 @@ namespace TYPO3\CMS\Redirects\Service;
 use Psr\EventDispatcher\EventDispatcherInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Message\UriInterface;
-use Psr\Log\LoggerAwareInterface;
-use Psr\Log\LoggerAwareTrait;
+use Psr\Log\LoggerInterface;
+use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
 use TYPO3\CMS\Core\Http\Uri;
@@ -34,6 +34,7 @@ use TYPO3\CMS\Core\Routing\PageArguments;
 use TYPO3\CMS\Core\Site\Entity\NullSite;
 use TYPO3\CMS\Core\Site\Entity\SiteInterface;
 use TYPO3\CMS\Core\Site\SiteFinder;
+use TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Frontend\Aspect\PreviewAspect;
@@ -47,18 +48,19 @@ use TYPO3\CMS\Redirects\Event\BeforeRedirectMatchDomainEvent;
 /**
  * Creates a proper URL to redirect from a matched redirect of a request
  *
- * @internal due to some possible refactorings in TYPO3 v9
+ * @internal due to some possible refactorings
  */
-class RedirectService implements LoggerAwareInterface
+class RedirectService
 {
-    use LoggerAwareTrait;
-
     public function __construct(
         private readonly RedirectCacheService $redirectCacheService,
         private readonly LinkService $linkService,
         private readonly SiteFinder $siteFinder,
         private readonly EventDispatcherInterface $eventDispatcher,
         private readonly PageInformationFactory $pageInformationFactory,
+        private readonly FrontendTypoScriptFactory $frontendTypoScriptFactory,
+        private readonly PhpFrontend $typoScriptCache,
+        private readonly LoggerInterface $logger,
     ) {}
 
     /**
@@ -402,9 +404,25 @@ class RedirectService implements LoggerAwareInterface
         $originalRequest = $originalRequest->withAttribute('frontend.page.information', $pageInformation);
         $controller = GeneralUtility::makeInstance(TypoScriptFrontendController::class);
         $controller->initializePageRenderer($originalRequest);
-        $frontendTypoScript = $controller->getFromCache($originalRequest);
+        $expressionMatcherVariables = $this->getExpressionMatcherVariables($site, $originalRequest, $controller);
+        $frontendTypoScript = $this->frontendTypoScriptFactory->createSettingsAndSetupConditions(
+            $site,
+            $pageInformation->getSysTemplateRows(),
+            // $originalRequest does not contain site ...
+            $expressionMatcherVariables,
+            $this->typoScriptCache,
+        );
+        $frontendTypoScript = $this->frontendTypoScriptFactory->createSetupConfigOrFullSetup(
+            false,
+            $frontendTypoScript,
+            $site,
+            $pageInformation->getSysTemplateRows(),
+            $expressionMatcherVariables,
+            '0',
+            $this->typoScriptCache,
+            null
+        );
         $newRequest = $originalRequest->withAttribute('frontend.typoscript', $frontendTypoScript);
-        $controller->releaseLocks();
         $controller->newCObj($newRequest);
         if (!isset($GLOBALS['TSFE']) || !$GLOBALS['TSFE'] instanceof TypoScriptFrontendController) {
             $GLOBALS['TSFE'] = $controller;
@@ -412,6 +430,24 @@ class RedirectService implements LoggerAwareInterface
         return $controller;
     }
 
+    private function getExpressionMatcherVariables(SiteInterface $site, ServerRequestInterface $request, TypoScriptFrontendController $controller): array
+    {
+        $pageInformation = $request->getAttribute('frontend.page.information');
+        $topDownRootLine = $pageInformation->getRootLine();
+        $localRootline = $pageInformation->getLocalRootLine();
+        ksort($topDownRootLine);
+        return [
+            'request' => $request,
+            'pageId' => $pageInformation->getId(),
+            'page' => $pageInformation->getPageRecord(),
+            'fullRootLine' => $topDownRootLine,
+            'localRootLine' => $localRootline,
+            'site' => $site,
+            'siteLanguage' => $request->getAttribute('language'),
+            'tsfe' => $controller,
+        ];
+    }
+
     protected function replaceRegExpCaptureGroup(array $matchedRedirect, UriInterface $uri, array $linkDetails): array
     {
         $uriToCheck = rawurldecode($uri->getPath());
diff --git a/typo3/sysext/redirects/Configuration/Services.yaml b/typo3/sysext/redirects/Configuration/Services.yaml
index 0b021b02588b..ff62488ec202 100644
--- a/typo3/sysext/redirects/Configuration/Services.yaml
+++ b/typo3/sysext/redirects/Configuration/Services.yaml
@@ -24,3 +24,7 @@ services:
   TYPO3\CMS\Redirects\Configuration\CheckIntegrityConfiguration:
     arguments:
       $extensionConfiguration: '@extension.configuration.redirects'
+
+  TYPO3\CMS\Redirects\Service\RedirectService:
+    arguments:
+      $typoScriptCache: '@cache.typoscript'
diff --git a/typo3/sysext/redirects/Tests/Functional/Service/IntegrityServiceTest.php b/typo3/sysext/redirects/Tests/Functional/Service/IntegrityServiceTest.php
index 18559e081a09..5053ca23e79a 100644
--- a/typo3/sysext/redirects/Tests/Functional/Service/IntegrityServiceTest.php
+++ b/typo3/sysext/redirects/Tests/Functional/Service/IntegrityServiceTest.php
@@ -19,10 +19,14 @@ namespace TYPO3\CMS\Redirects\Tests\Functional\Service;
 
 use PHPUnit\Framework\Attributes\Test;
 use PHPUnit\Framework\MockObject\MockObject;
+use TYPO3\CMS\Core\Cache\CacheManager;
+use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
 use TYPO3\CMS\Core\EventDispatcher\NoopEventDispatcher;
 use TYPO3\CMS\Core\LinkHandling\LinkService;
+use TYPO3\CMS\Core\Log\LogManager;
 use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\SiteFinder;
+use TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory;
 use TYPO3\CMS\Frontend\Page\PageInformationFactory;
 use TYPO3\CMS\Redirects\Service\IntegrityService;
 use TYPO3\CMS\Redirects\Service\RedirectCacheService;
@@ -40,13 +44,18 @@ final class IntegrityServiceTest extends FunctionalTestCase
     {
         parent::setUp();
         $siteFinder = $this->getSiteFinderMock();
+        /** @var PhpFrontend $typoScriptCache */
+        $typoScriptCache = $this->get(CacheManager::class)->getCache('typoscript');
         $this->subject = new IntegrityService(
             new RedirectService(
                 new RedirectCacheService(),
                 $this->getMockBuilder(LinkService::class)->disableOriginalConstructor()->getMock(),
                 $siteFinder,
                 new NoopEventDispatcher(),
-                $this->get(PageInformationFactory::class)
+                $this->get(PageInformationFactory::class),
+                $this->get(FrontendTypoScriptFactory::class),
+                $typoScriptCache,
+                $this->get(LogManager::class)->getLogger('Testing'),
             ),
             $siteFinder
         );
diff --git a/typo3/sysext/redirects/Tests/Functional/Service/RedirectServiceTest.php b/typo3/sysext/redirects/Tests/Functional/Service/RedirectServiceTest.php
index d44b39a52e90..ab5655fa3bea 100644
--- a/typo3/sysext/redirects/Tests/Functional/Service/RedirectServiceTest.php
+++ b/typo3/sysext/redirects/Tests/Functional/Service/RedirectServiceTest.php
@@ -22,6 +22,8 @@ use PHPUnit\Framework\Attributes\Test;
 use Psr\EventDispatcher\EventDispatcherInterface;
 use Psr\Log\NullLogger;
 use Symfony\Component\DependencyInjection\Container;
+use TYPO3\CMS\Core\Cache\CacheManager;
+use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
 use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
@@ -29,8 +31,10 @@ use TYPO3\CMS\Core\EventDispatcher\NoopEventDispatcher;
 use TYPO3\CMS\Core\Http\ServerRequest;
 use TYPO3\CMS\Core\Http\Uri;
 use TYPO3\CMS\Core\LinkHandling\LinkService;
+use TYPO3\CMS\Core\Log\LogManager;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait;
+use TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
 use TYPO3\CMS\Frontend\Page\PageInformationFactory;
@@ -98,14 +102,18 @@ final class RedirectServiceTest extends FunctionalTestCase
             ]
         );
 
+        /** @var PhpFrontend $typoScriptCache */
+        $typoScriptCache = $this->get(CacheManager::class)->getCache('typoscript');
         $redirectService = new RedirectService(
             new RedirectCacheService(),
             $linkServiceMock,
             $siteFinder,
             new NoopEventDispatcher(),
-            $this->get(PageInformationFactory::class)
+            $this->get(PageInformationFactory::class),
+            $this->get(FrontendTypoScriptFactory::class),
+            $typoScriptCache,
+            $this->get(LogManager::class)->getLogger('Testing'),
         );
-        $redirectService->setLogger($logger);
 
         // Assert correct redirect is matched
         $redirectMatch = $redirectService->matchRedirect($uri->getHost(), $uri->getPath(), $uri->getQuery());
@@ -850,12 +858,11 @@ final class RedirectServiceTest extends FunctionalTestCase
             $this->buildSiteConfiguration(1, 'https://acme.com/')
         );
 
-        $logger = new NullLogger();
         $frontendUserAuthentication = new FrontendUserAuthentication();
 
         $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
         $uri = new Uri('https://acme.com/non-existing-page');
-        $request = $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest($uri))
+        $GLOBALS['TYPO3_REQUEST'] = (new ServerRequest($uri))
             ->withAttribute('site', $siteFinder->getSiteByRootPageId(1))
             ->withAttribute('frontend.user', $frontendUserAuthentication)
             ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE);
@@ -878,14 +885,18 @@ final class RedirectServiceTest extends FunctionalTestCase
         $eventListener = $container->get(ListenerProvider::class);
         $eventListener->addListener(BeforeRedirectMatchDomainEvent::class, 'before-redirect-match-domain-event-is-triggered');
 
+        /** @var PhpFrontend $typoScriptCache */
+        $typoScriptCache = $this->get(CacheManager::class)->getCache('typoscript');
         $redirectService = new RedirectService(
             new RedirectCacheService(),
             new LinkService(),
             $siteFinder,
             $this->get(EventDispatcherInterface::class),
-            $this->get(PageInformationFactory::class)
+            $this->get(PageInformationFactory::class),
+            $this->get(FrontendTypoScriptFactory::class),
+            $typoScriptCache,
+            $this->get(LogManager::class)->getLogger('Testing'),
         );
-        $redirectService->setLogger($logger);
 
         $redirectMatch = $redirectService->matchRedirect($uri->getHost(), $uri->getPath(), $uri->getQuery());
 
diff --git a/typo3/sysext/redirects/Tests/Unit/Service/RedirectServiceTest.php b/typo3/sysext/redirects/Tests/Unit/Service/RedirectServiceTest.php
index 5c136fa9ebd2..501394647bd7 100644
--- a/typo3/sysext/redirects/Tests/Unit/Service/RedirectServiceTest.php
+++ b/typo3/sysext/redirects/Tests/Unit/Service/RedirectServiceTest.php
@@ -20,7 +20,10 @@ namespace TYPO3\CMS\Redirects\Tests\Unit\Service;
 use PHPUnit\Framework\Attributes\DataProvider;
 use PHPUnit\Framework\Attributes\Test;
 use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
 use Psr\Log\NullLogger;
+use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Domain\Access\RecordAccessVoter;
@@ -30,12 +33,20 @@ use TYPO3\CMS\Core\Http\Uri;
 use TYPO3\CMS\Core\LinkHandling\LinkService;
 use TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService;
 use TYPO3\CMS\Core\Log\Logger;
+use TYPO3\CMS\Core\Package\PackageManager;
 use TYPO3\CMS\Core\Resource\Exception\InvalidPathException;
 use TYPO3\CMS\Core\Resource\File;
 use TYPO3\CMS\Core\Resource\Folder;
+use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
 use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\SiteFinder;
+use TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateTreeBuilder;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\ConditionVerdictAwareIncludeTreeTraverser;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\Traverser\IncludeTreeTraverser;
+use TYPO3\CMS\Core\TypoScript\IncludeTree\TreeFromLineStreamBuilder;
+use TYPO3\CMS\Core\TypoScript\Tokenizer\LossyTokenizer;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
 use TYPO3\CMS\Frontend\Controller\ErrorController;
@@ -78,9 +89,23 @@ final class RedirectServiceTest extends UnitTestCase
                 new RecordAccessVoter(new NoopEventDispatcher()),
                 new ErrorController(),
                 new SysTemplateRepository(new NoopEventDispatcher(), $this->createMock(ConnectionPool::class), new Context()),
-            )
+            ),
+            new FrontendTypoScriptFactory(
+                $this->createMock(ContainerInterface::class),
+                new NoopEventDispatcher(),
+                new SysTemplateTreeBuilder(
+                    $this->createMock(ConnectionPool::class),
+                    $this->createMock(PackageManager::class),
+                    new Context(),
+                    new TreeFromLineStreamBuilder(new FileNameValidator())
+                ),
+                new LossyTokenizer(),
+                new IncludeTreeTraverser(),
+                new ConditionVerdictAwareIncludeTreeTraverser(),
+            ),
+            $this->createMock(PhpFrontend::class),
+            $this->createMock(LoggerInterface::class),
         );
-        $this->redirectService->setLogger($logger);
 
         $GLOBALS['SIM_ACCESS_TIME'] = 42;
     }
@@ -633,13 +658,25 @@ final class RedirectServiceTest extends UnitTestCase
                     new ErrorController(),
                     new SysTemplateRepository(new NoopEventDispatcher(), $this->createMock(ConnectionPool::class), new Context()),
                 ),
+                new FrontendTypoScriptFactory(
+                    $this->createMock(ContainerInterface::class),
+                    new NoopEventDispatcher(),
+                    new SysTemplateTreeBuilder(
+                        $this->createMock(ConnectionPool::class),
+                        $this->createMock(PackageManager::class),
+                        new Context(),
+                        new TreeFromLineStreamBuilder(new FileNameValidator())
+                    ),
+                    new LossyTokenizer(),
+                    new IncludeTreeTraverser(),
+                    new ConditionVerdictAwareIncludeTreeTraverser(),
+                ),
+                $this->createMock(PhpFrontend::class),
+                $this->createMock(LoggerInterface::class),
             ],
             '',
         );
 
-        $logger = new NullLogger();
-        $redirectService->setLogger($logger);
-
         $pageRecord = 't3://page?uid=13';
         $redirectTargetMatch = [
             'target' => $pageRecord . ' - - - foo=bar',
diff --git a/typo3/sysext/seo/Tests/Functional/Canonical/CanonicalGeneratorTest.php b/typo3/sysext/seo/Tests/Functional/Canonical/CanonicalGeneratorTest.php
index ff699a7c2b8c..035d3c5e622f 100644
--- a/typo3/sysext/seo/Tests/Functional/Canonical/CanonicalGeneratorTest.php
+++ b/typo3/sysext/seo/Tests/Functional/Canonical/CanonicalGeneratorTest.php
@@ -180,7 +180,7 @@ final class CanonicalGeneratorTest extends FunctionalTestCase
         ];
         $pageInformation->setPageRecord($pageRecord);
         $request = $request->withAttribute('frontend.page.information', $pageInformation);
-        $typoScript = new FrontendTypoScript(new RootNode(), []);
+        $typoScript = new FrontendTypoScript(new RootNode(), [], [], []);
         $typoScript->setConfigArray([]);
         $request = $request->withAttribute('frontend.typoscript', $typoScript);
         $this->get(CanonicalGenerator::class)->generate(['request' => $request]);
-- 
GitLab