From db26cbf95bb76d9cccf843eb3b8dc1e32d3ea832 Mon Sep 17 00:00:00 2001 From: Giorgio Garasto Date: Thu, 25 Apr 2024 18:04:21 +0200 Subject: [PATCH] =?UTF-8?q?perf:=20enable=20remix=20single=20fetch=20+=20n?= =?UTF-8?q?ative=20web=20streams=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entry.server.tsx | 45 ++++++++++++++++++++++++++++++++++++++------ env.d.ts | 4 ++++ tsconfig.json | 7 ++++++- vite.config.ts | 9 ++++++++- 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 4f78e9f..207dbb3 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -1,4 +1,5 @@ -import { renderToString } from 'react-dom/server'; +// Use the server.browser package to be able to use renderToReadableStream also on Node.js/Bun +import { renderToReadableStream } from 'react-dom/server.browser'; import { RemixServer } from '@remix-run/react'; import type { EntryContext } from '@remix-run/node'; import { @@ -16,6 +17,9 @@ import PageStyles from './src/PageStyles'; import i18next, { localesDirectory } from './i18next.server'; import i18n from './i18n'; +// Reject all pending promises from handler functions after 5 seconds +export const streamTimeout = 5000; + export default async function handleRequest( request: Request, responseStatusCode: number, @@ -45,18 +49,47 @@ export default async function handleRequest( - {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} - + ); } - // Render the component to a string. - const html = renderToString(); + const controller = new AbortController(); + let timeout = setTimeout(() => controller.abort(), streamTimeout * 2); + + const stream = await renderToReadableStream(, { + signal: controller.signal, + }); + try { + await stream.allReady; + clearTimeout(timeout); + } catch (error) { + responseStatusCode = 500; + console.error(error); + } + + // Read the stream to a string + const reader = stream.getReader(); + const decoder = new TextDecoder(); + let html = ''; + await reader.read().then(function processText({ + done, + value, + }: ReadableStreamReadResult): void | Promise { + if (done) { + return; + } + html += decoder.decode(value); + return reader.read().then(processText); + }); // Grab the CSS from emotion const { styles } = extractCriticalToChunks(html); @@ -77,7 +110,7 @@ export default async function handleRequest( responseHeaders.set('Content-Type', 'text/html'); - return new Response(`${markup}`, { + return new Response(markup, { status: responseStatusCode, headers: responseHeaders, }); diff --git a/env.d.ts b/env.d.ts index 8d2f951..2d03aeb 100644 --- a/env.d.ts +++ b/env.d.ts @@ -1,2 +1,6 @@ /// /// + +declare module 'react-dom/server.browser' { + export * from 'react-dom/server'; +} diff --git a/tsconfig.json b/tsconfig.json index 269c0cc..af422c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,10 @@ { - "include": ["env.d.ts", "**/*.ts", "**/*.tsx"], + "include": [ + "env.d.ts", + "**/*.ts", + "**/*.tsx", + "node_modules/@remix-run/react/future/single-fetch.d.ts" + ], "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2022"], "isolatedModules": true, diff --git a/vite.config.ts b/vite.config.ts index 815f1dd..e9be8fc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,7 +6,14 @@ import tsconfigPaths from 'vite-tsconfig-paths'; installGlobals({ nativeFetch: true }); export default defineConfig(({ mode }) => ({ - plugins: [remix(), tsconfigPaths()], + plugins: [ + remix({ + future: { + unstable_singleFetch: true, + }, + }), + tsconfigPaths(), + ], build: { sourcemap: true, rollupOptions: {