I’m thankful to Evan for pointing out perf bugs on beta.reactjs.org. I planned to look later but got nerdsniped. My screenshots are ~averages from ten runs I just did for both sites. (Prone to some variance, of course!) I want to share what I fixed.
The main mistake from the bundle size perspective was that we had an “utils” file, it got a heavy dependency (CodeMirror linter logic for sandboxes), but it was imported from the main entry point. The fix for that was to split the utils file into several. github.com/reactjs/reactj…
While doing this, I noticed we load the CodeMirror linter logic eagerly on the pages that *do* have sandboxes, but that’s also a bit too early. I wanted to defer that a bit compared to loading other code. Doesn’t affect the homepage but is a nice fix too. github.com/reactjs/reactj…
While doing *that*, I noticed that our ESLint sandbox integration bundles all built-in ESLint rules. But actually, we only want to bundle the React ESLint rules which our sandboxes will run automatically. So I fixed that too (also doesn’t affect homepage). github.com/reactjs/reactj…
Then I noticed that Next.js has not yet switched to bundling for modern browsers by default. Not a huge deal, but it bloats the generated code a bit. Until this is the default behavior, I flipped an experimental flag to enable it. github.com/reactjs/reactj…
Running that version on webpagetest (webpagetest.org/video/compare.…) shows there is a layout shift due to the custom font loading too late. Very bad! The font was hosted from another domain, and connecting to it took too much time. Serving from same domain fixed the shift in most cases.
Finally, you might have noticed the Total Blocking Time has gone down quite a bit. Actually, I didn’t do much. In React 18, wrapping some UI in <Suspense> makes its hydration non-blocking. I added a couple <Suspense> tags, and React 18 did the rest. github.com/reactjs/reactj…
React 18 hydrates synchronously down to the closest <Suspense> boundaries. Remaining hydration is non-blocking and happens in chunks. This is automatic: I added some <Suspense> boundaries, and hydrating them became non-blocking! That’s why TBT went down.
Now, of course you could say... But why run this code on the client in the first place? Isn’t hydrating something that’s mostly static kinda useless? Things are usually not 100% static, but a mix. One hybrid solution is Astro-like “islands”: splitting your code in two paradigms.
We don’t find solutions that split by paradigm very satisfying. So the approach we are working on is Server Components (which can run during the build, btw). Server Components let you stay within a single paradigm, but make some code server-only. Hydrating those parts is instant.
Instead of solving performance in the order of [ship less code] -> [optimize remaining client code], we are solving it in the order [optimize client code assuming there’s a lot] -> [ship less code]. Same eventual result but there is a higher bar to make client optimizations good.
In the end, the code that isn’t needed on the client shouldn’t be shipped to the client. Hydrating should be instant for static parts and only take a bit of time for interactive parts. But if a slowdown does happen, your <Suspense> boundaries will keep it non-blocking. 😍
As <Suspense> becomes widely adopted (after data fetching integrations), this perf improvement will come from simply adding a loading state somewhere in your code. It’s essentially a better default for hydration behavior. For more about React 18, check out
There’ll be more to optimize. Especially on sandbox-heavy pages. Like Evan pointed out, the INP metric (web.dev/inp/) looks sad. I doubt it’s because React is slow. :) I have one guess for a possible cause. If you want to help, here’s an idea: github.com/reactjs/reactj…
anyway,,, goodnight
*typo in the first tweet — should say “from nine runs”. the calculations were correct though
One last thing. The benefits of non-blocking hydration in React 18 are much more obvious on a page with a *lot* of content. It doesn’t matter how much content there is! The browser becomes (and remains) responsive after the outer shell hydrates. Concurrent rendering is nice.
Of course, it would be even better if this only included the interactive components and skipped the static parts... But we already talked about this. We want mechanisms to do both so that you don’t have to choose.
my favorite “bundle analyzer” tool is taking <script> tag contents, putting it though Prettier, copy pasting into my editor, and scrolling through it. if i don’t understand where the code is from, i guess by names or search for identifiers in the libraries i’m using.
this lets me notice a whole class of issues (“wtf is this thing”, “why is it compiled inefficiently”, “why does this run so early”, “wait wasn’t this code supposed to be dead”) that higher-level friendlier tools miss
for react, we have a build mode that applies all the usual minification techniques *except* it leaves all variable names intact. this is amazing and something i miss a lot when dealing with analyzing Next.js/CRA bundles. a flag to build with readable output would be dope
The difference between having Strict Mode on and off is between “eager bugs” and “on-demand bugs”. Strict Mode immediately forces you to handle the edge cases. This includes bugs you won’t hit in prod today but definitely will as your logic changes. I’ve seen this many times.
This means it’s less of a technical and more or a cultural question. Do you prefer to hit all the edge cases in the beginning and use them to guide (and sometimes rethink) the approach? Or do you prefer to hit them as the code evolves and different cases appear in prod?
I emphatize with “we need to ship this week” mindset. I say it’s fine to just disable the Strict Mode and reenable it back when you have some time to do foundational work. But I also think people undervalue a tool that magically surfaces entire classes of future prod bugs today.
*Every* value inside useEffect is considered a dependency. The effect body is fully “reactive” — whenever any value changes, we re-fire the effect. This is to ensure that the result of the effect is always consistent with the latest props/state.
But you don’t always want that...
Consider this example. You want to log every page visit and attach current user name to the log. You only want to re-run it when the visited URL changes.
However, the linter nags you to include the current user name as a dependency. That can be a problem!
We’ve posted an RFC for useEvent. We suspect it might have been the missing piece in the original Hooks release. It lets you define an event handler that “sees” fresh props/state but has a stable identity. Would love to hear feedback! github.com/reactjs/rfcs/p…
i probably won’t be able to follow all the discussion on twitter so please comment on the RFC too!
i think we’re all going to die and disappear. i don’t believe in literal afterlife, stateful reincarnation, etc. but! i have a metaphysical mortality cope that i want to share. it doesn’t mean much (i said metaphysical!) but soothes my mind a little bit.
it goes like this. there is a feeling of “nowness” that you feel right now. it changes with every moment. it’s like you’re flowing through time. in that sense once you run out of those moments, you’re gone. no more experiencing. sounds sad so far…
now suppose that each of those moments “existed” in some metaphysical moment-space. like a collection of all frames in a movie. each frame “exists” regardless of where the playhead is. each frame “exists” regardless of whether the file is over, or whether it is playing.