SSR: pipeThrough(new TextEncoderStream()) crashes Chrome Mobile iOS / WKWebView apps (RangeError: Maximum call stack size exceeded)
Environment
- React Router: 8.x (SSR,
<Scripts />component) - Affected browsers: Chrome Mobile iOS, Google Search app, and any other WKWebView-based browser on iOS
- Unaffected: Safari iOS, all desktop browsers, Android Chrome
Symptom
Every SSR page load throws:
RangeError: Maximum call stack size exceeded
at Tk (chrome-extension://…)
at Rk (chrome-extension://…)
at Tk (chrome-extension://…)
at Rk (chrome-extension://…)
…
Tk/Rk are minified symbols. They are not WebKit JS builtins — WebKit uses full English names like readableStreamPipeTo. They appear to come from Chrome's injected WKUserScript running in page context.
Root cause
React Router 8's <Scripts /> component emits this inline script on every SSR page load:
window.__reactRouterContext.stream = new ReadableStream({
start(controller) {
window.__reactRouterContext.streamController = controller;
}
}).pipeThrough(new TextEncoderStream());
Chrome Mobile iOS (and other WKWebView-based browsers) inject minified JavaScript into every page via WKUserScripts at documentStart. These scripts appear to hook into one or both of ReadableStream.prototype.pipeThrough and TextEncoderStream, causing Tk ↔ Rk mutual recursion that overflows WKWebView's restricted JS call stack.
We cannot determine with certainty whether pipeThrough or TextEncoderStream is the specific trigger, because Chrome Mobile iOS does not allow Web Inspector connections — we cannot inspect Chrome's injected WKUserScript source or observe pipeThrough.toString() in Chrome's context. What we can confirm:
- The crash occurs in the React Router SSR inline script (it is the first script executed on page load)
- Only WKWebView-based browsers are affected; Safari is not
- Removing both
pipeThroughandTextEncoderStreamfrom the inline script eliminates the crash entirely
The relevant line is the contextScript in lib/dom/ssr/components.js inside Scripts → useMemo → initialScripts.
Impact
This is not a browser crash — the WKWebView process survives. But the consequence is severe:
- Inline context script throws
RangeError→ aborts window.__reactRouterContext.streamis never assignedwindow.__reactRouterContext.streamControlleris undefined- Subsequent inline
<script>tags fromStreamTransfercallstreamController.enqueue(...)→TypeError decodeViaTurboStreamwaits for data that never arrives- App does not hydrate — user sees a broken, non-interactive page
This affects all React Router 8 SSR apps using <Scripts /> on Chrome iOS, regardless of whether they use defer() or streaming loaders — the pipeThrough / TextEncoderStream call is unconditional.
Proposed fix
Replace pipeThrough(new TextEncoderStream()) with a manual TextEncoder wrapper inside the ReadableStream start handler. This eliminates both pipeThrough and TextEncoderStream from the inline script. Output is functionally identical (ReadableStream<Uint8Array>, same enqueue/close/error API for decodeViaTurboStream):
// Before
window.__reactRouterContext.stream = new ReadableStream({
start(controller) {
window.__reactRouterContext.streamController = controller;
}
}).pipeThrough(new TextEncoderStream());
// After
window.__reactRouterContext.stream = new ReadableStream({
start(controller) {
var enc = new TextEncoder();
window.__reactRouterContext.streamController = {
enqueue: function(s) { controller.enqueue(enc.encode(s)); },
close: function() { controller.close(); },
error: function(e) { controller.error(e); }
};
}
});
We have verified at scale (~144M affected page loads/month across our RR8 SSR fleet) that this eliminates the crash, and that decodeViaTurboStream operates correctly with the patched stream.
Workaround
Apply as a pnpm patch (patchedDependencies) or equivalent until a fix lands in a React Router release.
Source: remix-run/react-router