diff --git a/typo3/sysext/core/Classes/TypoScript/FrontendTypoScript.php b/typo3/sysext/core/Classes/TypoScript/FrontendTypoScript.php
index aa3bcdd9ce4863a251543cee63fc65bb8f8078be..3fd6fb3c1f5c658ff18893a3fcbce544f4bf5a0f 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 0000000000000000000000000000000000000000..24fd0c8c3e05d9a443777b2516297eb435907c9e
--- /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 3d2c4e34c3604dcb5acbf209e20fe211e189047e..83cc53a579d511311717084dd1dc050bbc2ef885 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 fe5314605e60bf96dba7deb922fdb9d4ae540bf1..7d38fbae51747057424f3249e9c005148fe19175 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 b4a6f425de4898663148c91b43cb3e2e0b623bd4..dec4d7f844b76bb40ab9b4e3b4fdb1398aaad98f 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 f2198087f81133471ad7cd726c70a3572b175d8f..b37038551089767252036d77b4438e41260563f6 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 8b35411d8fc334ca0c0140f09290055b72bf602b..955edaeec33085582f2e864f020583948986df97 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 0b093a2731adbc3ecad4f19c96818f6182d97a6d..e59be60434cde593f762a3d466720289fbc9613f 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 cadc67b8019a67f0635ea6db78fe83bf94d8a192..bbe377817f92b349e06bb01cee81b37d9a47554d 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 7280dec7c025b643974c9dacfabc4261355b74ac..d98d9353f05e624d25a2a91e038a88f842e0354f 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 9423e759d00ea32ee6cea6a8339d3279c83607f3..91844e502de26d998a5c24318190cde4e70b0bae 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 c1deed47c62a4d5d6e5333f63bacad352ddd3caf..8088b0ea69ada6d158f38910b02d2ba7ce8cd758 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 ca30813fedc6ddb6632cf4febb680d671cf5c1cf..b2d72e6b9830f55b829b2e79b9de90858c818a50 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 7dd600abd23896298a827d57391554e85b829cc1..122e5ca601d13fc6e3e49aa6a7410fd37500bb44 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 91143b603ca2e7e372e6f2fd97ecf249676089d3..b92ce8c982e385616af0ff1f61c0c5ef041a2b25 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 79851ed366e98a5a2d70ec02e24db3a2f7754d19..b00a67d148e75f9bf824ef378e49ac778b2e198d 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 c5c73ea75a6468ae3291fa2bf5ea2fed892225e6..6148c85312a2fea5f24cba696317a5aa274c788f 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 836331b473601d68b074c180a94a9ff9a0db236e..d102abdb080668ae7872d1bc6546ffbc9123109d 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 f93bf5418c048426af33b6654d8b437c1780d29e..46e0d963c3bf7b3f7d1f67a8012bc5c2c8f27643 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 b5602b3d7f2db76b3d94325d71a38cc790f85f8c..6b31e7e790f2dd22be43f5c3e685a33be92016a2 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 397cf9cb27d7e62ce21be65c4f3ac0d058fa23dc..3d9379501625b95a44f69bb3a314a1a48fb0a3d9 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 fd654d806e82a7634cdea29c8bb1317b8996efe1..636de88afdfcc01c1f708eb99870ce1d688e5012 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 c8ee38a0eeddb521ee4f6bacfccd6506b311dbbc..52305edbb6a709bfa3bc805a60a3a037754af578 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 22c1db10780b4d150b0ab26550019977bfeac916..14549e716d7a51283a1fd5afc8a8eedf485c306c 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 343b2f4b8b1d632fdf0ca4e810f7fee12d696e33..745fe16cc51fef58c22b7c26ee862d6d1fbc4997 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 b1f183b89b2c6563a6d1534d1d2c23c645090a47..62c6d1ce234acea83b4f9185007223ac2b0593b2 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 e681c8a246d5f10446a5685913c641e3481c2cbb..a116d585d39af1e9298198843c9a0bbafe80be47 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 34c24d924f84df715f8a69d0e28d571e8accdeb7..1225fb2134ba0efd76ba36357b01ede7fda2229f 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 88de1806ce0da80805a8e3dc14629d5ab54569e2..bb431a4545af490cffcb412d44f45acb5b0f612b 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 0b021b02588be598db1cf45307624401bb910d1b..ff62488ec202f671f8af55ccc9aed13bda73f858 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 18559e081a09717145e13c1955f98cd5f78ec7f1..5053ca23e79ac5226726ce528578c027a6518341 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 d44b39a52e90fe08cc0a62d8d3e818809ac546cb..ab5655fa3bead214dd0bcce04bf39cddae10018a 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 5c136fa9ebd2f00e1a2a4ccd9f59225d19dee848..501394647bd7bee0646c860f010c4284a509e2fe 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 ff699a7c2b8c6f04730bd12741fb29e6d8b18757..035d3c5e622fc6ae837fd5bb36a14296dd332701 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]);