Hacker News new | ask | show | jobs
by jasonkillian 1695 days ago
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.

3 comments

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.

[1] https://en.wikipedia.org/wiki/Normalization_of_deviance

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?