这是用户在 2024-5-14 10:43 为 https://www.joshwcomeau.com/react/use-deferred-value/ 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?
Skip to content 跳至内容

Snappy UI Optimization with useDeferredValue
使用 useDeferredValue 进行 Snappy UI 优化

Introduction

Over the years, React has given us a number of tools for optimizing the performance of our applications. One of the most powerful hidden gems is useDeferredValue. It can have a tremendous impact on user experience in certain situations! ⚡
多年来,React 为我们提供了许多用于优化应用程序性能的工具。最强大的隐藏宝石之一是 useDeferredValue 。在某些情况下它会对用户体验产生巨大影响! ⚡

I recently used this hook to fix a gnarly performance issue on this blog, and it sorta blew my mind. The improvement on low-end devices felt illegal, like black magic.
我最近使用这个钩子来解决这个博客上的一个棘手的性能问题,它有点让我震惊。对低端设备的改进感觉是非法的,就像黑魔法一样。

useDeferredValue has a bit of an intimidating reputation, and it is a pretty sophisticated tool, but it isn’t too scary with the right mental model. In this tutorial, I’ll show you exactly how it works, and how you can use it to dramatically improve the performance of your applications.
useDeferredValue 享有有点令人生畏的声誉,它是一个相当复杂的工具,但如果有正确的心智模型,它并不会太可怕。在本教程中,我将向您展示它的具体工作原理,以及如何使用它来显着提高应用程序的性能。

Link to this heading
The problem 问题

A couple of years ago, I released Shadow Palette Generator, a tool for generating realistic shadows:
几年前,我发布了 Shadow Palette Generator,一个用于生成逼真阴影的工具:

By experimenting with sliders and other controls, you can design your own set of shadows. The CSS code is provided for you to copy/paste it into your own application.
通过尝试滑块和其他控件,您可以设计自己的一组阴影。提供 CSS 代码供您将其复制/粘贴到您自己的应用程序中。

Here’s the problem: the controls in this UI are designed to provide immediate feedback; as the user slides the “Oomph” slider, for example, they see the effect of that change right away. This means that the UI is re-rendered dozens of times a second while one of these inputs is being dragged.
问题是:此 UI 中的控件旨在提供即时反馈;例如,当用户滑动“Oomph”滑块时,他们会立即看到该更改的效果。这意味着当拖动这些输入之一时,UI 每秒会重新渲染数十次。

Now, React is fast, and most of this UI is pretty easy to update. The problem is the syntax-highlighted code snippet at the bottom:
现在,React 速度很快,并且大部分 UI 都非常容易更新。问题在于底部语法突出显示的代码片段:

Screenshot of the code output from the above video, showing 4 declared CSS variables, fully syntax-highlighted

Syntax highlighting is a surprisingly complex operation. First, the raw code has to be “tokenized”, a process which splits the code into a set of labeled pieces. Each token can be given a different color, and so each token needs to be wrapped in its own <span>.
语法突出显示是一项极其复杂的操作。首先,原始代码必须被“标记化”,该过程将代码分割成一组带标签的片段。每个令牌都可以指定不同的颜色,因此每个令牌都需要包装在自己的 <span> 中。

Here’s the amount of markup required for a single line from this snippet:
以下是此代码片段中单行所需的标记量:

screenshot of the developer tools, showing a <div> with 24 <span> elements, each with two classes and occasionally some inline styles.

Without any optimizations, we’re asking React to re-calculate all of this markup dozens of times per second. On most devices, the browser just won’t be able to do this quickly enough, and things will get pretty choppy:
如果不进行任何优化,我们会要求 React 每秒重新计算所有这些标记数十次。在大多数设备上,浏览器无法足够快地完成此操作,并且事情会变得相当不稳定:

The change events are firing up to 60 times per second, but the UI can only process a handful of updates per second. The result is a UI that feels janky and unresponsive.
change 事件每秒最多触发 60 次,但 UI 每秒只能处理少量更新。结果是用户界面感觉很卡顿且反应迟钝。

It’s an interesting problem: the most important part of this UI is the set of figures on the left showing what the shadows look like. We want this part to update immediately in response to the user’s tweaks, so that they can understand the effect of their changes. We also want the controls themselves to feel snappy and responsive.
这是一个有趣的问题:这个用户界面最重要的部分是左侧的一组图形,显示了阴影的样子。我们希望这部分能够立即更新以响应用户的调整,以便他们能够了解更改的效果。我们还希望控件本身感觉敏捷且反应灵敏。

The code snippet, on the other hand, doesn’t really need to be updated dozens of times a second; the user only cares about the code at the end, when they’re ready to copy it over to their application. By recalculating it on every change, the entire user experience is degraded.
另一方面,代码片段实际上并不需要每秒更新数十次;用户只关心最后的代码,当他们准备好将其复制到他们的应用程序时。通过在每次更改时重新计算,整个用户体验都会下降。

Put another way, this UI has high-priority and low-priority areas:
换句话说,这个 UI 具有高优先级和低优先级区域:

The same “Shadow Palette Generator” UI but with boxes drawn on top. The shadow output on the left and the controls are labeled “high priority”, while the code output in the bottom right is labeled “low priority”.

We want the high-priority stuff to update in real-time, as quickly as possible. But the low-priority stuff should be put on the back burner.
我们希望高优先级的内容能够尽快实时更新。但低优先级的事情应该放在一边。

Link to this heading
An imperfect solution 不完美的解决方案

My original solution to this problem used a technique known as “throttling”. Essentially, I restricted this component so that it could only re-render once every 200 milliseconds.
我最初解决这个问题的方法是使用一种称为“节流”的技术。本质上,我限制了这个组件,使其只能每 200 毫秒重新渲染一次。

Here’s what this looked like:
看起来是这样的:

Notice that the code snippet updates much less frequently than the other parts of the UI? It will only update every 200 milliseconds, 5 times per second, while the rest of the UI can re-render as often as necessary.
请注意,代码片段的更新频率比 UI 的其他部分要低得多?它只会每 200 毫秒更新一次,每秒更新 5 次,而 UI 的其余部分可以根据需要经常重新渲染。

This is better, but it's far from a perfect solution.
这更好,但远非完美的解决方案。

It still feels a bit laggy / janky; users won’t understand that we’re intentionally slowing down part of the UI!
它仍然感觉有点滞后/卡顿;用户不会理解我们故意减慢部分 UI 的速度!

More importantly, people use a wide variety of devices, from super-powerful modern computers to ancient low-end Android phones. If the user’s device is fast enough, the throttle is unnecessary, and we’re just slowing things down for no reason. On the other hand, if the device is really slow, 200ms might not be sufficient, and the important parts of the UI will still get janked up.
更重要的是,人们使用各种各样的设备,从超强大的现代计算机到古老的低端 Android 手机。如果用户的设备足够快,那么节流阀是不必要的,我们只是无缘无故地减慢速度。另一方面,如果设备真的很慢,200 毫秒可能还不够,UI 的重要部分仍然会卡住。

This is exactly the sort of problem that useDeferredValue can help with.
这正是 useDeferredValue 可以帮助解决的问题。

Link to this heading
Introducing useDeferredValue
介绍 useDeferredValue

useDeferredValue is a React hook that allows us to split our UI into high-priority and low-priority areas. It works by allowing React to interrupt itself when something important happens.

To help us understand how this works, let’s start with a simpler example. Consider this code:
为了帮助我们理解它是如何工作的,让我们从一个更简单的例子开始。考虑这段代码:

jsx

Our piece of state is count, a number which can be incremented by clicking a button. ImportantStuff represents our high-priority part of the UI. We want this to update right away, whenever count changes. SlowStuff represents the less-important part of the UI.
我们的状态是 count ,这是一个可以通过单击按钮递增的数字。 ImportantStuff 代表 UI 的高优先级部分。我们希望每当 count 发生变化时立即更新。 SlowStuff 代表 UI 中不太重要的部分。

Whenever the user clicks the button to increment count, React has to re-render both of these child components before the UI can be updated.
每当用户单击按钮来增加 count 时,React 都必须重新渲染这两个子组件,然后才能更新 UI。

Let’s analyze this. Click the button below to see this render in action:
我们来分析一下这个。单击下面的按钮查看此渲染的实际效果:

count 数数
1
1
10
20
30
40
50
60
70
80
90
100

The UI in this demo is a video, showing a recorded interaction. You can scrub through this timeline to see exactly what the UI looked like at any moment in time. Notice that the render starts when the button is clicked, but the UI doesn’t update until the render has completed.
此演示中的 UI 是一个视频,显示录制的交互。您可以浏览此时间线以准确查看 UI 在任何时刻的外观。请注意,单击按钮时就会开始渲染,但直到渲染完成后 UI 才会更新。

This render represents the entire chunk of work that React has to do, rendering both ImportantStuff and SlowStuff. Click/tap this render snapshot to peek inside:
该渲染代表了 React 必须完成的全部工作,渲染 ImportantStuffSlowStuff 。单击/点击此渲染快照以查看内部:

In this hypothetical example, ImportantStuff renders super quickly. The bulk of the time is spent rendering SlowStuff.
在这个假设的示例中, ImportantStuff 渲染速度非常快。大部分时间都花在渲染 SlowStuff 上。

If the user clicks the button too quickly, our renders will “pile up”, since React isn't able to finish the job before the next update happens. This leads to janky UI:
如果用户单击按钮太快,我们的渲染将会“堆积”,因为 React 无法在下一次更新发生之前完成工作。这会导致用户界面卡顿:

count 数数
1
count 数数
2
count 数数
3
1
10
20
30
40
50
60
70
80
90
100
110
120
130
140
150

Before that first render (count: 1) has finished, the user clicks the button again, setting count to 2. React abandons that render and starts a new one with the correct value for count. The UI only gets updated when a render successfully completes.
在第一次渲染 ( count: 1 ) 完成之前,用户再次单击该按钮,将 count 设置为 2 。 React 放弃该渲染并使用正确的 count 值启动一个新的渲染。仅当渲染成功完成时,UI 才会更新。

Now, given all that context, let’s see how useDeferredValue helps us solve this problem.
现在,考虑到所有这些背景,让我们看看 useDeferredValue 如何帮助我们解决这个问题。

Here’s the code: 代码如下:

jsx

For the initial render, count and deferredCount are the exact same value (0). When the user clicks the “Increment” button, though, something interesting happens:
对于初始渲染, countdeferredCount 是完全相同的值 ( 0 )。然而,当用户单击“增量”按钮时,会发生一些有趣的事情:

count
1
dCount
0
count 数数
1
dCount 计数
1
1
10
20
30
40
50
60
70
80
90
100

Each render now shows the value for count as well as the deferred value we pass to <SlowStuff>. If there isn’t enough space to include their labels, the timeline instead shows count and deferredCount separated by a line.
现在,每个渲染都会显示 count 的值以及我们传递给 <SlowStuff> 的延迟值。如果没有足够的空间来包含它们的标签,时间线会显示 countdeferredCount ,并用一条线分隔。

Alright, let’s unpack this. When the count state changes, the App component re-renders immediately. count is now equal to 1, but interestingly, deferredCount hasn’t changed. It still resolves to the previous value of 0.
好吧,我们来解压一下。当 count 状态改变时, App 组件立即重新渲染。 count 现在等于 1 ,但有趣的是, deferredCount 没有改变。它仍然解析为 0 的先前值。

This means that SlowStuff receives the exact same props that it did in the previous render. If it’s been memoized with React.memo(), it won’t bother re-rendering, since React already knows what would be produced. It’s able to re-use the stuff from the first render.
这意味着 SlowStuff 接收到的 props 与之前渲染中的 props 完全相同。如果它是用 React.memo() 记忆的,它就不会费心重新渲染,因为 React 已经知道会生成什么。它能够重复使用第一次渲染中的内容。

Right after that render finishes, a second re-render is started, except now, deferredCount has been updated to match count’s value of 1. This means that SlowStuff will re-render this time. When all is said and done, the UI has been fully updated.
渲染完成后,将开始第二次重新渲染,但现在 deferredCount 已更新为匹配 count1 值。这意味着 SlowStuff 这次将重新渲染。总而言之,UI 已完全更新。

Why go through all that song and dance?? You might be thinking that this seems unnecessarily complicated, that it's a lot of work to wind up in the same place as before.
为什么要经历所有这些歌曲和舞蹈?您可能会认为这似乎不必要地复杂,要在与以前相同的地方结束需要做很多工作。

Here’s why this is so clever: If React gets interrupted by another state change, the important stuff has already been updated. React can abandon the less-important second render, and start work immediately on the more-important part.
这就是为什么它如此聪明:如果 React 被另一个状态更改中断,那么重要的东西已经被更新了。 React 可以放弃不太重要的第二次渲染,并立即开始处理更重要的部分。

This is hard to describe in words, but hopefully this recording will make it clearer:
这很难用语言来描述,但希望这段录音能让它更清楚:

count
1
dCount
0
count
1
dCount
1
count
2
dCount
0
count 数数
2
dCount 计数
2
count
3
dCount
0
count 数数
3
dCount 计数
3
1
10
20
30
40
50
60
70
80
90
100
110
120
130
140
150

Like we saw earlier, the user is clicking too fast for React to finish updating everything in time. But, because each re-render is split into high-priority and low-priority parts, React is still able to update the important part of the UI between clicks. When those extra clicks happen, React abandons its work-in-progress, but that’s fine since that work was low-priority.
就像我们之前看到的那样,用户点击速度太快,React 无法及时完成所有内容的更新。但是,由于每次重新渲染都分为高优先级和低优先级部分,React 仍然能够在单击之间更新 UI 的重要部分。当这些额外的点击发生时,React 会放弃正在进行的工作,但这没关系,因为该工作的优先级较低。

This is tricky business. If you’re feeling a bit overwhelmed, the next section should help, as we explore the underlying mechanism that allows this to work.
这是一件棘手的事情。如果您感到有点不知所措,下一节应该会有所帮助,因为我们将探索使其发挥作用的底层机制。

Link to this heading
Gotcha: memoization required
陷阱:需要记忆

An important thing to note: useDeferredValue only works when the slow / low-priority component has been wrapped with React.memo():
需要注意的重要一点: useDeferredValue 仅在慢速/低优先级组件被 React.memo() 包装时才有效:

js

React.memo() instructs React to only re-render this component when its props/state changes. Without React.memo(), SlowComponent would re-render whenever its parent component re-renders, regardless of whether the count prop has changed or not.
React.memo() 指示 React 仅在其 props/state 更改时重新渲染此组件。如果没有 React.memo()SlowComponent 每当其父组件重新渲染时都会重新渲染,无论 count 属性是否已更改。

This is a really important thing to understand, so let’s go over it in a bit more depth. As a reminder, this is the relevant code:
这是一件需要理解的非常重要的事情,所以让我们更深入地讨论一下。提醒一下,这是相关代码:

jsx

When the user clicks the button for the first time, the count state will increment from 0 to 1. The App component will re-render, but the useDeferredValue hook will re-use the previous value. deferredCount will be assigned to 0, not 1.
当用户第一次单击该按钮时, count 状态将从 0 增加到 1App 组件将重新渲染,但 useDeferredValue 钩子将重新使用以前的值。 deferredCount 将分配给 0 ,而不是 1

The default behaviour in React is for all child components to be re-rendered, regardless of whether their props have changed or not. Without React.memo(), both ImportantStuff and SlowStuff would re-render, and we wouldn't get any benefit from useDeferredValue.
React 中的默认行为是重新渲染所有子组件,无论它们的 props 是否已更改。如果没有 React.memo()ImportantStuffSlowStuff 都会重新渲染,并且我们不会从 useDeferredValue 中获得任何好处。

When we wrap SlowStuff with React.memo(), React will check to see if a re-render is actually necessary by comparing the current props with the previous ones. And since deferredCount is still 0, React says “Ok, nothing new here. This chunk of the UI doesn’t have to be recalculated”.
当我们用 React.memo() 包裹 SlowStuff 时,React 会通过将当前的 props 与之前的 props 进行比较来检查是否确实需要重新渲染。由于 deferredCount 仍然是 0 ,React 说“好吧,这里没有什么新内容。这部分 UI 不需要重新计算”。

This was the lightbulb moment for me. useDeferredValue allows us to postpone rendering the low-priority parts of our UI, pushing that work down the road like a really boring homework assignment. Eventually, that work will be done, and the UI will be fully updated. But it's on the back burner; whenever the state changes, React abandons that work and focuses on the more important stuff.
这对我来说是灵光一现的时刻。 useDeferredValue 允许我们推迟渲染 UI 的低优先级部分,将这项工作像一项非常无聊的家庭作业一样推进下去。最终,这项工作将会完成,用户界面也将得到全面更新。但现在它已经被搁置了。每当状态发生变化时,React 就会放弃这项工作并专注于更重要的事情。

Link to this heading
Gotcha: Working with multiple state variables
陷阱:使用多个状态变量

So, we’ve seen how useDeferredValue works with a single primitive value like count. But things in the real world are rarely so simple.
因此,我们已经了解了 useDeferredValue 如何与 count 这样的单个原始值一起使用。但现实世界中的事情很少如此简单。

In my “Shadow Palette Generator”, I have several pieces of relevant state:
在我的“阴影调色板生成器”中,我有几个相关状态:

jsx

My initial thought was that I'd need to create a deferred value for each one:
我最初的想法是,我需要为每个创建一个递延值:

jsx

I could do this, but there’s a simpler option. I can defer the derived value, the chunk of CSS code generated within the render:
我可以这样做,但还有一个更简单的选择。我可以推迟派生值,即渲染中生成的 CSS 代码块:

jsx

The hook is called useDeferredValue, not useDeferredState. There’s no rule that says the value has to be a state variable!
该钩子称为 useDeferredValue ,而不是 useDeferredState 。没有规则规定该值必须是状态变量!

This is why it’s so important to understand the underlying mechanism here. The critical thing is that our low-priority component (CodeSnippet in this case) doesn’t receive new values for any of its props during the high-priority render.
这就是为什么理解这里的底层机制如此重要。关键的是,我们的低优先级组件(在本例中为 CodeSnippet )在高优先级渲染期间不会收到其任何 props 的新值。

Link to this heading
Loading indications 装载指示

In some cases, we might want to make it clear to the user when parts of the UI are stale, so that they know that a re-calculation is in progress.
在某些情况下,我们可能希望让用户清楚 UI 的某些部分何时过时,以便他们知道正在进行重新计算。

For example, maybe we could do something like this:
例如,也许我们可以这样做:

While <SlowStuff> is out of date, we make it semi-transparent and include a little spinner. That way, the user knows that this part of the UI is recalculating.
虽然 <SlowStuff> 已经过时,但我们将其设为半透明并包含一个小微调器。这样,用户就知道 UI 的这一部分正在重新计算。

But hm, how can we tell whether part of the UI is stale or not? It turns out that we already have all the tools we need for this!
但是嗯,我们如何判断 UI 的一部分是否已过时?事实证明,我们已经拥有了为此所需的所有工具!

Here’s the code: 代码如下:

jsx

We can tell whether the UI is stale or not by comparing count and deferredCount.
我们可以通过比较 countdeferredCount 来判断 UI 是否过时。

When I first saw this, I thought it was suspiciously simple. But when I really thought about it, it made sense:
当我第一次看到这个时,我认为这非常简单。但当我认真思考时,我发现这是有道理的:

  • During the first high-priority render, deferredCount reuses the previous value. count gets updated to 1, but deferredCount is still 0. The values are different.
    在第一次高优先级渲染期间, deferredCount 重用之前的值。 count 更新为 1 ,但 deferredCount 仍然是 0 。价值观不同。
  • During the low-priority render that follows, deferredCount is updated to the current value, 1. Both count and deferredCount point to the same value.
    在随后的低优先级渲染期间, deferredCount 更新为当前值 1countdeferredCount 都指向相同的值。

The same mechanism that allows us to skip rendering <SlowStuff> on the first render also allows us to tell that the UI isn't fully in sync yet. Pretty cool, right?
允许我们在第一次渲染时跳过渲染 <SlowStuff> 的机制也允许我们知道 UI 尚未完全同步。很酷,对吧?

Now, whether we actually want to do this is another matter. I tested it out on my Shadow Palette Generator:
现在,我们是否真的想这样做是另一回事。我在我的阴影调色板生成器上测试了它:

Personally, I don’t think that this is an improvement in this case. It draws the user’s attention to the code snippet when it should stay fixed on the shadow figures.
就我个人而言,我不认为这在这种情况下是一种改进。当用户的注意力应该停留在阴影人物上时,它会将用户的注意力吸引到代码片段上。

Depending on the context, though, this could be a really useful way to make sure users know that part of the UI is stale!
不过,根据具体情况,这可能是一种非常有用的方法,可以确保用户知道部分 UI 已过时!

Link to this heading
Speeding up the initial render
加快初始渲染速度

A couple of weeks ago, React 19 entered beta. This upcoming major version is tweaking a bunch of stuff, and useDeferredValue is getting a nice new lil’ superpower!
几周前,React 19 进入测试阶段。这个即将到来的主要版本正在调整一堆东西, useDeferredValue 正在获得一个漂亮的新小超级大国!

Before React 19, useDeferredValue would get initialized to the supplied value:
在 React 19 之前, useDeferredValue 将被初始化为提供的值:

jsx

React doesn’t do the "double render" thing we’ve been talking about because React doesn’t have a previous value it can use. And so, effectively, useDeferredValue has no effect for the first render.
React 不会做我们一直在谈论的“双重渲染”事情,因为 React 没有可以使用的先前值。因此,实际上, useDeferredValue 对第一次渲染没有影响。

Starting in React 19, we can specify an initial value:
从 React 19 开始,我们可以指定一个初始值:

jsx

Why would we want to do this?? This pattern will allow us to potentially speed up the initial render.
我们为什么要这样做?这种模式将使我们有可能加快初始渲染速度。

For example, in our Shadow Palette Generator example, I could do something like this:
例如,在我们的阴影调色板生成器示例中,我可以执行以下操作:

jsx

During the quick high-priority render, deferredCssCode will be null, and so we won’t even render <CodeSnippet>. Immediately after that quick render, however, this component automatically re-renders, filling in that slot with the code.
在快速高优先级渲染期间, deferredCssCode 将是 null ,因此我们甚至不会渲染 <CodeSnippet> 。然而,在快速渲染之后,该组件会立即自动重新渲染,并用代码填充该槽。

This should allow the application as a whole to become responsive more quickly, since we don’t have to wait for less-important parts of the UI.
这应该允许整个应用程序更快地响应,因为我们不必等待 UI 中不太重要的部分。

Link to this heading
A world of difference 一个不同的世界

Alright, so with the useDeferredValue hook in place, check out what the end result looks like:
好的,添加了 useDeferredValue 钩子后,看看最终结果是什么样的:

So good! Everything is butter smooth. 💯
超好的!一切都如黄油般光滑。 💯

But hang on, I'm testing this on a high-end MacBook Pro. What is the experience like on a lower-end device?
但请稍等,我正在高端 MacBook Pro 上对此进行测试。在低端设备上体验如何?

A few years ago, I went into my local computer store and asked to buy the cheapest new Windows laptop they had. They dug up a US$110 Intel Celeron Acer laptop. Here’s how it runs on this machine, with useDeferredValue implemented:
几年前,我走进当地的电脑商店,要求购买他们拥有的最便宜的新型 Windows 笔记本电脑。他们挖出了一台价值 110 美元的英特尔赛扬宏碁笔记本电脑。以下是它在这台机器上的运行方式,并实现了 useDeferredValue

It’s not as smooth, but for a machine that struggles to open its own Start menu, this is pretty great! Notice that the code snippet doesn’t update until I've finished interacting with the controls. useDeferredValue is helping us a ton here.
虽然不是那么顺利,但对于一台难以打开自己的“开始”菜单的机器来说,这非常棒!请注意,在我完成与控件的交互之前,代码片段不会更新。 useDeferredValue 在这里帮了我们很多忙。

Like so much in React, useDeferredValue seems really complex unless you have the right mental model. Over the years, React has become a very sophisticated tool, and if we want to use it effectively, we need to develop an intuition for how it works.
就像 React 中的很多东西一样, useDeferredValue 看起来非常复杂,除非你有正确的思维模型。多年来,React 已经成为一个非常复杂的工具,如果我们想有效地使用它,我们需要对它的工作原理有一种直觉。

I spent nearly two years creating the ultimate resource for learning React. It's called The Joy of React. It covers everything I’ve learned after nearly a decade of professional React experience.
我花了近两年的时间创建了学习 React 的终极资源。这就是 React 的乐趣。它涵盖了我在近十年的专业 React 经验中学到的一切。

If you found this blog post helpful, you’ll get so much out of my course. The course is optimized for “lightbulb moments”, building a robust mental model for how React works, and how you can use it to build rich, dynamic web applications.
如果您发现这篇博文有帮助,您将从我的课程中获益匪浅。该课程针对“灵光一现”进行了优化,为 React 的工作原理以及如何使用它构建丰富的动态 Web 应用程序构建了强大的心理模型。

Visit the “Joy of React” homepage

I'm currently having a 🌸 Spring Sale.. The course is up to $200 off. The sale ends . You can learn more here:
我目前正在进行 ♥ 春季促销.. 该课程最高可享受 200 美元的折扣。大约18小时后销售结束。您可以在这里了解更多信息:

Thanks so much for reading! 💖
非常感谢您的阅读! 💖

Last Updated 最近更新时间

May 13th, 2024 2024 年 5 月 13 日

Hits 命中率

3D portrait of the blog's author, Josh Comeau

A front-end web development newsletter that sparks joy

My goal with this blog is to create helpful content for front-end web devs, and my newsletter is no different! I'll let you know when I publish new content, and I'll even share exclusive newsletter-only content now and then.

No spam, unsubscribe at any time.



If you're a human, please ignore this field.

3D portrait of the blog's author, Josh Comeau

Hi friend! Hope I didn't startle you. Can I let you know about my newsletter?
朋友你好!希望我没有吓到你。我可以让您了解我的时事通讯吗?