diff --git a/Build/types/TYPO3/index.d.ts b/Build/types/TYPO3/index.d.ts
index 615d35c80678a4bb5984d0bd7c48677f6146b3f8..c3e77596006d844f1a4da74617b78d8156dbb260 100644
--- a/Build/types/TYPO3/index.d.ts
+++ b/Build/types/TYPO3/index.d.ts
@@ -29,6 +29,14 @@ declare namespace TYPO3 {
   export const lang: { [key: string]: string };
   export const configuration: any;
   export namespace CMS {
+    export namespace Core {
+      export class JavaScriptHandler {
+        public processItems(data: string|any[], isParsed?: boolean): void;
+        public globalAssignment(data: string|any, isParsed?: boolean): void;
+        public javaScriptModuleInstruction(data: string|any, isParsed?: boolean): void;
+      }
+    }
+
     export namespace Backend {
       export class FormEngineValidation {
         public readonly errorClass: string;
@@ -95,6 +103,12 @@ declare namespace TYPO3 {
 /**
  * Current AMD/RequireJS modules are returning *instances* of ad-hoc *classes*, make that known to TypeScript
  */
+
+declare module 'TYPO3/CMS/Core/JavaScriptHandler' {
+  const _exported: TYPO3.CMS.Core.JavaScriptHandler;
+  export = _exported;
+}
+
 declare module 'TYPO3/CMS/Backend/FormEngineValidation' {
   const _exported: TYPO3.CMS.Backend.FormEngineValidation;
   export = _exported;
diff --git a/typo3/sysext/core/Classes/Page/JavaScriptItems.php b/typo3/sysext/core/Classes/Page/JavaScriptItems.php
new file mode 100644
index 0000000000000000000000000000000000000000..3821ba70b206c7f4a432a1671987591fbd5ce5de
--- /dev/null
+++ b/typo3/sysext/core/Classes/Page/JavaScriptItems.php
@@ -0,0 +1,80 @@
+<?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\Page;
+
+class JavaScriptItems implements \JsonSerializable
+{
+    /**
+     * @var list<array>
+     */
+    protected array $globalAssignments = [];
+
+    /**
+     * @var list<JavaScriptModuleInstruction>
+     */
+    protected array $javaScriptModuleInstructions = [];
+
+    public function jsonSerialize(): array
+    {
+        return $this->toArray();
+    }
+
+    public function addGlobalAssignment(array $payload): void
+    {
+        if (empty($payload)) {
+            return;
+        }
+        $this->globalAssignments[] = $payload;
+    }
+
+    public function addJavaScriptModuleInstruction(JavaScriptModuleInstruction $instruction): void
+    {
+        $this->javaScriptModuleInstructions[] = $instruction;
+    }
+
+    /**
+     * @return list<array{type: string, payload: mixed}>
+     * @internal
+     */
+    public function toArray(): array
+    {
+        if ($this->isEmpty()) {
+            return [];
+        }
+        $items = [];
+        foreach ($this->globalAssignments as $item) {
+            $items[] = [
+                'type' => 'globalAssignment',
+                'payload' => $item,
+            ];
+        }
+        foreach ($this->javaScriptModuleInstructions as $item) {
+            $items[] = [
+                'type' => 'javaScriptModuleInstruction',
+                'payload' => $item,
+            ];
+        }
+        return $items;
+    }
+
+    public function isEmpty(): bool
+    {
+        return $this->globalAssignments === []
+            && empty($this->javaScriptModuleInstructions);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Page/JavaScriptModuleInstruction.php b/typo3/sysext/core/Classes/Page/JavaScriptModuleInstruction.php
index b9adbc48ade6b4693af5896b5305890202258b05..417aed861e0f38f3fc35996d3d4ab9b69ad6fe14 100644
--- a/typo3/sysext/core/Classes/Page/JavaScriptModuleInstruction.php
+++ b/typo3/sysext/core/Classes/Page/JavaScriptModuleInstruction.php
@@ -34,6 +34,7 @@ class JavaScriptModuleInstruction implements \JsonSerializable
 
     /**
      * @param string $name RequireJS module name
+     * @param ?string $exportName (optional) name used internally to export the module
      * @return static
      */
     public static function forRequireJS(string $name, string $exportName = null): self
diff --git a/typo3/sysext/core/Classes/Page/JavaScriptRenderer.php b/typo3/sysext/core/Classes/Page/JavaScriptRenderer.php
index f6ec3a85db65d02f42ca8e69ad6021bdc968ed1e..6784dd2d9fb376a629fbb5548a1ef9b3127257f2 100644
--- a/typo3/sysext/core/Classes/Page/JavaScriptRenderer.php
+++ b/typo3/sysext/core/Classes/Page/JavaScriptRenderer.php
@@ -23,18 +23,9 @@ use TYPO3\CMS\Core\Utility\PathUtility;
 class JavaScriptRenderer
 {
     protected string $handlerUri;
+    protected JavaScriptItems $items;
     protected ?RequireJS $requireJS = null;
 
-    /**
-     * @var list<array>
-     */
-    protected array $globalAssignments = [];
-
-    /**
-     * @var list<JavaScriptModuleInstruction>
-     */
-    protected array $javaScriptModuleInstructions = [];
-
     public static function create(string $uri = null): self
     {
         $uri ??= PathUtility::getAbsoluteWebPath(
@@ -46,6 +37,7 @@ class JavaScriptRenderer
     public function __construct(string $handlerUri)
     {
         $this->handlerUri = $handlerUri;
+        $this->items = GeneralUtility::makeInstance(JavaScriptItems::class);
     }
 
     public function loadRequireJS(RequireJS $requireJS): void
@@ -55,15 +47,12 @@ class JavaScriptRenderer
 
     public function addGlobalAssignment(array $payload): void
     {
-        if (empty($payload)) {
-            return;
-        }
-        $this->globalAssignments[] = $payload;
+        $this->items->addGlobalAssignment($payload);
     }
 
     public function addJavaScriptModuleInstruction(JavaScriptModuleInstruction $instruction): void
     {
-        $this->javaScriptModuleInstructions[] = $instruction;
+        $this->items->addJavaScriptModuleInstruction($instruction);
     }
 
     /**
@@ -78,21 +67,12 @@ class JavaScriptRenderer
         $items = [];
         if ($this->requireJS !== null) {
             $items[] = [
-                'type' => 'loadRequireJS',
+                'type' => 'loadRequireJs',
                 'payload' => $this->requireJS,
             ];
         }
-        foreach ($this->globalAssignments as $item) {
-            $items[] = [
-                'type' => 'globalAssignment',
-                'payload' => $item,
-            ];
-        }
-        foreach ($this->javaScriptModuleInstructions as $item) {
-            $items[] = [
-                'type' => 'javaScriptModuleInstruction',
-                'payload' => $item,
-            ];
+        foreach ($this->items->toArray() as $item) {
+            $items[] = $item;
         }
         return $items;
     }
@@ -104,15 +84,13 @@ class JavaScriptRenderer
         }
         return $this->createScriptElement([
             'src' => $this->handlerUri,
-            'data-process-type' => 'processItems',
+            'data-process-text-content' => 'processItems',
         ], $this->jsonEncode($this->toArray()));
     }
 
     protected function isEmpty(): bool
     {
-        return $this->requireJS === null
-            && $this->globalAssignments === []
-            && empty($this->javaScriptModuleInstructions);
+        return $this->requireJS === null && $this->items->isEmpty();
     }
 
     protected function createScriptElement(array $attributes, string $textContent = ''): string
diff --git a/typo3/sysext/core/Resources/Public/JavaScript/JavaScriptHandler.js b/typo3/sysext/core/Resources/Public/JavaScript/JavaScriptHandler.js
index a712ad6c3ab78bbac2ee9f8375bb89efebd0f211..fb1c7a755d3bb04967d38962919019e66205318c 100644
--- a/typo3/sysext/core/Resources/Public/JavaScript/JavaScriptHandler.js
+++ b/typo3/sysext/core/Resources/Public/JavaScript/JavaScriptHandler.js
@@ -12,147 +12,184 @@
  */
 /**
  * This handler is used as client-side counterpart of `\TYPO3\CMS\Core\Page\JavaScriptRenderer`.
+ * It either can be used standalone or as requireJS module internally.
+ *
+ * @module TYPO3/CMS/Core/JavaScriptHandler
+ * @internal Use in TYPO3 core only, API can change at any time!
  */
 (function() {
+  "use strict";
+
   // @todo Handle document.currentScript.async
   if (!document.currentScript) {
     return false;
   }
+
   const FLAG_LOAD_REQUIRE_JS = 1;
   const deniedProperties = ['__proto__', 'prototype', 'constructor'];
-  const supportedItemTypes = ['assign', 'invoke', 'instance'];
+  const allowedRequireJsItemTypes = ['assign', 'invoke', 'instance'];
+  const allowedRequireJsNames = ['globalAssignment', 'javaScriptModuleInstruction'];
+  const allowedDirectNames = ['processTextContent', 'loadRequireJs', 'processItems', 'globalAssignment', 'javaScriptModuleInstruction'];
   const scriptElement = document.currentScript;
-  const handlers = {
-    /**
-     * @param {string} type sub-handler type (processItems, loadRequireJs, globalAssignment, javaScriptModuleInstruction)
-     */
-    processType: (type) => {
-      // extracts JSON payload from `/* [JSON] */` content
-      invokeHandler(type, scriptElement.textContent.replace(/^\s*\/\*\s*|\s*\*\/\s*/g, ''));
-    },
+
+  class JavaScriptHandler {
     /**
-     * Processes multiple items and delegates to sub-handlers (processItems, loadRequireJs, globalAssignment, javaScriptModuleInstruction)
-     * @param {string} data JSON data
+     * @param {any} json
+     * @param {string} json.name module name
+     * @param {string} json.exportName? name used internally to export the module
+     * @param {array<{type: string, assignments?: object, method?: string, args: array}>} json.items
      */
-    processItems: (data) => {
-      const json = JSON.parse(data);
-      if (!isArrayInstance(json)) {
+    static loadRequireJsModule(json) {
+      // `name` is required
+      if (!json.name) {
+        throw new Error('RequireJS module name is required');
+      }
+      if (!json.items) {
+        require([json.name]);
         return;
       }
-      json.forEach((item) => invokeHandler(item.type, item.payload, true));
-    },
+      const exportName = json.exportName;
+      const resolveSubjectRef = (__esModule) => {
+        return typeof exportName === 'string' ? __esModule[exportName] : __esModule;
+      }
+      const items = json.items
+        .filter((item) => allowedRequireJsItemTypes.includes(item.type))
+        .map((item) => {
+          if (item.type === 'assign') {
+            return (__esModule) => {
+              const subjectRef = resolveSubjectRef(__esModule);
+              JavaScriptHandler.mergeRecursive(subjectRef, item.assignments);
+            };
+          } else if (item.type === 'invoke') {
+            return (__esModule) => {
+              const subjectRef = resolveSubjectRef(__esModule);
+              subjectRef[item.method].apply(subjectRef, item.args);
+            };
+          } else if (item.type === 'instance') {
+            return (__esModule) => {
+              // this `null` is `thisArg` scope of `Function.bind`,
+              // which will be reset when invoking `new`
+              const args = [null].concat(item.args);
+              const subjectRef = resolveSubjectRef(__esModule);
+              new (subjectRef.bind.apply(subjectRef, args));
+            }
+          }
+        });
+      require(
+        [json.name],
+        (subjectRef) => items.forEach((item) => item.call(null, subjectRef))
+      );
+    }
+
+    static isObjectInstance(item) {
+      return item instanceof Object && !(item instanceof Array);
+    }
+
+    static isArrayInstance(item) {
+      return item instanceof Array;
+    }
+
+    static mergeRecursive(target, source) {
+      Object.keys(source).forEach((property) => {
+        if (deniedProperties.indexOf(property) !== -1) {
+          throw new Error('Property ' + property + ' is not allowed');
+        }
+        if (!JavaScriptHandler.isObjectInstance(source[property]) || typeof target[property] === 'undefined') {
+          Object.assign(target, {[property]:source[property]});
+        } else {
+          JavaScriptHandler.mergeRecursive(target[property], source[property]);
+        }
+      });
+    }
+
+    constructor(invokableNames) {
+      this.invokableNames = invokableNames;
+    }
+
+    invoke(name, data, isParsed = false) {
+      if (!this.invokableNames.includes(name) || typeof this[name] !== 'function') {
+        throw new Error('Unknown handler name "' + name + '"');
+      }
+      this[name].call(this, data, Boolean(isParsed));
+    }
+
+    /**
+     * @param {string} type of sub-handler (processItems, loadRequireJs, globalAssignment, javaScriptModuleInstruction)
+     */
+    processTextContent(type) {
+      // extracts JSON payload from `/* [JSON] */` content
+      this.invoke(type, scriptElement.textContent.replace(/^\s*\/\*\s*|\s*\*\/\s*/g, ''));
+    }
+
     /**
      * Initializes require.js configuration - require.js sources must be loaded already.
-     * @param {string} data JSON data
+     * @param {string|any} data JSON data
      * @param {boolean} isParsed whether data has been parsed already
      */
-    loadRequireJS: (data, isParsed) => {
+    loadRequireJs(data, isParsed = false) {
       const payload = isParsed ? data : JSON.parse(data);
-      if (!isObjectInstance(payload)) {
-        return;
+      if (!JavaScriptHandler.isObjectInstance(payload)) {
+        throw new Error('Expected payload object');
       }
       require.config(payload.config);
-    },
+    }
+
+    /**
+     * Processes multiple items and delegates to sub-handlers
+     * (processItems, loadRequireJs, globalAssignment, javaScriptModuleInstruction)
+     * @param {string|any[]} data JSON data
+     * @param {boolean} isParsed whether data has been parsed already
+     */
+    processItems(data, isParsed = false) {
+      const payload = isParsed ? data : JSON.parse(data);
+      if (!JavaScriptHandler.isArrayInstance(payload)) {
+        throw new Error('Expected payload array');
+      }
+      payload.forEach((item) => this.invoke(item.type, item.payload, true));
+    }
+
     /**
      * Assigns (filtered) variables to `window` object globally.
-     * @param {string} data JSON data
+     * @param {string|any} data JSON data
      * @param {boolean} isParsed whether data has been parsed already
      */
-    globalAssignment: (data, isParsed) => {
+    globalAssignment(data, isParsed = false) {
       const payload = isParsed ? data : JSON.parse(data);
-      if (!isObjectInstance(payload)) {
-        return;
+      if (!JavaScriptHandler.isObjectInstance(payload)) {
+        throw new Error('Expected payload object');
       }
-      mergeRecursive(window, payload);
-    },
+      JavaScriptHandler.mergeRecursive(window, payload);
+    }
+
     /**
      * Loads and invokes a requires.js module (AMD).
-     * @param {string} data JSON data
+     * @param {string|any} data JSON data
      * @param {boolean} isParsed whether data has been parsed already
      */
-    javaScriptModuleInstruction: (data, isParsed) => {
+    javaScriptModuleInstruction(data, isParsed = false) {
       const payload = isParsed ? data : JSON.parse(data);
       if ((payload.flags & FLAG_LOAD_REQUIRE_JS) === FLAG_LOAD_REQUIRE_JS) {
-        loadRequireJsModule(payload);
+        JavaScriptHandler.loadRequireJsModule(payload);
       }
     }
-  };
-
-  function loadRequireJsModule(json) {
-    // `name` is required
-    if (!json.name) {
-      return;
-    }
-    if (!json.items) {
-      require([json.name]);
-      return;
-    }
-    const exportName = json.exportName;
-    const resolveSubjectRef = (__esModule) => {
-      return typeof exportName === 'string' ? __esModule[exportName] : __esModule;
-    }
-    const items = json.items
-      .filter((item) => supportedItemTypes.includes(item.type))
-      .map((item) => {
-        if (item.type === 'assign') {
-          return (__esModule) => {
-            const subjectRef = resolveSubjectRef(__esModule);
-            mergeRecursive(subjectRef, item.assignments);
-          };
-        } else if (item.type === 'invoke') {
-          return (__esModule) => {
-            const subjectRef = resolveSubjectRef(__esModule);
-            subjectRef[item.method].apply(subjectRef, item.args);
-          };
-        } else if (item.type === 'instance') {
-          return (__esModule) => {
-            // this `null` is `thisArg` scope of `Function.bind`,
-            // which will be reset when invoking `new`
-            const args = [null].concat(item.args);
-            const subjectRef = resolveSubjectRef(__esModule);
-            new (subjectRef.bind.apply(subjectRef, args));
-          }
-        }
-      });
-    require(
-      [json.name],
-      (subjectRef) => items.forEach((item) => item.call(null, subjectRef))
-    );
   }
 
-  function isObjectInstance(item) {
-    return item instanceof Object && !(item instanceof Array);
-  }
-  function isArrayInstance(item) {
-    return item instanceof Array;
-  }
-  function mergeRecursive(target, source) {
-    Object.keys(source).forEach((property) => {
-      if (deniedProperties.indexOf(property) !== -1) {
-        throw new Error('Property ' + property + ' is not allowed');
-      }
-      if (!isObjectInstance(source[property]) || typeof target[property] === 'undefined') {
-        Object.assign(target, {[property]:source[property]});
-      } else {
-        mergeRecursive(target[property], source[property]);
-      }
+  // called using requireJS
+  if (scriptElement.dataset.requirecontext && scriptElement.dataset.requiremodule) {
+    const handler = new JavaScriptHandler(allowedRequireJsNames);
+    define(['require','exports'], () => {
+      return handler;
     });
-  }
-
-  function invokeHandler(name, data, isParsed) {
-    if (typeof handlers[name] === 'undefined') {
-      return;
-    }
-    handlers[name].call(null, data, Boolean(isParsed));
-  }
-
-  // start processing dataset declarations
-  Object.keys(scriptElement.dataset)
-    .forEach((name) => {
+  // called directly using `<script>` element
+  } else {
+    const handler = new JavaScriptHandler(allowedDirectNames);
+    // start processing dataset declarations
+    Object.keys(scriptElement.dataset).forEach((name) => {
       try {
-        invokeHandler(name, scriptElement.dataset[name]);
+        handler.invoke(name, scriptElement.dataset[name]);
       } catch (e) {
         console.error(e);
       }
     });
+  }
 })();
diff --git a/typo3/sysext/core/Tests/Functional/Page/JavaScriptRendererTest.php b/typo3/sysext/core/Tests/Functional/Page/JavaScriptRendererTest.php
index dc3760e6c747aa86910ab8050e950e5bcd5f8192..09736194bd3305adcc6a87d911bab29f5a3a3ee3 100644
--- a/typo3/sysext/core/Tests/Functional/Page/JavaScriptRendererTest.php
+++ b/typo3/sysext/core/Tests/Functional/Page/JavaScriptRendererTest.php
@@ -37,7 +37,7 @@ class JavaScriptRendererTest extends FunctionalTestCase
         );
         $subject->addGlobalAssignment(['section*/' => ['key*/' => 'value*/']]);
         self::assertSame(
-            '<script src="anything.js" data-process-type="processItems">'
+            '<script src="anything.js" data-process-text-content="processItems">'
                 . '/* [{"type":"globalAssignment","payload":{"section*\/":{"key*\/":"value*\/"}}},'
                 . '{"type":"javaScriptModuleInstruction","payload":{"name":"TYPO3\/CMS\/Test*\/","exportName":null,'
                 . '"flags":1,"items":[{"type":"invoke","method":"test*\/","args":["arg*\/"]}]}}] */</script>',
diff --git a/typo3/sysext/core/Tests/Functional/Page/PageRendererTest.php b/typo3/sysext/core/Tests/Functional/Page/PageRendererTest.php
index 51e0687d94ea28c4d612f2a3d0e77aece8b5b888..7e28cbd4ef6d471c36c730d26ad387b65175e019 100644
--- a/typo3/sysext/core/Tests/Functional/Page/PageRendererTest.php
+++ b/typo3/sysext/core/Tests/Functional/Page/PageRendererTest.php
@@ -230,7 +230,7 @@ class PageRendererTest extends FunctionalTestCase
         if ($requestType === SystemEnvironmentBuilder::REQUESTTYPE_FE) {
             $expectedInlineAssignmentsPrefix = 'var TYPO3 = Object.assign(TYPO3 || {}, Object.fromEntries(Object.entries({"settings":';
         } else {
-            $expectedInlineAssignmentsPrefix = '<script src="typo3/sysext/core/Resources/Public/JavaScript/JavaScriptHandler.js" data-process-type="processItems">/* [{"type":"globalAssignment","payload":{"TYPO3":{"settings":';
+            $expectedInlineAssignmentsPrefix = '<script src="typo3/sysext/core/Resources/Public/JavaScript/JavaScriptHandler.js" data-process-text-content="processItems">/* [{"type":"globalAssignment","payload":{"TYPO3":{"settings":';
         }
 
         $renderedString = $subject->render();