Build1 distinct publisher3 min readPublished
Code-splitting thirteen pages with React.lazy left all seventeen prerendered files at 12.6kB of header and footer. Hydration rendered the missing content in the browser, so nothing threw and the byte count was the only tell.
The Engineer · Build desk
build
A build-tool swap that took 70 days and 166 files, and the build was the easy part1 distinct publisher
build
Chess's one-game-a-day cap is a two-core bill, and WebAssembly moves it onto your laptop1 distinct publisher
build
NocoBase documents an AI kernel role for itself thirteen alphas before the 3.0 announcement1 distinct publisher
build
A strict CSP killed one form in production, and every monitor stayed green1 distinct publisher
Compiled by The EngineerSomething wrong?How this is made
The expensive part, per the author's write-up on dev.to, was that the obvious theory survived the obvious experiment. Resolve every page's dynamic import up front, await all of them, then render: identical 12.6kB output, still green [6]. At that point the module is loaded, awaited and resolved before render starts, which rules out the tidy explanation that it is not loaded yet [6].
React.lazy never asks whether the module is available. It keeps its own status field, and only its own initialiser advances it [7]. On the first render it sees Uninitialized, calls your loader, takes back a promise, marks the component Pending, attaches a .then to write the result in, and suspends [8]. An already-resolved promise still does not invoke that .then synchronously; it schedules a microtask that runs once the current synchronous execution finishes [9]. renderToString finishes nothing mid-render. It runs top to bottom, never yields, sees Pending, writes the fallback and returns, all before the callback that would have marked the component Resolved gets a turn [10]. The exit code was accurate as far as it went: renderToString did return a string.
So the thing to check before assuming this transfers is which renderer your build script calls, not the React version. Under renderToPipeableStream the same lazy components are fine, because a streaming renderer can wait; under renderToString, that status belongs to React's initialiser alone, and your code never gets a turn to write it [11].
The fix stops delegating that state. Loaded is a plain variable the .then assigns, preload is memoised with `pending ??= load()`, and the component calls `use(preload())` only when Loaded is still null [12]. That leaves two states with no microtask between them. Loaded set, and rendering is an ordinary synchronous function call that never suspends, which is the path the build takes every time; Loaded null, and use() suspends exactly as lazy would, which is what you want when a browser navigates to a page it has not fetched [13].
What you buy is a registry you now maintain. Every route pushes its own preload into the list the build awaits [12], and a route nobody registers will not fail loudly: it takes the null branch during the build, suspends, and writes a fallback into a green run [13][10]. That is the same failure with a smaller blast radius, and this time the variable is yours, so a build script can read it.
Seventeen files, one byte count [1][4]. Seventeen times 12.6kB is 214.2kB of HTML written without a page body in any of it [14]. In my context the check worth adding is not a byte floor, because the next footer edit lifts the floor and the assertion quietly starts passing again; it is a per-route string that only that page's body can produce, checked against the file written for that path [1].
Ranked by verification strength, evidence, and original report placement.
A React 19 site prerenders every route to static HTML at build time: thirteen page components, seventeen URLs, renderToString called in a build script, one HTML file written per path.
Before code splitting, all thirteen pages were static imports in a single bundle, so someone opening the privacy policy downloaded the pricing comparison table, six blog posts and the sign-in form; none of it was reachable from that page and none could be dropped, because a static import is a promise the module is present before the first line runs.
After the pages were switched to React.lazy, the build stayed green, every page loaded correctly in the browser, and nothing threw.
It took the author most of a day to notice that every prerendered file was 12.6kB, and that the 12.6kB was the header and the footer with nothing in between.
The pages looked fine in a browser because React hydrated and rendered the content client-side, exactly as it would have without any prerendering, so the only symptom was that the build step added specifically to produce static content had quietly stopped producing it, with no error, no warning and correct-looking output.
Resolving every page's dynamic import up front, awaiting all of them with Promise.all, and only then calling renderToString produced identical output: still 12.6kB, still green.
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · September 1, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
One developer's build, but a mechanism you can check yourself
The whole account rests on a single first-person post: one project, one afternoon, one set of file sizes nobody else has seen. What raises it above anecdote is that the causal chain does not require trusting the author — promise callbacks are microtasks, renderToString never yields, and both the broken and working code are printed in full. What is missing is ordinary corroboration: no repository to clone, no second project reporting identical shells, no React maintainer confirming that lazy's status machine behaves as described.
No read on how many projects sit in this trap
Nothing in this reporting speaks to spread. It is one private landing-page template; there are no download counts, no issue threads, no other teams describing 12.6kB shells, and no indication of how common the renderToString-plus-React.lazy combination actually is. Treat it as a reproducible mechanism rather than a measured population.
Undersold rather than oversold
Developer blog posts usually inflate; this one deflates. The author calls the bug narrow, declines to blame React, and hands over the general shape — a build step whose output the runtime can silently reconstruct — as almost an afterthought. If there is a stretch, it is that closing generalisation reaching further than a single case can carry; the quiet framing also understates how alarming it is that a byte count was the only available signal.
A template to promote, but nothing bent to promote it
The last line is the tell: the work came out of a landing-page template the author is building, and dev.to publishes whatever authors submit, with no editor between draft and page. That is a mild marketing tailwind. It does not visibly distort anything — no product is benchmarked, no competitor is named, and the technical content would read the same if nothing were being sold.
Solid on the why, thin on the where
Confidence splits along a seam. The explanation of why an already-resolved promise cannot help a synchronous render is strong enough to reason from directly, and the fix is complete rather than sketched. The environment is not: React 19 is named but no patch version, no bundler, no router version, and no one has reproduced the 12.6kB result elsewhere. Anyone matching these symptoms should confirm against their own output before adopting the pattern.