diff --git a/integration/defer-loader-test.ts b/integration/defer-loader-test.ts
new file mode 100644
index 00000000000..a8a292f9f34
--- /dev/null
+++ b/integration/defer-loader-test.ts
@@ -0,0 +1,93 @@
+import { test, expect } from "@playwright/test";
+
+import { createAppFixture, createFixture, js } from "./helpers/create-fixture";
+import type { Fixture, AppFixture } from "./helpers/create-fixture";
+import { PlaywrightFixture } from "./helpers/playwright-fixture";
+
+let fixture: Fixture;
+let appFixture: AppFixture;
+
+test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/routes/index.jsx": js`
+ import { useLoaderData, Link } from "@remix-run/react";
+ export default function Index() {
+ return (
+
+ Redirect
+ Direct Promise Access
+
+ )
+ }
+ `,
+
+ "app/routes/redirect.jsx": js`
+ import { defer } from "@remix-run/node";
+ export function loader() {
+ return defer({food: "pizza"}, { status: 301, headers: { Location: "/?redirected" } });
+ }
+ export default function Redirect() {return null;}
+ `,
+
+ "app/routes/direct-promise-access.jsx": js`
+ import * as React from "react";
+ import { defer } from "@remix-run/node";
+ import { useLoaderData, Link, Await } from "@remix-run/react";
+ export function loader() {
+ return defer({
+ bar: new Promise(async (resolve, reject) => {
+ resolve("hamburger");
+ }),
+ });
+ }
+ let count = 0;
+ export default function Index() {
+ let {bar} = useLoaderData();
+ React.useEffect(() => {
+ let aborted = false;
+ bar.then((data) => {
+ if (aborted) return;
+ document.getElementById("content").innerHTML = data + " " + (++count);
+ document.getElementById("content").setAttribute("data-done", "");
+ });
+ return () => {
+ aborted = true;
+ };
+ }, [bar]);
+ return (
+
+ Waiting for client hydration....
+
+ )
+ }
+ `,
+ },
+ });
+
+ appFixture = await createAppFixture(fixture);
+});
+
+test.afterAll(async () => appFixture.close());
+
+test("deferred response can redirect on document request", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/redirect");
+ await page.waitForURL(/\?redirected/);
+});
+
+test("deferred response can redirect on transition", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+ await app.clickLink("/redirect");
+ await page.waitForURL(/\?redirected/);
+});
+
+test("can directly access result from deferred promise on document request", async ({
+ page,
+}) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/direct-promise-access");
+ let element = await page.waitForSelector("[data-done]");
+ expect(await element.innerText()).toMatch("hamburger 1");
+});
diff --git a/integration/defer-test.ts b/integration/defer-test.ts
new file mode 100644
index 00000000000..bbb85be363d
--- /dev/null
+++ b/integration/defer-test.ts
@@ -0,0 +1,1272 @@
+import { test, expect } from "@playwright/test";
+import type { ConsoleMessage, Page } from "@playwright/test";
+
+import { PlaywrightFixture } from "./helpers/playwright-fixture";
+import type { Fixture, AppFixture } from "./helpers/create-fixture";
+import { createAppFixture, createFixture, js } from "./helpers/create-fixture";
+
+const ROOT_ID = "ROOT_ID";
+const INDEX_ID = "INDEX_ID";
+const DEFERRED_ID = "DEFERRED_ID";
+const RESOLVED_DEFERRED_ID = "RESOLVED_DEFERRED_ID";
+const FALLBACK_ID = "FALLBACK_ID";
+const ERROR_ID = "ERROR_ID";
+const ERROR_BOUNDARY_ID = "ERROR_BOUNDARY_ID";
+const MANUAL_RESOLVED_ID = "MANUAL_RESOLVED_ID";
+const MANUAL_FALLBACK_ID = "MANUAL_FALLBACK_ID";
+const MANUAL_ERROR_ID = "MANUAL_ERROR_ID";
+
+declare global {
+ // eslint-disable-next-line prefer-let/prefer-let
+ var __deferredManualResolveCache: {
+ nextId: number;
+ deferreds: Record<
+ string,
+ { resolve: (value: any) => void; reject: (error: Error) => void }
+ >;
+ };
+}
+
+test.describe("non-aborted", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
+ ////////////////////////////////////////////////////////////////////////////
+ // 💿 Next, add files to this object, just like files in a real app,
+ // `createFixture` will make an app and run your tests against it.
+ ////////////////////////////////////////////////////////////////////////////
+ files: {
+ "app/components/counter.tsx": js`
+ import { useState } from "react";
+
+ export default function Counter({ id }) {
+ let [count, setCount] = useState(0);
+ return (
+
+
setCount((c) => c+1)}>Increment
+
{count}
+
+ )
+ }
+ `,
+ "app/components/interactive.tsx": js`
+ import { useEffect, useState } from "react";
+
+ export default function Interactive() {
+ let [interactive, setInteractive] = useState(false);
+ useEffect(() => {
+ setInteractive(true);
+ }, []);
+ return interactive ? (
+
+ ) : null;
+ }
+ `,
+ "app/root.tsx": js`
+ import { defer } from "@remix-run/node";
+ import { Links, Meta, Outlet, Scripts, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+ import Interactive from "~/components/interactive";
+
+ export const meta: MetaFunction = () => ({
+ charset: "utf-8",
+ title: "New Remix App",
+ viewport: "width=device-width,initial-scale=1",
+ });
+
+ export const loader = () => defer({
+ id: "${ROOT_ID}",
+ });
+
+ export default function Root() {
+ let { id } = useLoaderData();
+ return (
+
+
+
+
+
+
+
+
+ {/* Send arbitrary data so safari renders the initial shell before
+ the document finishes downloading. */}
+ {Array(10000).fill(null).map((_, i)=>YOOOOOOOOOO {i}
)}
+
+
+ );
+ }
+ `,
+
+ "app/routes/index.tsx": js`
+ import { defer } from "@remix-run/node";
+ import { Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ id: "${INDEX_ID}",
+ });
+ }
+
+ export default function Index() {
+ let { id } = useLoaderData();
+ return (
+
+
{id}
+
+
+
+ deferred-script-resolved
+ deferred-script-unresolved
+ deferred-script-rejected
+ deferred-script-unrejected
+ deferred-script-rejected-no-error-element
+ deferred-script-unrejected-no-error-element
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-noscript-resolved.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: Promise.resolve("${RESOLVED_DEFERRED_ID}"),
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-noscript-unresolved.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: new Promise(
+ (resolve) => setTimeout(() => {
+ resolve("${RESOLVED_DEFERRED_ID}");
+ }, 10)
+ ),
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-resolved.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: Promise.resolve("${RESOLVED_DEFERRED_ID}"),
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-unresolved.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: new Promise(
+ (resolve) => setTimeout(() => {
+ resolve("${RESOLVED_DEFERRED_ID}");
+ }, 10)
+ ),
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-rejected.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: Promise.reject(new Error("${RESOLVED_DEFERRED_ID}")),
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+
+ error
+
+
+ }
+ children={(resolvedDeferredId) => (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-unrejected.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: new Promise(
+ (_, reject) => setTimeout(() => {
+ reject(new Error("${RESOLVED_DEFERRED_ID}"));
+ }, 10)
+ ),
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+
+ error
+
+
+ }
+ children={(resolvedDeferredId) => (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-rejected-no-error-element.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: Promise.reject(new Error("${RESOLVED_DEFERRED_ID}")),
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+
+ export function ErrorBoundary() {
+ return (
+
+ error
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-script-unrejected-no-error-element.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: new Promise(
+ (_, reject) => setTimeout(() => {
+ reject(new Error("${RESOLVED_DEFERRED_ID}"));
+ }, 10)
+ ),
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+
{resolvedDeferredId}
+
+
+ )}
+ />
+
+
+ );
+ }
+
+ export function ErrorBoundary() {
+ return (
+
+ error
+
+
+ );
+ }
+ `,
+
+ "app/routes/deferred-manual-resolve.tsx": js`
+ import { Suspense } from "react";
+ import { defer } from "@remix-run/node";
+ import { Await, Link, useLoaderData } from "@remix-run/react";
+ import Counter from "~/components/counter";
+
+ export function loader() {
+ global.__deferredManualResolveCache = global.__deferredManualResolveCache || {
+ nextId: 1,
+ deferreds: {},
+ };
+
+ let id = "" + global.__deferredManualResolveCache.nextId++;
+ let promise = new Promise((resolve, reject) => {
+ global.__deferredManualResolveCache.deferreds[id] = { resolve, reject };
+ });
+
+ return defer({
+ deferredId: "${DEFERRED_ID}",
+ resolvedId: new Promise(
+ (resolve) => setTimeout(() => {
+ resolve("${RESOLVED_DEFERRED_ID}");
+ }, 10)
+ ),
+ id,
+ manualValue: promise,
+ });
+ }
+
+ export default function Deferred() {
+ let { deferredId, resolvedId, id, manualValue } = useLoaderData();
+ return (
+
+
{deferredId}
+
+
fallback }>
+ (
+
+ )}
+ />
+
+ manual fallback}>
+
+ error
+
+
+ }
+ children={(value) => (
+
+
{JSON.stringify(value)}
+
+
+ )}
+ />
+
+
+ );
+ }
+ `,
+ },
+ });
+
+ // This creates an interactive app using puppeteer.
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test.afterAll(() => {
+ appFixture.close();
+ });
+
+ test("works with critical JSON like data", async ({ page }) => {
+ let response = await fixture.requestDocument("/");
+ let html = await response.text();
+ let criticalHTML = html.slice(0, html.indexOf("