React 是当今最受欢迎的 JavaScript 框架之一,它的创新就是引入了虚拟 DOM 技术,但很多现代框架已经不再采用这种方案。Svelte 的创建者 Rich Harris 曾将其称作纯粹的开销。一位名为 Aidenybai 的高中生开发了一个名为 million.js 的轻量级(小于 4KB)虚拟 DOM 库,其可将 React 组件的性能提高多达 70%。那 million 是什么,又是如何让 React 的速度提高 70% 的呢?下面就来详细看看!
全文概览:
- Million.js 基本概念
- Million.js 使用步骤
- Million.js 打包体积
- Million.js 工作原理
- Million.js 使用场景
- 总结
Million.js基本概念
Million.js 提供了一个极致优化的虚拟 DOM,可以与 React 兼容。使用 Million 创建 Web 应用程序就像使用 React 组件一样简单(它只是一个包装 React 组件的高阶组件),但加载和渲染速度更快。Million.js 使用经过微调和优化的虚拟 DOM 减少了 React 的开销,就像 React 组件以纯 JavaScript 的速度运行一样。
一个高中生就这么超越了Meta 的整个顶级工程师团队?带着怀疑看了看性能基准对比:
数据不言自明,在第二张表中,内存消耗的差异更加显著,它清楚的显示了 Million 如何在内方面得到更好的优化。
那为什么 million.js 会如此之快呢?
React 默认的虚拟 DOM 是实际 DOM 的一种内存抽象。组件被存储在一个树形结构中,当状态发生变化时,React 会创建一个新的虚拟 DOM。接下来,将新的虚拟 DOM 树与旧的虚拟 DOM 树进行比较,找出两者之间的差异。最后,使用这些差异更新实际 DOM 树。这就是所谓的协调过程。但是,创建全新的虚拟 DOM 代价很大。
Million 通过使用块虚拟 DOM,采用了更加精简的方式。将应用程序中的动态部分提取出来并进行跟踪,当状态发生变化时,只对变化的部分进行 diff 操作。相比默认的虚拟 DOM,不需要对整个树进行 diff。由于 Million 跟踪了动态部分的位置,因此可以精确地找到并更新它们,这种方法与 Svelte 很相似。
Million.js使用步骤
在 使用 million 之前,首先需要创建一个 React 项目在,这里略过创建项目的过程。
或者也可以直接克隆官方提供的 React + Vite 项目模板(github.com/aidenybai/m…),打开项目根目录,依次执行 npm install
和 npm run dev
命令来启动项目,启动完成之后在浏览器输入 localhost:3000
就可以看到以下界面:
可以通过以下命令来安装 million 库:
javascript
复制代码
npm install million
使用方式很简单,在组件中引入 million,并在组件中使用:
import React, { useState } from 'react'; import { block } from 'million/react'; import './App.css'; function App() { const [count, setCount] = useState(0); return ( <div className="App"> <h1>Million + React</h1> <button onClick={() => setCount((count) => count + 1)}> count: {count} </button> </div> ); } const AppBlock = block(App) export default AppBlock
可以看到,组件从 million 中引入了 block()
,并使用 block()
包裹 App 组件。Million.js 可以让我们创建块(block),块是一种特殊的高阶组件,可以像 React 组件一样使用,但具有更快的渲染速度。
块的一个用例是高效地渲染数据列表。下面在 React 中构建一个数据网格。可以在组件中分别定义 (用于展示数据列表) 和
(用于输入展示列表的行数) 组件,使用
useState()
hook 存储要显示的行数。
import React, { useState } from 'react'; import './App.css'; function App() { const [rows, setRows] = useState(1); return ( <div> <Input value={rows} setValue={setRows} /> <Table> // ... </Table> </div> ); } export default App;
假设我们通过一个名为
buildData(rows)
的函数获取任意数据数组:
javascript
复制代码
const data = buildData(100);
// [{ adjective: '...', color: '...', noun: '...' }, ... x100]
现在可以使用
Array.map()
在表格中渲染数据:
import React, { useState } from 'react'; import './App.css'; function App() { const [rows, setRows] = useState(1); const data = buildData(rows); return ( <div> <Input value={rows} setValue={setRows} /> <Table> {data.map(({ adjective, color, noun }) => ( <tr> <td>{adjective}</td> <td>{color}</td> <td>{noun}</td> </tr> ))} </Table> </div> ); } export default App;
页面效果如下:
它的性能表现非常好。从 0 到 100,几乎没有延迟,但一旦超过 500 左右,渲染时就会有明显的延迟。
这时引入 million 来看看:
import React, { useState } from 'react'; import { block } from 'million/react'; import './App.css'; function App() { const [rows, setRows] = useState(1); const data = buildData(rows); return ( <div> <Input value={rows} setValue={setRows} /> <Table> {data.map(({ adjective, color, noun }) => ( <tr> <td>{adjective}</td> <td>{color}</td> <td>{noun}</td> </tr> ))} </Table> </div> ); } const AppBlock = block(App) export default AppBlock
此时,再添加超过 500 条数据时,页面渲染就会快很多。
除此之外,Million 还提供了其他实用工具,特别是用于高效地渲染列表。Million 并不推荐将传统的列表包装在
block
HOC 中进行渲染,而是推荐使用内置的 For
组件:
<For each={data}> ({ adjective, color, noun }) => ( <tr> <td>{adjective}</td> <td>{color}</td> <td>{noun}</td> </tr> ) </For>
Million.js打包体积
页面的执行性能非常重要,其初始加载也非常重要,其中一个重要因素就是项目的打包体积。这里我们使用 Million 和不使用它构建了相同的应用。
使用纯 React 的打包体积:
使用 Million 的打包体积:
可以看到,gzip 捆绑包大小的差异小于 5 kB,这意味着对于多数 React 应用来说,Million 对项目的体积影响可以忽略不计。
Million.js工作原理
最后,我们来看看 million 的工作原理。
React 的虚拟 DOM
虚拟 DOM 的产生是为了解决频繁操作真实 DOM 带来的性能问题。它是真实 DOM 的轻量级、内存中的表示形式。当一个组件被渲染时,虚拟 DOM 会计算新状态和旧状态之间的差异(称为 diff 过程),并对真实 DOM 进行最小化的变化,使它与更新后的虚拟 DOM 同步(这个过程称为协调)。 下面来看一个例子,假设有一个 React 组件
:
function Numbers() { return ( <foo> <bar> <baz /> </bar> <boo /> </foo> ); }
当 React 渲染此组件时,它将经过检查更改的 diff 和更新 DOM 的协调过程。这个过程看起来像这样:
我们得到了两个虚拟 DOM:current(当前的),代表当前 UI 的样子,和 new(新的),代表想要看到的样子。
比较第一个节点,发现没有差异,继续比较下一个
比较第二个节点,发现有一个差异,在 DOM 中进行更新。比较第三个节点,发现它在新的虚拟 DOM 中已经不存在了,在 DOM 中将其删除。
比较第四个节点,发现它在新的虚拟 DOM 中已经不存在了,在 DOM 中将其删除。
比较第五个节点,发现有差异,在 DOM 中进行更新并完成了整个过程。
diff 过程取决于树的大小,最终导致虚拟 DOM 的性能瓶颈。组件的节点越多,diff 所需要的时间就越长。
随着像 Svelte 这样的新框架的出现,由于性能开销的问题,甚至不再使用虚拟 DOM。相反,Svelte 使用一种称为 "脏检查" 的技术来确定哪些内容已经发生了改变。类似 SolidJS 这样的精细响应式框架更进一步,精确定位于 DOM 中哪些部分发生了变化,并仅更新这部分内容。
高中生打破React性能极限,将React性能提升70%!(下)https://developer.aliyun.com/article/1411539