- Notifications
You must be signed in to change notification settings - Fork 49.7k
Description
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:
- https://prerender.io/blog/hidden-content-that-hurts-seo/
- https://wolf-of-seo.de/en/what-is/hidden-content-2/
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;- Create
AppComponent 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) ) ) );- Serve the app via
renderToPipeableStreamandonAllReady
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.