Why not React?
Tags: webdev, javascript, performance, react
Published: 2023-09-17 04:22:50

---
title: Why not React?
published: true
description: "REACT FANS DON’T LOOK: A formerly-internal analysis around fitting a ≥44.5kB framework in a 20kB bag."
tags: webdev, javascript, performance, react
series: Backstreets
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-07-16 13:37 +0000
---

This was an internal analysis I wrote when challenged if Kroger.com could do the same MPA speed tricks as [my Marko demo](https://dev.to/tigt/so-what-c8j), but still using React because, well, you know.[^1]

[^1]: Men would rather ignore [20% of the US](https://www.usac.org/lifeline/resources/program-data/) than stop using React.[^2]

[^2]: Or [50% of the US, depending on what you count](https://blogs.microsoft.com/on-the-issues/2019/04/08/its-time-for-a-new-approach-for-mapping-broadband-data-to-better-serve-americans/).

It’s from around 2021, so some questions got answered, some predictions got proven wrong or boring-in-retrospect, and React’s official SSR story actually released. I once planned a rewrite, but I’m burnt out and can’t be arsed. (Feel free to tell me what I got wrong in the comments.)

However, I’m publishing it (with some updated hyperlinks) because this post might still be useful to someone:

- [React’s streaming has interesting flaws](https://twitter.com/royalicing/status/1520983370410254336), and [the official RSC implementation ain’t exactly fast](https://calendar.perfplanet.com/2022/mobile-performance-of-next-js-sites/)
- [Low-end devices remain stubbornly stagnant 2 years later](https://www.youtube.com/watch?v=ZKH3DLT4BKw)
- [React indeed says it’s changing in unfamiliar ways](https://addyosmani.com/blog/react-server-components-app-router/)
- The intro became more relevant than I could have possibly known when I wrote it.

Don’t believe me? Well, a WebPageTest is worth a thousand posts:

<table>
  <caption><center><small><em>WebPageTest results for: Simulated 3G, emulated MOTOG4, Los Angeles</em></small></center></caption>
  <thead><tr><td><th>Next.js RSCs + streaming<th>Marko <th>HackerNews (control)</thead>

  <tr><th>URL
    <td><a href="https://next-news-rsc.vercel.sh">next-news-rsc.<wbr>vercel.<wbr>sh</a>
    <td><a href="https://marko-hackernews.ryansolid.workers.dev">marko-hackernews.<br>ryansolid.<wbr>workers.<wbr>dev</a>
    <td><a href="https://news.ycombinator.com">news.<wbr>ycombinator.<wbr>com</a>

  <tr><th>WPT link
    <td><a href="https://www.webpagetest.org/result/230717_BiDcVJ_9GN/1/details/">230717_BiDcVJ_9GN</a>
    <td><a href="https://www.webpagetest.org/result/230717_AiDc35_A1Q/3/details/">230717_AiDc35_A1Q</a>
    <td><a href="https://www.webpagetest.org/result/230717_AiDcNY_A0G/2/details/">230717_AiDcNY_A0G</a>

  <tr><th>JS
    <td align="right">94.9 kB
    <td align="right">0.3 kB
    <td align="right">1.9 kB

  <tr><th>HTML
    <td align="right">9.5 kB
    <td align="right">3.8 kB
    <td align="right">5.8 kB

  <tr><th><code>text/x-component</code>
    <td align="right">111.1 kB
    <td align="right">0 kB
    <td align="right">0 kB

  <tr><th>Waterfall chart
    <td><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zgtw1qp9bc76d5aqkwna.png" width="100">
    <td><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tg05u5yt7c3e1b5keknw.png" width="100">
    <td><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ermt62x32ecyhtlixypf.png" width="100">

</table>

I’m not including timings, since unfortunately WPT seems to have stopped using real Androids, and [Chrome’s CPU throttling is way too optimistic for low-end devices](https://www.youtube.com/watch?v=4bZvq3nodf4&t=617s). However, note that browsers can’t parse `text/x-component` natively[^3], so that parsing is blocked behind JS and the parse time is more punishing on low-end devices than usual.

[^3]: Well, [_one_ browser can natively parse `text/x-component`](https://en.wikipedia.org/wiki/HTML_Components), sorta. I guess RSCs reused the MIME type as an easter egg?

I’m also not sure if I should test [next-edge-demo.netlify.app/rsc](https://next-edge-demo.netlify.app/rsc) instead, but [its results seemed so inconsistent](https://www.webpagetest.org/result/230717_BiDcTQ_B5W/) I wasn’t sure it’s functioning correctly.

Anyway, time for the original analysis.

## Can we get React to be as fast as the Kroger Lite demo?

Probably not.

## Okay smart guy, why not?

This is not going to be a short answer; bear with me. I’m going to start with an abstract point, then back it up with concrete evidence for my pessimism.

----------------------

Code struggles to escape why it was created. **You can often trace the latest version’s strengths and weaknesses all the way back to the goals of the original authors.** Is that because of backwards compatibility? Developer cultures? Feedback loops? Yes, and tons of other reasons. The broader effect is known as [path dependence](https://en.wikipedia.org/wiki/Path_dependence), where the best way to predict a technology’s future is to examine its past.

Front-end frameworks are no exception:

<dl>
  <dt><a href="https://www.offerzen.com/blog/rich-harris-on-why-he-created-svelte">Svelte was invented to embed data visualizations into other web pages.</a>
  <dd>💪 Svelte has first-class transitions and fine-grained updates, because those things are very important for good dataviz.
  <dd>🤕 Svelte’s <a href="https://github.com/sveltejs/svelte/issues/958">streaming nonsupport</a> and <a href="https://gist.github.com/ryansolid/71e2b160df4db33fcca2862355377983">iconoclastic “𝑋 components↦bundle size” curve</a> make sense if you consider the code Svelte was invented for didn’t <b>do</b> those things in the first place.

  <dt><a href="https://en.wikipedia.org/wiki/AngularJS#Development_history">Angular was made to quickly build internal desktop web apps.</a>
  <dd>💪 Angular fans are right about how quickly you can make a functional UI.
  <dd>🤕 If you think about what big-company workstations are like and the open-in-a-tab-all-day nature of intranet desktop webapps, Angular’s performance tradeoffs are entirely rational — until you try using it for mobile.

  <dt><a href="https://www.youtube.com/watch?v=KVZ-P-ZI6W4&t=94s">React was created to stop Facebook’s org chart from Conway’s Law-ing all over their desktop site’s front-end.</a>
  <dd>💪 React has absolutely proven its ability to let teams of any size, any degree of cooperation, and any skill level work together on large codebases.
  <dd>🤕 As for React’s original weaknesses… Well, don’t take my word for it, <a href="https://www.youtube.com/watch?v=KVZ-P-ZI6W4&t=310s">take it from the React team</a>: <blockquote>So people started playing around with this internally, and everyone had the same reaction. They were like, “Okay, A.) I have no idea how this is going to be performant enough, but B.) it’s <em>so</em> fun to work with.” Right? <em>Everybody</em> was like, “This is so cool, I don’t really <em>care</em> if it’s too slow — somebody will make it faster.”</blockquote>

</dl>

That quote explains a lot of things, like:

- React’s historical promises that new versions will make your app faster for you
- The pattern of React websites appointing specialized front-end performance teams
- Why big companies like React, since departmentalizing concerns so other departments don’t worry about them is how big companies _work_

I’m not claiming those things are wrong! I’m saying it’s a _consistent philosophy_. For once, I’m not even being snide about React’s performance; the React team are very open about their strategy of relieving framework consumers from worrying about performance, by having the framework authors do it for them. I even think that strategy works for (most of) Meta’s websites!

But so far, it [has not made websites overall any faster](https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/). And the things the React team have resorted to have gotten odder and more involved. Lastly, **our site (and indeed, most sites) are not very similar to facebook.com**.

{% details 🔮 <b>Note from the future</b> %}
Because it’s the obvious question to ask: [Marko got started when eBay devs wanted to use Node.js, and the business said “Okay, but it can’t regress performance”](https://twitter.com/RyanCarniato/status/1565209384241602560).
💪 Strength: [_It didn’t._](https://calendar.perfplanet.com/2014/async-fragments-rediscovering-progressive-html-rendering-with-marko/) That’s not typical for JS frameworks.
🤕 Weakness: Marko’s early focus on performance instead of outreach/integrations beyond what eBay uses/etc. also explains why most devs haven’t heard of it.
{% enddetails %}

## But can React be that fast _anyway?_ It’s all just code; it can be worked with.

It’s code _and_ path dependencies _and_ culture _and_ a support ecosystem, each with their own values, golden paths, and pewter paths. Let’s examine those concretely — we’ll look at **how feasible _technically_ it would be to have React perform at the same speed as the Kroger Lite demo**.

Starting with what traits are desirable for MPAs:

<dl>

<dt>📃 Streamed HTML
<dd>Incrementally flush parts of the page to the HTTP stream, so pages aren’t as slow as their slowest part.

<dt>🥾 Fast boot
<dd>If JS reboots on every navigation, it should do so quickly.

<dt>🥀 Hydration correctness
<dd>Like airplane takeoff, hydration is a few moments where any of a hundred tiny things could ruin the rest of the trip.
<dd>In MPAs, it’s vital to reconcile DOM updates from user input during load, as that “edge case” becomes a repeat occurrence.

<dt>🏸 Fast server runtime
<dd>If we’re leaning on the server, it better render efficiently.
<dd>Even more important for spinning up distributed datacenter instances, edge rendering, <a href="https://developers.google.com/web/updates/2018/05/beyond-spa">Service Worker rendering</a>, and other near-user processors.

<dt>🍂 Tree-shakeable framework
<dd>SPAs assume use of all of framework’s features <i>eventually</i>, so they bundle them to get into JS engines’ caches early. MPAs want to remove code from the critical path if it’s not used in it, amortizing framework cost across pages.

<dt>🧠 Multi-page mental model
<dd>If a component only renders on the server, should you pretend it’s in the DOM?
<dd>If you can’t have API parity between server and client, provide clear and obvious boundaries.

</dl>

### 📃 Streamed HTML

I consider streaming HTML to be **the** most important thing for consistently-high-performance server rendering. If you didn’t read [the post about it](https://dev.to/tigt/the-weirdly-obscure-art-of-streamed-html-4gc2), here’s the difference it makes:

<figure>
<video title="Head-to-head recording of two web pages with identical content, but loaded differently." src="https://assets.codepen.io/183091/HTML+streaming+vs.+non.mp4" muted controls crossorigin>
<track kind="descriptions" src="https://assets.codepen.io/183091/html-streaming.vtt" srclang="en-us" label="Text description">
<a href="https://assets.codepen.io/183091/HTML+streaming+vs.+non.mp4">Download video: HTML Streaming vs. Non-streaming</a>
</video>

<figcaption><a href="https://assets.codepen.io/183091/html-streaming.vtt">Here’s the closed captions</a>, since Forem doesn’t allow <code>&lt;track&gt;</code> elements.</figcaption>
</figure>

#### Important facets of incremental HTML streaming

- **Both explicit _and_ implicit flushes;** early `<head>` for asset loading, around API calls, at TCP window size boundaries…
- All flushes, but especially implicit ones, should **avoid too much chunking**: overeager flushing defeats compression, inflates HTTP encoding overhead, bedevils TCP scheduling/fragmentation, and hitches Node’s event loop.
- **Nested component flushes** help avoid contorting code to expose flush details at the renderer’s top level.
- **Out-of-order server rendering**, for when APIs don’t return in the same order they’re used.
- **Out-of-order flushes** , [so inessential components don’t hold up the rest of the page (like Facebook’s old BigPipe)](https://engineering.fb.com/2010/06/04/web/bigpipe-pipelining-web-pages-for-high-performance/).
- **Controlling render dependencies of nested and out-of-order flushes** is important to prevent displaying funky UI states. 
- **Mid-stream render errors** should finish cleanly without wasting resources, and emit an `error` event on the stream so [the HTTP layer can properly signal the error state](https://github.com/marko-js/community/pull/1).
- **Chunked component hydration**, so component interactivity matches component visibility.

Marko has been optimizing those important subtleties for almost a decade, and React… hasn’t.

Additionally, we had a brownfield tax. Kroger.com didn’t render its React app to a stream, so that app had many stream incompatibilities of the kind described here:

> Generally, any pattern that uses the server render pass to generate markup that needs to be added to the document _before_ the SSR-ed chunk will be fundamentally incompatible with streaming.
>
> —[What’s New With Server-Side Rendering in React 16 § Streaming Has Some Gotchas](https://hackernoon.com/whats-new-with-server-side-rendering-in-react-16-9b0d78585d67)

### 🥾 Fast boot

SPAs’ core tradeoff: the first page load can be painful in order for fast future interactions. But in MPAs, _every_ page load must be fast.

#### Costs in JS runtime boot

1. Download time
2. Parse time & effort
3. Compilation: compile time, bytecode memory pressure, and JIT bailouts
4. Execution (repeats every page, regardless of JIT caching)
5. Initial memory churn/garbage collection

Only some of those costs can be skipped on future page loads, with the difficulty increasing as you go down:

1. Downloads are skipped with HTTP caching.
2. Modern browsers are smart enough to background and cache parses, but not for all `<script>`s — either from script characteristics or parallelization limits.
3. Compiler caches intentionally require multiple executions to store an entire script, and compilation bailouts can thrash for a surprisingly long time.
4. Execution can never be _skipped_, but warm JIT caches and stored execution contexts can slash properly-planned runtimes.
5. Memory churn and overhead during load is impossible to avoid — intentionally by the ECMAScript standard, even.

Luckily, [v8 moved most parses to background threads](https://v8.dev/blog/cost-of-javascript-2019). Unfortunately, while that doesn’t block the main thread, parsing still has to _finish_. This is exacerbated by Android devices’ [big.LITTLE architecture](https://en.wikipedia.org/wiki/ARM_big.LITTLE), where the available LITTLE cores are much slower than the cores already occupied with core browser tasks in the main, network, and compositor threads.

More on v8’s JS costs and caching, as it’s the primary target for low-spec Androids and JavaScript servers alike:

- [The cost of JavaScript in 2019](https://v8.dev/blog/cost-of-javascript-2019)
- [Code caching for JavaScript developers](https://v8.dev/blog/code-caching-for-devs)
- [Improved code caching](https://v8.dev/blog/improved-code-caching)

#### Does React play nice with JS engines’ boot process?

Remember React’s demos back in the day that got interactive much faster than competing frameworks? React’s lazier one-way data flow was the key, as it didn’t spend time building dependency graphs like most of its peers.

Unfortunately, that’s the only nice thing I found about React and JS engines.

- The sheer [size of `react` + `react-dom`](https://bundlephobia.com/scan-results?packages=react,react-dom) negatively affects every step of JS booting: parse time, going to ZRAM on cheap Androids, and eviction from compiler caches.

- React components are functions or classes with big `render` methods, which defeats eager evaluation, duplicates parse workload via [lazy compilation](https://v8.dev/blog/preparser), and frustrates code caching:

  > One caveat with code caching is that it only caches what’s being eagerly compiled. This is generally only the top-level code that’s run once to setup global values. Function definitions are usually lazily compiled and aren’t always cached.
  >
  > —[JavaScript Start-up Performance](https://medium.com/reloading/javascript-start-up-performance-69200f43b201#5aaa)

- React prioritizes stable in-page performance at the expense of reconciliation and memory churn at boot.

- React renders and VDOM results are infamously [megamorphic](https://erdem.pl/2019/08/v-8-function-optimization), so JS compilers waste cycles optimizing and bailing out.

- React’s synthetic event system slows event listener attachment and makes early user input sluggish each load.

#### Rehydration performance

Rehydration spans all of the above considerations for JS boot, and was the primary culprit in our performance traces. You can’t ignore rehydration costs for the theoretical ideal React MPA.

> Performance metrics collected from real websites using SSR rehydration indicate its use should be heavily discouraged. Ultimately, the reason comes down to User Experience: it’s extremely easy to end up leaving users in an “uncanny valley”.
>
> —[Rendering on the Web § A Rehydration Problem: One App for the Price of Two](https://developers.google.com/web/updates/2019/02/rendering-on-the-web#rehydration)

Thus, faster rehydration is almost as common as incremental HTML in the React ecosystem:

- [Strategies for server-side rendering of asynchronously initialized React.js components](https://stackoverflow.com/questions/25983001/strategies-for-server-side-rendering-of-asynchronously-initialized-react-js-comp)
- [Why React Server § Streaming client initialization](https://react-server.io/docs/intro/why-react-server)
- [next-super-performance](https://github.com/LukasBombach/next-super-performance) — [The case of partial hydration (with Next and Preact)](https://medium.com/@luke_schmuke/how-we-achieved-the-best-web-performance-with-partial-hydration-20fab9c808d5)
- [react-lightyear](https://github.com/Ephem/react-lightyear)

Along with their own caveats, each implementation runs into the same limitations from React itself:

- Mandatory `[data-reactroot]` wrappers hurt DOM size and reflow time.

- React listens for events on each render root, which increases memory usage and slows event handling since previous `===` invariants are no longer guaranteed.

- The Virtual DOM imposes a lot of rehydration overhead:
    1. Render the entire component tree
    2. Read back the existing DOM
    3. Diff the two
    4. Render the reconciled component tree

    That’s a lot of work to show something nigh-identical to when you started!

### 🥀 Rehydration correctness

Cursory research turns up _a lot_ of folks struggling to hand off SSR’d HTML to React:

- [Why Server Side Rendering In React Is So Hard](http://farmdev.com/thoughts/107/why-server-side-rendering-in-react-is-so-hard/)
- [The Perils of Rehydration](https://joshwcomeau.com/react/the-perils-of-rehydration/)
- [Case study of SSR with React in a large e-commerce app](https://blog.jakoblind.no/case-study-ssr-react/)
- [Fixing Gatsby’s rehydration issue](https://blog.logrocket.com/fixing-gatsbys-rehydration-issue/)
- [gatsbyjs#17914: [Discussion] Gatsby, React & Hydration](https://github.com/gatsbyjs/gatsby/issues/17914)
- [React bugs for “Server Rendering”](https://github.com/facebook/react/issues?q=is%3Aissue+label%3A"Component%3A+Server+Rendering")

No, really, skim those links. The nature of their problems strongly suggests that React was not designed for SSR, and thus uniquely struggles with it. If you think that’s an opinion, consider the following:

- React handles [intentional differences between client and server render](https://medium.com/the-svt-tech-blog/react-suspense-and-server-rendering-da3711b2a26a#907a) about as ungracefully as possible.

- If we use React for regions of more-complex interactivity throughout a page, what’s the best way to handle the rest of the page?

  - Is it a pain to share state/props/etc. across multiple React “islands” on a page? Do Hooks behave oddly if you do that?

  - Can we use Portals to get around this? (Note Portals don’t SSR.)

- React’s error handling when rehydrating is… nonexistent. By default, it rejects showing any errors to the user in favor of flashing content or tearing down entire DOM trees into blank nodes.

- > React 16 doesn’t fix mismatched SSR-generated HTML attributes
  >
  > —[What’s New with Server-Side Rendering in React 16 § Gotchas](https://hackernoon.com/whats-new-with-server-side-rendering-in-react-16-9b0d78585d67)

  That’s… kind of a big deal.

- Loses interaction state like focus, selection, `<details>`, edited `<input>`s on hydration. Losing users’ work is maddening in the best case, and this issue is magnified over slow networks/bad reception/low-spec devices. 

- Has issues with controlled elements [when the boot process catches up and event listeners fire all at once](https://twitter.com/dan_abramov/status/1200119993712726016). Note the above demo is using future Suspense APIs to solve a problem all React apps can fall into today.

### 🏸 Fast server runtime

Server-side optimizations for React are more common than anything else in this analysis:

- [Rax](https://rax.js.org/)
- [react-ssr-optimization](https://github.com/walmartlabs/react-ssr-optimization)
- [ESX](https://www.nearform.com/blog/speeding-up-react-ssr-announcing-esx/)
- [react-ssr-error-boundary](https://github.com/zekchan/react-ssr-error-boundary)
- [React suspense and server rendering § So what’s the catch?](https://medium.com/the-svt-tech-blog/react-suspense-and-server-rendering-da3711b2a26a#69e4)
- …and a million others

<small>(The cynical takeaway is that because developers have to pay for React’s inefficiencies on servers, they are directly incentivized to to fix them, as opposed to inefficiences on clients.)</small>

- Isomorphic rendering is not a helpful abstraction for tweaking performance between server-side vs. client-side — web applications often end up CPU-bound on arbitrarily-slow user devices, but memory-bound on servers with resources split between connections.

- A fast, efficient runtime can double as [Service Worker rendering to streams](https://developers.google.com/web/updates/2018/05/beyond-spa) for offline rendering, without needing to ship heavier CSR for things that don’t need it.

  Unfortunately, almost all prior art for optimizing React server render involves caching output, which won’t help for Service Workers, EdgeWorkers, cloud functions, etc. So the suggested “trisomorphic rendering” pattern (which the demo bet on for offline) is probably a no-go with React.

### 🍂 Tree-shakeable runtime

Omitting code saves load time, memory use, and evaluation cost — including on the server! Svelte’s “disappearing framework” philosophy would be the logical conclusion of a tree-shakeable runtime — or maybe its reduction to absurdity, for [Svelte’s pathological cases](https://github.com/feltcoop/why-svelte#the-bundle-size-inflection-point).

[Facebook has considered modularizing React, but didn’t conclude it was a win for how they use it.](https://github.com/facebook/react/issues/16173) They also experimented with [an event system that tree-shook events you didn’t listen for](https://github.com/facebook/react/issues/15257), but abandoned it as well.

In short: nope.

### 🧠 Multi-page mental model

<!-- https://svelte.dev/blog/virtual-dom-is-pure-overhead#It_s_not_just_the_diffing_though
https://docs.google.com/presentation/d/1PUvpXMBEDS45rd0wHu6tF3j_8wmGC6cOLtOw2hzU-mw/edit#slide=id.g33fc23c0b5_0_0
https://docs.google.com/presentation/d/1PUvpXMBEDS45rd0wHu6tF3j_8wmGC6cOLtOw2hzU-mw/edit#slide=id.g60d3ee4d75_0_116 -->

The most extreme MPA mental model probably belongs to Ruby on Rails. Rails wholly bets on server-side rendering, and even abstracts its JavaScript APIs to resemble its HTTP-invoked Controller/Model/View paradigm.

At the other end, you have React:

- JSX prefers properties to HTML attributes
- Server-side React pretends to be in a browser
- The ecosystem strives to imitate DOM features on the server 
- Differences between server and browser renders are considered failures of isomorphic JavaScript that should be fixed

Pretending the server is a browser makes sense if you only use HTML as a fancy skeleton screen, and hydration is only a hiccup at the start of long sessions. But that abstraction gets leakier and more annoying the more times the original web page lifecycle occurs. Why bother with the `className` alias if markup may only ever render as `class`? Why treat SSR markup as a tree when it’s only ever a stream?

- React plus forms (and other DOM-stored state) is annoying, controlled or not — will that be painful when we lean more on native browser features?

  - React’s special handling of `.defaultValue` and `.defaultChecked` vs. `.value` and `.checked` can get very confusing across SSR-only vs. CSR-only vs. both

  - React’s `onChange` pretending to be the `input` event, and the resulting bugs

- Is it more likely we’d persist SPA habits that are MPA antipatterns if we continue with React?

- Using another language/template system/whatever for non-React bits seems not ideal, but that’s how it’s been done for years anyway — [especially since React straight-up can’t handle the outer scaffold of a web page](https://medium.com/@dan_abramov/two-weird-tricks-that-fix-react-7cf9bbdef375#486f).

- React’s abstractions/features are designed to work at runtime

  - Even without a build step, which is an impedance mismatch for apps with build steps. Not an intractable problem, but one with ripple effects that disfavor it.

  - Leads to unusual code that JS engines haven’t optimized, such as [the infamous thrown `Promise`](https://stackoverflow.com/questions/59791769/what-is-the-react-official-position-for-throwing-promises-inside-render-function), or Hooks populating a call index state graph via multiple closures.

- Many new React features don’t currently work on the server, with vacillating or unclear timeframes for support:

  - [`.lazy()` and Suspense](https://reactjs.org/docs/code-splitting.html#reactlazy) _[EDITOR’S NOTE: yes I know they do now]_

  - Portals

  - [Error Boundaries](https://asmagin.com/2019/01/18/catching-server-side-rendering-errors-in-react-jss-apps/)

  - How long does it usually take for React to bring client APIs to server parity? That lag may forecast similar problems in the future.

Speaking of problems in the future…

## What about where React is going?

<!-- 
https://dev.to/pomber/about-react-suspense-and-concurrent-mode-21aj
https://reactjs.org/docs/concurrent-mode-adoption.html#what-to-expect
https://reactjs.org/docs/concurrent-mode-adoption.html#who-is-this-experimental-release-for
https://reactjs.org/docs/concurrent-mode-intro.html
https://github.com/sw-yx/concurrent-react-notes
https://reactjs.org/blog/2019/11/06/building-great-user-experiences-with-concurrent-mode-and-suspense.html
https://github.com/reduxjs/react-redux/issues/1177
https://github.com/facebook/react/issues/16938#issuecomment-536207871

[In the future, React.Suspense will enable progressive hydration](https://twitter.com/dan_abramov/status/1200111677833973760)

###### React’s priorities and how they hint at its future

Software rarely escapes the desires that lead to its creation:

- Svelte has first-class transitions and fine-grained updating because it was made for embeddable data visualizations. It’s not great at dynamic SSR, knitting together many APIs into one page, or nailing a local bundle size minima with multiple dynamic components.

- Angular was built to help Google engineers quickly build internal desktop web apps. As a result, Angular was bad at content-focused sites and mobile performance.

- > Meteor was really bad with large data sets. Like you said above it’s a flaw in how it was designed from the beginning. It was built for quick updates and you basically had a database in the browser. That was fine for small data sets but eventually as the databases got larger most people didn’t understand why their pages got slow. They just knew that they did some magic and the data would appear on the client.
  >
  > Meteor really was magic for its wheelhouse but as soon as you had to eject and do something else it was painful and most of the developers didn’t understand enough of the classic ways of doing things to understand the problem

React was created to stop independent teams from accidentally tanking client-side render performance with simultaneous widget updates. Remember when React didn’t support SVG and server-side rendering for years, because Facebook didn’t use them?

Dan Abramov has repeatedly talked about how React’s reconciler is intentionally naïve about fine-grained data changes, because usually bailing out early and swapping large parts of DOM trees is more efficient for user interfaces. -->

As we’ve seen, there’s much prior art of features desirable for fast SSR: incremental HTML streams, in-page patching for out-of-order rendering, omitting JS for components that never rerender on the client, compiling to a more efficient output target for the server, SSRing React APIs that otherwise aren’t, etc.

There is no reason to think that these individual upgrades are inherently incompatible with each other — with [enough glue code](https://github.com/Ephem/react-lightyear#does-reactlazy-work-with-lightyear), debugging, and linting, React theoretically _could_ have the rendering tricks I found useful in Marko.

But we’ve already gotten a taste of future compat problems with our React SSR relying on deprecated APIs. What about React’s upcoming APIs? Do we want to redo our SSR optimizations with every major version?

This risk is made more annoying by [the upcoming APIs’ expected drawbacks](https://reactjs.org/docs/concurrent-mode-adoption.html#what-to-expect):

> In our experience, code that uses idiomatic React patterns and doesn’t rely on external state management solutions is the easiest to get running in the Concurrent Mode.

This makes me worry that the previously-mentioned possibility of a state manager to synchronize multiple React “islands” will be mutually exclusive with Concurrent Mode. Less specifically, I doubt we’d be sticking to “idiomatic React patterns”.

In [the words of a Facebook engineer](https://twitter.com/acdlite/status/1228727481764466688):

> Better comparison is to a hard fork where we don’t maintain any backwards compatibility at all. React itself might be the sunk cost fallacy.

Essentially, future React will be different enough to break many of the reasons companies rely on it today:

- Knowledge gained from experience with the library
- [3rd-party libraries](https://twitter.com/AdamRackis/status/1236460463199850496) (in [deep ways](https://twitter.com/dai_shi/status/1236276385489883137))
- Established patterns

Potentially worse is how Suspense would affect React’s memory consumption. The VDOM roughly triples the DOM’s memory usage (real DOM + incoming VDOM + diffed-against VDOM), and [the “double-buffering” used in Suspense’s examples](https://reactjs.org/docs/concurrent-mode-intro.html#interruptible-rendering) will worsen that. React also considered slimming its synthetic event system, but Concurrent Mode would break without it.

> If you look at how Fiber is written, the architecture truly makes no sense and is unnecessarily complex… except to support Concurrent Mode.
>
> Similar case in design of hooks. If not for concurrency, we woulda just used mutation.
>
> —&thinsp;[Andrew Clark](https://twitter.com/acdlite/status/1228728395564867584)

So Concurrent Mode promises smart scheduling about long JavaScript tasks so React’s cost can be interrupted, but the ways React had to change for Concurrent Mode caused other possible issues:

- Worse raw performance from frequently yielding the main thread
- Garbage collection pressure from Hooks
- Memory consumption from multiple simultaneous React trees
- Risks [“tearing” during hydration](https://gist.github.com/bvaughn/054b82781bec875345bd85a5b1344698)
- Doubling down on synthetic events instead of omitting them for less bundle size
- [Existing patterns that didn’t break before, but will](https://github.com/reactjs/rfcs/pull/147) — often [patterns that libraries needed to eke out performance](https://www.framer.com/motion/component/##performance)

What if, instead of trying to be clever about doing a lot of work in the browser, we instead _did less work_ in the browser? _That’s_ why Kroger Lite is fast. It’s [not just from box-ticking the features I mentioned](https://timkadlec.com/remembers/2023-06-01-performance-is-not-a-checklist/), it’s because its technologies were chosen and app code was written in service of that principle.

----------------------

It may be wise to judge future React not as a migration, but as a completely different framework. Its guarantees, risks, mental model, and benefits are no longer the same. And it really seems to be pushing the depths of framework-level cleverness.

Assume we nail the above; it takes little time to augment React, there are no unforeseen bugs, and teams quickly update components to reap the rewards. We would nevertheless shoulder some drawbacks:

- More painful upgrades to React versions with internal or breaking changes
- Required linting/CI/etc. to ensure React features aren’t used in ways or contexts that would cause problems
- Unknown compatibility issues with ecosystem code like Jest, 3<sup>rd</sup> party components, React DevTools, etc.

Open questions:

- What new rehydration problems will we see with Concurrent Mode, async rendering, and time-slicing?
- Will hook call order remain persistent across React islands, Suspense, deferred updating, different renderers, combinations of all of those, [or scenarios I haven’t anticipated](https://github.com/reactjs/rfcs/blob/84d88774f8edc2210c147572c724fae36d8dc26d/text/0000-anonymous-components.md#motivation)?

## Conclusion

This analysis is harsh on React’s MPA suitability. But is that so odd?

It was created to client-render non-core bits of Facebook. Its maintainers only recently used it for server rendering, navigation, or delivering traditional web content. In fact, [its SSR was a happy accident](https://twitter.com/sebmarkbage/status/1516907614566854659). And finally, [longstanding evidence holds React trends antagonistic towards performance.](https://timkadlec.com/remembers/2020-04-21-the-cost-of-javascript-frameworks/)

_Why **would** React be good at the things we ask it to do?_

With the FB5 redesign, Facebook is finally using React in the ways that we are, [and they have found it wanting](https://twitter.com/dan_abramov/status/1259614150386425858). On the one hand, this means React will surely become much better at desirable SSR features. On the other, _when_ this will happen is unsure, it will heavily change React’s roadmap, and React could change so much that familiarity with how it works today could be a liability rather than a strength.

- For the target audience of rural/new/poorly-connected customers, does Facebook even use React to serve them? Did FB5 change anything, or **does `m.facebook.com` still not use React?**

- If we want a version of Kroger.com as fast as the demo, but **using the same framework, processes, management, and developers as the existing site — wouldn’t that just become our existing site?** We can’t change our personnel, but we _can_ change the technologies we build on.

- Last, but certainly not least: **can you make an industry-beating app out of industry-standard parts?**