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

推荐订阅源

博客园 - 司徒正美
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
美团技术团队
WordPress大学
WordPress大学
罗磊的独立博客
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
爱范儿
爱范儿
MongoDB | Blog
MongoDB | Blog
J
Java Code Geeks
H
Hackread – Cybersecurity News, Data Breaches, AI and More
H
Help Net Security
S
SegmentFault 最新的问题
C
Check Point Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
腾讯CDC
Engineering at Meta
Engineering at Meta
The GitHub Blog
The GitHub Blog
F
Fortinet All Blogs
D
DataBreaches.Net
雷峰网
雷峰网
GbyAI
GbyAI
宝玉的分享
宝玉的分享

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.