惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
有赞技术团队
有赞技术团队
S
SegmentFault 最新的问题
宝玉的分享
宝玉的分享
量子位
小众软件
小众软件
The Cloudflare Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
大猫的无限游戏
大猫的无限游戏
C
Check Point Blog
G
Google Developers Blog
博客园 - 叶小钗
H
Help Net Security
Jina AI
Jina AI
Y
Y Combinator Blog
Last Week in AI
Last Week in AI
GbyAI
GbyAI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Apple Machine Learning Research
Apple Machine Learning Research
MyScale Blog
MyScale Blog
T
Tailwind CSS Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Vercel News
Vercel News

overreacted — A blog by Dan Abramov

There Are No Instances in atproto — overreacted Algebraic Effects for the Rest of Us — overreacted A Social Filesystem — overreacted Introducing RSC Explorer — overreacted Hire Me in Japan — overreacted How to Fix Any Bug — overreacted Where It's at:// — overreacted Open Social — overreacted A Lean Syntax Primer — overreacted Beyond Booleans — overreacted The Math Is Haunted — overreacted Suppressions of Suppressions — overreacted I'm Doing a Little Consulting — overreacted How Imports Work in RSC — overreacted RSC for LISP Developers — overreacted Progressive JSON — overreacted Why Does RSC Integrate with a Bundler? — overreacted One Roundtrip Per Navigation — overreacted Static as a Server — overreacted RSC for Astro Developers — overreacted Functional HTML — overreacted What Does "use client" Do? — overreacted Impossible Components — overreacted JSX Over The Wire — overreacted React for Two Computers — overreacted The Two Reacts — overreacted A Chain Reaction — overreacted npm audit: Broken by Design — overreacted The WET Codebase — overreacted Goodbye, Clean Code — overreacted
Before You memo() — overreacted
2021-02-23 · via overreacted — A blog by Dan Abramov

There are many articles written about React performance optimizations. In general, if some state update is slow, you need to:

  1. Verify you’re running a production build. (Development builds are intentionally slower, in extreme cases even by an order of magnitude.)
  2. Verify that you didn’t put the state higher in the tree than necessary. (For example, putting input state in a centralized store might not be the best idea.)
  3. Run React DevTools Profiler to see what gets re-rendered, and wrap the most expensive subtrees with memo(). (And add useMemo() where needed.)

This last step is annoying, especially for components in between, and ideally a compiler would do it for you. In the future, it might.

In this post, I want to share two different techniques. They’re surprisingly basic, which is why people rarely realize they improve rendering performance.

These techniques are complementary to what you already know! They don’t replace memo or useMemo, but they’re often good to try first.

An (Artificially) Slow Component

Here’s a component with a severe rendering performance problem:

import { useState } from 'react';
 
export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}
 
function ExpensiveTree() {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // Artificial delay -- do nothing for 100ms
  }
  return <p>I am a very slow component tree.</p>;
}

(Try it here)

The problem is that whenever color changes inside App, we will re-render <ExpensiveTree /> which we’ve artificially delayed to be very slow.

I could put memo() on it and call it a day, but there are many existing articles about it so I won’t spend time on it. I want to show two different solutions.

Solution 1: Move State Down

If you look at the rendering code closer, you’ll notice only a part of the returned tree actually cares about the current color:

export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}

So let’s extract that part into a Form component and move state down into it:

export default function App() {
  return (
    <>
      <Form />
      <ExpensiveTree />
    </>
  );
}
 
function Form() {
  let [color, setColor] = useState('red');
  return (
    <>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
    </>
  );
}

(Try it here)

Now if the color changes, only the Form re-renders. Problem solved.

Solution 2: Lift Content Up

The above solution doesn’t work if the piece of state is used somewhere above the expensive tree. For example, let’s say we put the color on the parent <div>:

export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div style={{ color }}>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p>Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}

(Try it here)

Now it seems like we can’t just “extract” the parts that don’t use color into another component, since that would include the parent <div>, which would then include <ExpensiveTree />. Can’t avoid memo this time, right?

Or can we?

Play with this sandbox and see if you can figure it out.

The answer is remarkably plain:

export default function App() {
  return (
    <ColorPicker>
      <p>Hello, world!</p>
      <ExpensiveTree />
    </ColorPicker>
  );
}
 
function ColorPicker({ children }) {
  let [color, setColor] = useState("red");
  return (
    <div style={{ color }}>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      {children}
    </div>
  );
}

(Try it here)

We split the App component in two. The parts that depend on the color, together with the color state variable itself, have moved into ColorPicker.

The parts that don’t care about the color stayed in the App component and are passed to ColorPicker as JSX content, also known as the children prop.

When the color changes, ColorPicker re-renders. But it still has the same children prop it got from the App last time, so React doesn’t visit that subtree.

And as a result, <ExpensiveTree /> doesn’t re-render.

What’s the moral?

Before you apply optimizations like memo or useMemo, it might make sense to look if you can split the parts that change from the parts that don’t change.

The interesting part about these approaches is that they don’t really have anything to do with performance, per se. Using the children prop to split up components usually makes the data flow of your application easier to follow and reduces the number of props plumbed down through the tree. Improved performance in cases like this is a cherry on top, not the end goal.

Curiously, this pattern also unlocks more performance benefits in the future.

For example, when Server Components are stable and ready for adoption, our ColorPicker component could receive its children from the server. Either the whole <ExpensiveTree /> component or its parts could run on the server, and even a top-level React state update would “skip over” those parts on the client.

That’s something even memo couldn’t do! But again, both approaches are complementary. Don’t neglect moving state down (and lifting content up!)

Then, where it’s not enough, use the Profiler and sprinkle those memo’s.

Didn’t I read about this before?

Yes, probably.

This is not a new idea. It’s a natural consequence of React composition model. It’s simple enough that it’s underappreciated, and deserves a bit more love.