Skip to content

Bug: [19.2.0] Streaming scripts injected in onAllReady in renderToPipeableStream and prerenderToNodeStream #34966

@jereaa

Description

@jereaa

When using renderToPipeableStream with onAllReady, while also using lazy loading components with React.lazy, React is injecting streaming scripts and hidden HTML elements into the resulting HTML.

React recommends using onAllReady for crawlers, like SEO crawlers, logically, but this new behaviour injects scripts and hidden HTML elements, impacting negatively in SEO scores.

References:

This same issue also occurs with the prerenderToNodeStream API

React version: 19.2.0
React DOM version: 19.2.0

Steps To Reproduce

Show steps to reproduce 1. Create a component to lazy load later in our app. Be sure to bloat it with attributes like data attributes
// Button.js import { createElement } from "react"; const Button = () => createElement( "button", { type: "button", "data-test-button-value1": "some-value-for-test", "data-test-button-value2": "some-value-for-test", "data-test-button-value3": "some-value-for-test", "data-test-button-value4": "some-value-for-test", "data-test-button-value5": "some-value-for-test", "data-test-button-value6": "some-value-for-test", "data-test-button-value7": "some-value-for-test", "data-test-button-value8": "some-value-for-test", "data-test-button-value9": "some-value-for-test", "data-test-button-value10": "some-value-for-test", "data-test-button-value11": "some-value-for-test", "data-test-button-value12": "some-value-for-test", "data-test-button-value13": "some-value-for-test", "data-test-button-value14": "some-value-for-test", "data-test-button-value15": "some-value-for-test", "data-test-button-value16": "some-value-for-test", "data-test-button-value17": "some-value-for-test", "data-test-button-value18": "some-value-for-test", "data-test-button-value19": "some-value-for-test", "data-test-button-value20": "some-value-for-test", }, "Test" ); export default Button;
  1. Create App Component and lazy load Button component. Be sure to bloat Head tag to trigger bug.
// App.js import { createElement, lazy, Suspense } from "react"; const TEST_DATA = Object.fromEntries( Array.from({ length: 200 }, (_, i) => [`key${i}`, "x".repeat(99)]) ); const LazyButton = lazy(async () => import("./Button.mjs")); const App = () => createElement( "html", null, createElement( "head", null, createElement("script", null, `console.log(${JSON.stringify(TEST_DATA)})`) ), createElement( "body", null, createElement( Suspense, { fallback: createElement("h1", null, "Loading...") }, createElement(LazyButton) ) ) );
  1. Serve the app via renderToPipeableStream and onAllReady
import http from "node:http"; import { renderToPipeableStream } from "react-dom/server"; http .createServer(function (req, res) { const { pipe } = renderToPipeableStream(App, { onAllReady: () => { res.setHeader("content-type", "text/plain"); pipe(res); }, }) .listen(8080);

Same thing happens with prerenderToNodeStream

Scripts and hidden elements injected

Bloated Head script and Button data attributes removed for better readability.

<!DOCTYPE html> <html> <head></head> <body> <!--$?--><template id="B:0"></template> <h1>Loading...</h1> <!--/$--> <script id="_R_"> requestAnimationFrame(function () { $RT = performance.now(); }); </script> <div hidden id="S:0"> <button type="button" data-test-button-value1="some-value-for-test" > Test </button> </div> <script> $RB = []; $RV = function (a) { $RT = performance.now(); for (var b = 0; b < a.length; b += 2) { var c = a[b], e = a[b + 1]; null !== e.parentNode && e.parentNode.removeChild(e); var f = c.parentNode; if (f) { var g = c.previousSibling, h = 0; do { if (c && 8 === c.nodeType) { var d = c.data; if ("/$" === d || "/&" === d) if (0 === h) break; else h--; else ("$" !== d && "$?" !== d && "$~" !== d && "$!" !== d && "&" !== d) || h++; } d = c.nextSibling; f.removeChild(c); c = d; } while (c); for (; e.firstChild; ) f.insertBefore(e.firstChild, c); g.data = "$"; g._reactRetry && requestAnimationFrame(g._reactRetry); } } a.length = 0; }; $RC = function (a, b) { if ((b = document.getElementById(b))) (a = document.getElementById(a)) ? ((a.previousSibling.data = "$~"), $RB.push(a, b), 2 === $RB.length && ("number" !== typeof $RT ? requestAnimationFrame($RV.bind(null, $RB)) : ((a = performance.now()), setTimeout( $RV.bind(null, $RB), 2300 > a && 2e3 < a ? 2300 - a : $RT + 300 - a )))) : b.parentNode.removeChild(b); }; $RC("B:0", "S:0"); </script> </body> </html>

Link to code example: https://codesandbox.io/p/devbox/wghlmy

The current behavior

Streaming scripts and hidden elements in the resulting HTML when using onAllReady in renderToPipeableStream and prerenderToNodeStream.

The expected behavior

Same as in React 19.1.1. No streaming scripts or hidden elements in the resulting HTML.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Status: UnconfirmedA potential issue that we haven't yet confirmed as a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions