We memo all the things (2020)
attardi.orgThis feels to me like another example of how the drive to use nothing but functional components in React is harming readability. Class based components were allowed to define a `shouldComponentUpdate` method which would be called ahead of rendering to decide whether a re-render is needed.
Having the parent component memoise the component instead feels like a step backwards as we're now asking the parent to carry an understanding of a child component's implementation to decide whether re-renders are needed, rather than allowing the child to communicate that up the tree.
the parent doesn't have to memoise the component though. You can do this:
though you would likely want to set displayName before exporting, in reality.export default React.memo(function myComponent(props) { // implementation });In which case, I stand corrected. I’ve not used React for a while and wasn’t aware that was possible.
But that is still memo-ing, just in a different part of the code. And I'm not sure wrapping all components in React.memo is a good practice, otherwise all components would be memo-ed by default. `shouldComponentUpdate` is more explicit
Have you ever dug through a complex heavily nested application trying to debug a performance issue, only to find a custom shouldComponentUpdate method within every individual component? It makes debugging an absolute nightmare.
I'm not sure why it would. The profiler will tell you exactly which component is the bottleneck, and you could then go inspect that one for the cause.
>The profiler will tell you exactly which component is the bottleneck, and you could then go inspect that one for the cause.
And then you have to understand the entire thought process and business logic that led to how the shouldComponentUpdate method for that class is implemented for every bottleneck, where one mistake or oversight can lead to an infinite re-render loop. This is the kind of stuff that should be handled at the framework level or with your state management library, not individual components.
Yeah but no one should be using shouldComponentUpdate except for rare cases
If you have to consistently replace a framework-managed hook for performance, either the framework is not suited for you, the framework is poorly designed, or you’re using the framework wrong.
What is your proposed alternative?
>What is your proposed alternative?
Precisely what the post describes. Functional components and memoize all the things.
Not the OP, but in a word: Redux.
no?
If you want state management you use a state management library. If you want optimization, you do memo, or pure component. You don't rely on the fact that some state library does that for you. You don't want your component's performance changes drastically when you refactor to context or whatever
See my response to another comment in this thread. react-redux manages shouldComponentUpdate on your behalf. That's what's being discussed: the pain that comes from custom shouldComponentUpdate implementations. I almost never have to write custom component lifecycle methods when using Redux.
The people who have a need for redux should already be using it. Are you telling people with a need to optimize their components to conjure up a need for redux, just so that they can use it to kill 2 birds with one stone?
I would argue that the people who find themselves maintaining an application that is littered with custom shouldComponentUpdate's (et al) have a clear need for _some_ organizing state management principle, whether that's Redux or MobX or something else.
> Have you ever dug through a complex heavily nested application trying to debug a performance issue, only to find a custom shouldComponentUpdate method within every individual component?
The author specified a performance issue that they're trying to debug, but in my experience it's just as common to waste cycles debugging a "why is my component not updating" issue, which is the flip side of the coin: not enough re-renders, instead of too many. So the way I see it, it is not a performance problem per se, it's a state management problem; poor performance (from spurious rerenders) is just one of the possible symptoms of immature state management.
What? Redux doesn't do any performance optimizations.
Not by itself, but most people who use React + Redux use the (unsurprisingly named) react-redux glue library, too. That does a lot of the "should component update" calculations for you as a function of the computed `map[State/Dispatch]ToProps` result. (There are still some gotchas with caching selectors and so on, but I personally find those a lot easier to implement post-fact than memoizing hook soup codebases once you realize you have a problem.)
Oh, I never knew that. TIL
I like React a lot and have used it professionally for more than 5 years. First off I mostly haven't needed to memoize any time I can remember in any enterprise (non-SaaS) production or personal apps. But surely CoinBase is at a bigger scale than my apps were/are.
But if it's the case that memoizing is such a good thing to do despite the effort (and I'm not debating that in this question), why is React designed in a way that requires you to opt into it and write the same boilerplate all over the place?
If hooks make this a problem maybe hooks aren't the best (or at least pinnacle) design? (And I really prefer hooks, personally.)
What does "needed" mean in "I mostly haven't needed to memoize any time I can remember"?
Does it mean that that your manager/team lead has never asked you to? Or that your production builds always hit some performance benchmark? Or that your development builds hit that benchmark? Or that you never noticed a qualitative slowdown in your development environment? Or something else?
"Needed" is a word you gotta define when you start talking about performance, unlike discussing functional correctness.
There was never a slowness in the apps I've worked on that came down to requiring memoization in React versus other basic things like doing pagination or other ways of rendering only things that would be displayed at the time for the user.
> pagination or other ways of rendering only things that would be displayed at the time for the user
Well, yes, those are alternate ways to deal performance problems, but they are not necessarily any easier than adding a bit of memoization.
No those solutions (and problems) were completely tangent to adding memoization. My point is that I've never seen a performance issue where non-memoization was the problem.
the only time I find it to be required is when passing in callbacks to custom hooks with props that may change and you'll notice immediately because the callback will continuously run
If you're using React.useCallback to avoid triggering further hooks down the line, then you're using it wrong. Since React.useCallback is ostensibly just React.useMemo wrapped around a function, this note in the documentation is just as applicable:
You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance.You're only using it wrong if you're trying to avoid triggering hooks as something other than a performance optimization.
For example, something like this should work correctly even without useCallback, but you would be constantly re-subscribing for no good reason.
const callback = useCallback(...); useEffect(() => { const unsubscribe = subscribe(callback); return () => unsubscribe(); }, [callback]);I was following a pattern similar to react-table, where I'm destructuring a function off of a previous hook and passing it to another hook as a callback. This allows the 2nd hook to update state in the first, which causes it to infinite loop if the function isn't wrapped. I think this pattern is more complicated than it sounds, but really helps to remove business logic from a component
If we have a component <ProfilePage id={id} onLoad={handleOnLoad} /> and inside profile page we use an effect to load from id and call handleOnLoad with the loaded profile data, then we need to put handleOnLoad in the useEffect dependencies. So we have to pass a callback wrapped in useCallback else the effect would fire every time ProfilePage rerenders.
yeah, I was thinking about this too as someone who's not super familiar with React — why isn't memoing the default behavior?
I think that's because everyone needs to debug their React app, but not everyone needs the performance. So you optimise for the most common use case, where you make debugging easier. Memoization is a form of caching, caching is hard. Most backend web frameworks don't come with a cache by default, you add one when you need the performance. It's the same here.
Kind of makes you wonder why React.memo isn't just the default behaviour.
https://github.com/facebook/react/issues/14463#issuecomment-... suggests it was meant to be the default behaviour, but was scrapped because it would "break backwards compatibility". No source is given for that claim though.
Interesting, I mostly followed this approach which is to basically never use those constructs unless you are 100% certain you know what will happen.
https://kentcdodds.com/blog/usememo-and-usecallback
TLDR;
> Specifically the cost for useCallback and useMemo are that you make the code more complex for your co-workers, you could make a mistake in the dependencies array, and you're potentially making performance worse by invoking the built-in hooks and preventing dependencies and memoized values from being garbage collected. Those are all fine costs to incur if you get the performance benefits necessary, but it's best to measure first.
The problem with avoiding useCallback is that another hook will bite you: useEffect. If you need to define functions that interact with your component's state, you have to memoize them with useCallback (or useRef) to avoid a useEffect infinite loop.
What's even worse is that if functions passed as props are unstable, your useEffect will run every time the parent component renders — meaning that a component can't trust functions passed into it.
This is one of many reasons I think useEffect is a huge footgun, and I really wish we had a better primitive for causing side effects.
I wouldn't say useEffect is a footgun, it just requires realising that React is doing shallow comparisons.
To me, a footgun isn't necessarily something that is hard to understand, but also something that's easy to misuse even if you understand it.
very good point
hooks in general are a huge footgun. You need to place them at the preamble of your function (and not nested). They have to be named with "use" prefix (worth mentioning is React basically commandeered the entire "use" namespace for their own purposes via lint enforcement, which will bite you in the ass eventually), and they are poorly designed.
Take useRef for example. You'd logically expect it to work with useEffect and that would be how you use refs in hooks land. But of course, refs are still a design wart on React (they've been through, what, 4 iterations now and they still can't figure out the interface?!). So of course you need to use useCallback. So what is the point of useRef then? I have no idea. The only use I've found is for "instance" variables. Or maybe onClick callbacks that run later. But now you have a ref that only works in some cases and not others. Yay, "composability"
Browse the React docs and you'll find the caveat-to-design ratio is exceedingly high. On any other project you'd assume this is beta or alpha ware.
In practice, I don't think the "rules of hooks" [1] are a footgun, especially if you use the eslint rule.
I'm confused about your second paragraph. useRef works perfectly fine for keeping a reference stable so as to avoid triggering useEffect. In fact, that's one of the main tools I use to work around the useEffect dependency array.
useRef can be used to hold a reference to DOM elements, useful for breaking out of Reactland or to hold and mutate values that should not cause a rerender.
> you could make a mistake in the dependencies array,
This is an auto-fix with eslint, and when it isn't exactly right (you need a "one way update") you can override that rule.
I would posit that passing a value that is regenerated every render (as opposed to when it actually changes) outside of the component (via props or context) is much more dangerous and likely to create infinite loops. For stuff that stays internal, sure, knock yourself out (until it is required by a useEffect or anything else that needs dependencies).
I find the state of things in React, pun intended, a bit sad. Hooks are definitely an improvement over class based components but it still suffers from the same issue - having logic and state in the view layer leads to spaghetti code. Not to mention dependency arrays which are easy to get wrong and the weird syntax you end up with everywhere.
I'm a big fan of MobX and it pains me to no end that it didn't took off better. It's a godsend from like 5 years ago and it makes so many of these React pain points disappear.
Instead of adding state to your components here and there, it works outside of the view layer. You define the model, derived computed views, actions and then just use it inside your components. You never ever worry about performance because MobX knows what is used where and it will optimize your renders automatically.
Moreover, dealing with state outside of the view layer makes it much more easier to refactor, reason about and test your app. Sure, you can do the same with Redux but it's 10x more code.
I recommend this article on this topic by the author - https://michel.codes/blogs/ui-as-an-afterthought
I +1 MobX wherever I see it mentioned, but I have to disagree here:
> having logic and state in the view layer leads to spaghetti code
Sometimes it makes sense to put logic and state in your view components, and the wonderful thing about MobX is that it doesn't care. It lets you freely move your state around to wherever you want it to live: inside components, outside components, in a module-scoped object, in a global store, all of the above. It's just JavaScript objects.
For those fighting with hooks: one of the best things about MobX is that it does all dependency-tracking (for effects, but also for components) automatically and flawlessly. An entire problem-space that's usually easy to mess up just vanishes in front of your eyes. Going back to anything else feels like going back to the dark ages.
I'm not familiar with MobX, but I am a big fan of Jotai (https://jotai.pmnd.rs/). It is a spiritual offshoot of Facebook's experimental(?) Recoil library.
It is a bottom up approach to state. Everything is modeled as atoms. Atoms can be defined in any module and are accessed by simply exporting/importing from/to the module. Simply call the useAtom hook and you're now using that state atom.
Under the covers it is scoped via a top-level React context, I believe.
Atom derivation, read/write, async, it's all there. It also hooks into a lot of other popular state libs like redux, XState, Zustand and many others.
I much prefer it to Redux because there is zero boilerplate and extremely flexible. It can be easy to hang yourself with all the extra rope it gives you if you aren't careful though.
> Sometimes it makes sense to put logic and state in your view components
Of course, I do that a lot with generic, reusable components. Once I know it's too complex I extract it to model that gets passed through props. Win/win
This 100%. I still don't understand why React devs are so infatuated with colocating business logic with the UI that presents the result of that business logic. Sure, in the small (a todo list app? a weekend project?) it's probably a lot easier to reason about if you just jam everything into the same file. But why is it so difficult for people to see that the reason their large application is bloated, untestable, unmaintainable, etc is directly due to the blatant violation of separation of concerns that they're parroting?
I feel like part of this is due to some devil's bargain on the part of the React maintainers. They want mindshare, and they know that it's easier to gain mindshare if the behaviour of the app appears simpler, and that appearance of simpler is easier to achieve if the behaviour is relegated to a smaller number of files...
but hooks allow you to easily move business logic outside of your component. most business logic is simple so people use it inline. isolating it makes it pretty easy to test and reuse
That's true. But to echo the underlying theme of the original blog post, it is difficult to ensure that a large team of developers (of varying skill levels) maintains a consistent approach to this. Different developers likely have different opinions of where that "simple" threshold is that warrants factoring out the business logic. And, when the push of a deadline comes to shove, it is tempting to just throw all the shit in the component with the promise that we'll refactor it at a later date.
More fundamentally, I feel like putting business logic hooks in a component (even simple ones) sacrifices the cleanliness of components as simple stream transformers: input some scalar props, output some HTML. As soon as you start calling hooks within a function component, it's no longer purely functional. You have side effects. And those side effects result in a component that's trickier to debug and trickier to test.
I've normally heeded to the advice from Dan Abramov (https://overreacted.io/before-you-memo/) and KCD (https://kentcdodds.com/blog/usememo-and-usecallback): mainly due to the fact that I figured performance would take a significant hit by using unnecessary React.memo / useMemo calls throughout the codebase.
One negative side-effect I could see as a result of this pattern is devs becoming too reliant on these optimizations and neglecting composition.
But I suppose if the performance gain is substantial enough and DX isn't negatively impacted too much, it could serve as worthwhile-- especially at the level of scale which Coinbase requires.
This is a great article and I agree with it fully.
The argument that a lot of popular React voices have made, "React is fast and it's prematurely optimizing to worry about memoizing things until a profile shows you need it", has never rung true with me. First and foremost, there's a huge time cost to figuring out what those exact spots that need optimization are, and there's also an educational cost with teaching less experienced engineers how to correctly identify and reason about those locations.
There are only two reasonable arguments for not using `memo`, `useMemo`, and `useCallback`. The first is that it decreases devx and makes the code less readable. This one is true, but it's a very small cost to pay and clearly not the most important thing at stake as it's only a slight net effect. The second argument is that the runtime cost of using these constructs is too high. As far as I can tell, nobody has ever done a profile showing that the runtime cost is significant at all, and the burden of proof lies with those claiming the runtime overhead is significant because it doesn't appear that it is typically when profiling an app.
So, given that the two possible reasons for avoiding `memo`, `useMemo`, and `useCallback` are not convincing, and the possible downsides for not using them are fairly large, I find it best to recommend to engineering teams to just use them consistently everywhere by default.
I've always thought of "premature optimisation" as optimising something that's not your "hot path". If there's no clear hot path, everything is the hot path, and small optimisation gains everywhere are the only thing you're going to get. So at this point, it's not premature.
You could also rewrite your code so that there is a clear hot path, but in that case it seems to be React rendering, that's optimised by using memo and avoiding it completely.
The death from a thousand papercuts.
I'm not terribly convinced with memoization though. You're using extra memory, so it's not free optimization. We have Redux memoized selectors everywhere. I can't help but wonder how much of that is actually a memory leak (i.e. it's never used more than once). Granted, components are a bit different.
I always do cringe when I see a lint rule forcing you to use a spread operator in an array reduce(). It's such a stupid self-inflicted way to turn an O(N) into an O(N^2) while adding GC memory pressure. All to serve some misguided dogma of immutability. I feel there is a need for a corollary to the "premature optimization is the root of all evil" rule.
> I always do cringe when I see a lint rule forcing you to use a spread operator in an array reduce(). It's such a stupid self-inflicted way to turn an O(N) into an O(N^2) while adding GC memory pressure. All to serve some misguided dogma of immutability. I feel there is a need for a corollary to the "premature optimization is the root of all evil" rule.
I think a rule of "don't try to use X as if it was Y" would be reasonable. I love immutability, but the performance cost in JS is really high. Many people are fine with using Typescript to enforce types at compile time and not at runtime. Maybe many people would be fine with enforced immutability at compile time (Elm, Rescript, OCaml, ...) and not runtime?
How could you not have a hot path? You're saying that you've measured actual usage and discovered that each thing happens to be called exactly the same number of times? That strikes me as extraordinarily improbable.
That's not exactly it. It's more of a "If you have nothing that takes more than 1% of your resources, no single optimisation can get you more than a 1% reduction in your resources". That seem to be how most web apps are: you parse a little bit of HTTP, a little bit of JSON, you validate a few things, you call the database, that does a few things too, you have a bit of business logic, you call the database again, then have a bit of glue code here and there, and finally respond to the user with a little bit of HTTP and maybe some HTML, maybe some JSON.
If that's how your app works and nothing can be optimised significantly, that's usually here where you can make big gains in performance by changing a big thing. One of these big things might be to put a cache in front of it, because a cache hit will be way faster than responding again to the same request. Another could be to change language. For example, from Python to Go. Since Go is (most of the time) a bit faster on everything, you end up being faster everywhere. Or even from Python to PyPy, a faster implementation. Another could be redesigning your program so that you have one single obvious hot path, and then optimising that.
That seem to be the case for them here: no component is taking all of the resources, but by using memo everywhere, all of them take less resources, which leads to a good reduction of resources in general.
It seems to me you're being pretty breezy about readability. At most places, developer time is by far the most expensive commodity, and the limiting factor in creating more user value.
In particular, bad readability is one of the sources of a vicious circle where normalization of deviance [1] leads to a gradual worsening of the code and a gradual increase in developer willingness to write around problems rather than clean the up. Over time, this death by a thousand cuts leads to the need for a complete rewrite, because that's easier than unsnarling things.
For throwaway code, I of course don't care about readability at all. But for systems that we are trying to sustain over time, I'm suspicious of anything that nudges us toward that vortex.
I don't disagree with you on readability being important or on the value of developer time. It's just that the marginal costs of `memo`, `useMemo`, and `useCallback` are quite low. They don't add cyclomatic complexity, they don't increase coupling, they can be added to code essentially mechanically and don't carry a large cognitive overhead to figure out how to use, etc.
The main downsides are that they take slightly longer to type and slightly decrease the succinctness of the code. And then there are a few React-specific complexities they add (maintaining the deps arrays and being sure not to use them conditionally) but these should be checked by lint rules to relieve developer cognitive load.
Of course I'd rather not have these downsides, but in the end, it's still much less developer overhead than having to constantly profile a large application to try and figure out the trouble spots and correctly test and fix them post-hoc. And it means users are much more likely to get an application that feels snappier, doesn't drain as much battery, and just provides a more pleasant experience overall, which is worth it imo.
> has never rung true with me.
Yeah, me neither. I'm seeing first-hand a "large" (but probably not Coinbase-large) webapp dying by 10 thousand cuts.
The "you shouldn't care if it rerenders" components are, together, affecting performance. Going back and memoizing everything would be a nightmare and not a viable business solution. Rewrite everything from scratch is also not viable. So we have to live with a sluggish app.
At the same time, memoizing everything does make your code unreadable.
Honestly, it's a mess. I only accept working with this kind of stuff because I'm very well paid for it.
On my personal projects I stay far away from the Javascript ecosystem, and it's a bless. Working with Elm or Clojurescript is a world of difference.
Clojurescript's reframe, by the way, uses React (via Reagent) and something somewhat similar to Redux, without having any of the pitfalls of modern JS/React.
I can write a large application and ensure that there are no unnecessary rerenders, without sacrificing readability and mental bandwidth by having to memorize everything.
The conclusion I have, which is personal (YMMV) and based on my own experience, is that modern JS development is fundamentally flawed.
Apologies for the rant.
> The conclusion I have, which is personal (YMMV) and based on my own experience, is that modern JS development is fundamentally flawed.
So because web developers using a particular UI library can debate one aspect of using the library, modern JS development is fundamentally flawed unless one transpiles from Elm or ClojureScript?
I created the benchmark for this.
Because in every aspect it seems that React.memo is better. Especially when we are sure of stable argument references.
Even when you add children to the component with memo, the worst case performance will be the same.
https://codesandbox.io/s/react-memo-benchmark-m2bk6?file=/sr...
There's no mention of what I think is the most important point: how is this enforced? If that's with a tool, I think it's great and a sane way to do things. If it's by asking everyone to remember doing it, I think it's a missed opportunity.
It's also interesting to see the age-old functional programming problem: you trade performance for ease of development. I think these days people assume that things like immutable data structures are optimised under the hood. That doesn't seem to be the case with React, as you have to explicitly use a performance trick everywhere.
Their argument that it would be premature optimisation to think about where memo is not needed makes sense, it's an interesting shift of optimisation of the runtime performance vs optimisation of the dev time.
> Using memo and its cousins everywhere is a sane default. It’s like a mask mandate during a coronavirus pandemic. Sure, we could have everybody tested every day and only ask people who are actively contagious to wear a mask. But it’s far cheaper, simpler, and ultimately more effective to ask everybody to wear a mask by default.
That's a good metaphor. It's easier for the people who decide, by shifting the burden on everyone else. I personally get headaches by wearing a mask all the time at work. I think I may get headaches too if I had to remember something like this all the time.
I think it would be easy to create a rule to see if the default export is wrapped in memo, but you could also just have it as a coding standard
What do you mean by coding standard? My point was that if it's enforced by tool, either as a reminder, or an automatic modification, it's fine, but if it's not, it's annoying.
I wonder if this is the plugin they are using:
https://github.com/steadicat/eslint-plugin-react-memo
Does anyone know of any other eslint plugins that help enforce this?
The author of the article wrote the ESLint plugin: https://github.com/steadicat So they probably use(d) it.
Has anyone added a compilation step that adds all the memoization boilerplate for you?
I think it can be a solution to readability problem
This should have „React:“ mentioned in the title.
I was thinking about a technique to write things down…
I don’t disagree that this likely improves performance in most cases, and I don’t blame the author here for any of my concerns.
We’ve implemented this in our code base and it’s awful. Yes it improves performance. It also makes debugging terrible. Combined with contexts, the react dev tools are virtually useless anywhere below a context with a memoized value.
Profiling is harder because of frameworky misdirections as well. You can do coarse benchmarks but actually following code in a profile gets noticeably worse once you memo everything.
I hope this is fixed. I really enjoy react, but this odd thing about it - that we arguably should memoize everything manually, and that it does make the dev tools a mess, is a huge hit to developer experience.
So tired of “Component did not render” in the Component tab.
From the useMemo docs: https://reactjs.org/docs/hooks-reference.html#usememo
You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance.
I think about this section a lot. If they actually changed useMemo to sometimes "forget" values, it would break so many useEffect dependency arrays (including my own).
This is an antipattern for a reason. Not only is the code less readable, it builds bad habits of code splitting state management and overall structure. A much better alternative is to just learn to understand the tools you are using better.
I have basically no interest in frontend stuff, but I can't stop staring at this page. I think it's the overall color scheme and that body font. It's gorgeous.
It certainly feels like a failing of the hooks design that these subjects are so common (I love hooks generally!). When someone introduces a new paradigm that by design has a list of footguns to avoid you can't help but wonder if this was necessary.
Has anyone tried tackling a hooks-like api that fixes the known pitfalls? encapsulating shared logic with hooks is a massive benefit but the subtleties can be difficult to teach to others.
> If you’ve ever profiled a React app – even in production mode – you know there is a non-negligible performance impact to every component that renders.
Maybe it's just me, but I've used React for ~5 years and I've never needed to profile an app, since the performance out of the box has always been good enough.
The situation is much more severe with react-native, which doesn’t have a JIT compiler available.
This was in 2020. I assume the team got made redundant in the last year or something. Coinbase has genuinely been the slowest performing website I've used in the past couple of years.
The team actually became part of a company-wide effort called Client Foundations! And no, we haven't changed our mind on this. (BTW we're hiring. :))
I asked the author on Twitter if this approach was still in place, but didn't get a reply :/
Edit: He did reply: https://twitter.com/steadicat/status/1452686612370915328
Dumb question, but is there an equivalent concern for Vue? Or a reason why this kinda checking isn't necessary there?