果肉教育:
react的虚拟dom的patch算法,时间复杂度
React Virtual DOM 的 Diffing 算法(即 Reconciliation 协调过程)的时间复杂度是 O(n),其中 n 是树中节点的数量。
这里的 “Patch” 过程,在 React 中更准确的叫法是 “Reconciliation”(协调) 或 “Diffing”(对比) 。Virtual DOM 的 Diff 结果是“需要更新的补丁”,然后 React 再将这个“补丁”应用到真实的 DOM 上,这个过程可以看作是“Patching”。
为什么是 O(n)?—— 基于两个核心假设
传统的“两棵树差异”算法,最先进的也需要 O(n³) 的时间复杂度,这对于 Web UI 来说是完全不可接受的。React 通过以下两个策略,将复杂度降低到了 O(n) :
-
相同类型的组件生成相似的树结构,不同类型的组件会生成完全不同的树。
- 如果两个组件的类型不同(例如从
<div>变成了<span>,或者从ComponentA变成了ComponentB),React 会直接销毁整个旧子树并重建新子树,不会再去递归比较它们的子节点。这节省了大量计算。
- 如果两个组件的类型不同(例如从
-
开发者可以通过
key属性,来标识哪些子元素在不同的渲染中可能是稳定的。- 当比较同一层级的一组子节点时(例如一个
ul下的多个li),如果没有key,React 会默认按照索引顺序进行比较。如果在列表头部插入一个元素,会导致后面所有元素都被认为发生了变化,从而进行不必要的更新和 DOM 操作。 - 提供了稳定且唯一的
key后,React 就能准确地匹配新旧列表中的对应节点,从而复用节点,只对移动、新增、删除的节点进行操作,大大提升了列表渲染的效率。
- 当比较同一层级的一组子节点时(例如一个
React 的 Diffing 算法具体是如何工作的?(O(n) 的实现)
React 的 Diff 算法并非对整个树进行深度优先搜索对比,而是分层比较。它只会对同一层级的节点进行比较,不会跨层级追踪节点的变化。
1. 对比不同类型的元素(根节点类型不同)
当根节点是不同类型的元素(如从 <a> 变成 <div>,或从 Article 变成 Comment 组件),React 会直接拆卸整个旧的 DOM 树并重建新的。
- 拆卸: 组件实例会执行
componentWillUnmount()。 - 重建: 新的 DOM 节点会被插入,组件实例会执行
constructor->componentWillMount->componentDidMount。 - 旧树的状态都会被销毁。
时间复杂度: 对于这棵子树,React 直接跳过比较,销毁和重建的成本是 O(1) 的决策,加上 O(k) 的销毁/重建操作(k 是子树大小),但整体算法流程不会因此陷入深度递归。
2. 对比同一类型的元素(根节点类型相同)
当比较两个相同类型的 React 元素时,React 会保留 DOM 节点,仅更新有变化的属性。
jsx
复制下载
// 更新前
<div className="before" title="stuff" />
// 更新后
<div className="after" title="stuff" />
React 会只修改 className 属性。
处理完当前节点后,React 会递归地更新其子节点。
3. 对比同层级的子节点列表(Keys 的作用)
这是 Diff 算法中最关键、最复杂的一部分。当处理一组子节点(如 ul 下的 li)时,React 默认使用一种“索引对比”的策略。
没有 Key 的情况(性能差):
jsx
复制下载
// 旧列表
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
// 新列表(在头部插入)
<ul>
<li>Connecticut</li> // 索引0,与旧li(Duke)对比,内容变了,更新
<li>Duke</li> // 索引1,与旧li(Villanova)对比,内容变了,更新
<li>Villanova</li> // 索引2,新增,插入
</ul>
React 看到 <li>Duke</li> 变成了 <li>Connecticut</li>,会更新这两个 li 的内容,然后插入第三个 li。这导致了两次更新和一次插入,效率低下。
有 Key 的情况(性能好):
jsx
复制下载
// 旧列表
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
// 新列表(在头部插入)
<ul>
<li key="2014">Connecticut</li> // 新增,插入
<li key="2015">Duke</li> // key匹配,节点可以复用,可能只需要移动
<li key="2016">Villanova</li> // key匹配,节点可以复用,可能只需要移动
</ul>
有了 key,React 知道 key="2015" 和 key="2016" 的节点只是移动了位置,可以复用它们对应的 DOM 节点和组件实例,只需要执行一次插入操作(将 Connecticut 插入到开头)。这极大地提高了性能。
React 使用了一种启发式算法来高效处理这种带 Key 的列表对比,它假设:
- 在列表头部添加比在中间随机排序更常见。
- 它会在一次遍历中,找出新增、删除和需要移动的节点。
这个列表对比算法的时间复杂度也是 O(n) 。
总结
| 特性 | 描述 | 对时间复杂度的贡献 |
|---|---|---|
| 分层比较 | 只比较同一层级的节点,不跨层级。 | 避免了 O(n³) 的树编辑距离计算。 |
| 组件类型策略 | 类型不同则直接销毁重建,跳过子树比较。 | 在结构发生重大变化时,避免了不必要的深度递归。 |
| Key 优化策略 | 使用唯一的 key 来稳定地标识列表项,实现节点的复用和高效移动。 | 将列表对比从 O(n²) 降低到 O(n)。 |
最终结果: 通过结合这三种策略,React 确保了对整棵树进行 Diff 的最坏情况时间复杂度为 O(n) ,而不是理论上的 O(n³),这使得 Virtual DOM 的 Diff 过程在大型应用中也具有可行的性能。
补充: 虽然算法是 O(n),但常数因子可能不小。对于非常庞大的应用,即使 O(n) 也可能成为性能瓶颈。这就是为什么 React 提供了 shouldComponentUpdate、PureComponent、React.memo 等 API,让开发者可以主动跳过不必要的子树的 Diff 过程,从而进一步优化性能。
react的为啥key更快?不用key会怎么样?
在 React 中,key 是虚拟 DOM 节点的唯一标识,其核心作用是帮助 React 的协调算法(patch 算法)更高效地识别新旧节点的对应关系,从而减少不必要的 DOM 操作。理解 key 的作用,需要结合 React 对比节点的逻辑来看:
1. 为什么用 key 会更快?
React 在对比同层级的子节点时,默认会按顺序逐一对比(即 “就地更新” 策略)。当子节点列表发生变化(如插入、删除、重排)时,若无 key,React 可能会误判节点的身份,导致大量冗余的 DOM 操作;而 key 能让 React 直接定位到 “相同” 节点,从而跳过不必要的销毁 / 创建操作,只做最小化更新。
举个例子:假设旧列表是 [A, B, C],新列表是 [B, C, D](在末尾添加 D,删除开头 A)。
-
无 key 时:React 会按顺序对比:
- 旧 A vs 新 B → 类型相同但内容不同 → 更新 A 为 B;
- 旧 B vs 新 C → 更新 B 为 C;
- 旧 C vs 新 D → 更新 C 为 D;结果:3 次 DOM 更新 + 1 次新增(无,因为长度相同),但其实 B、C 只是位置移动,却被强制更新了。
-
有 key 时(假设 A 的 key 是 'a',B 是 'b',C 是 'c',D 是 'd'):React 会通过 key 识别:
- 旧 'a' 不在新列表中 → 删除 A;
- 旧 'b'、'c' 在新列表中 → 仅移动位置(不更新内容);
- 新 'd' 不在旧列表中 → 创建 D;结果:1 次删除 + 2 次移动 + 1 次创建,避免了 B、C 的冗余更新。
2. 不用 key 会怎么样?
如果不指定 key,React 会默认使用索引(index)作为 key,但这在列表发生重排、新增 / 删除元素时会导致严重问题:
(1)性能下降
如上述例子,React 无法识别节点的真实身份,会对本可复用的节点进行销毁和重建,触发不必要的 DOM 操作和组件生命周期(或 Hooks)执行,浪费性能。
(2)状态错乱(最严重的问题)
当列表项包含自身状态(如输入框内容、复选框勾选状态)或子组件状态时,使用索引作为 key 会导致状态与节点 “错位”。
例:旧列表 [ {id:1, name: 'A'}, {id:2, name: 'B'} ](用索引 0、1 作 key),若删除第一个元素,新列表变为 [ {id:2, name: 'B'} ](索引变为 0)。React 会认为 “索引 0 的节点” 是同一个节点,从而复用旧节点的 DOM 和状态,但此时节点内容其实是 B,导致状态(如输入框内容)错误地保留了原 A 的状态。
3. 正确使用 key 的原则
key应在同层级节点中唯一(不同层级可重复);key应稳定不变(避免用随机数或索引,除非列表不会重排 / 增删);- 最好使用数据本身的唯一标识(如 id、uuid)作为 key。
总结:key 的作用是让 React 快速定位可复用的节点,避免冗余 DOM 操作和状态错乱。不用 key 时,React 会用索引替代,在列表变动时导致性能下降和状态异常。
使用稳定 key 的好处:
-
性能最优:
- 只有 1次 DOM 插入(新任务),其他三个节点只是被移动。
- 完美地复用了所有已有的 DOM 节点和组件实例。如果这些组件有内部状态(如复选框的选中状态、输入框的内容),状态会被完美保留。
-
正确性保证:
- 完全避免了状态错乱的问题。因为
key是基于数据本身的,而不是它在数组中的位置。
- 完全避免了状态错乱的问题。因为
总结对比表
| 方面 | 不用 key / 用 index 作为 key | 使用稳定的 key (如 id) |
|---|---|---|
| 性能 | 差。列表顺序变化时,导致大量不必要的更新和重建。 | 优。最大程度复用节点,只处理新增/删除/移动。 |
| 状态保持 | 会错乱。组件状态与错误的元素绑定。 | 正确保持。状态始终与正确的数据绑定。 |
| 适用场景 | 静态列表:永不重新排序、过滤或在中部增删的列表。 | 动态列表:任何可能发生顺序变化、增删的列表。最佳实践是总是使用。 |
结论: 为了应用的性能和正确性,永远不要使用数组索引 index 作为 key,除非你能 100% 保证它是一个静态的、永不变化的列表。请始终使用数据中唯一且稳定的标识(如 id)作为 key。
react的vnode是什么样的?数据结构是什么?
- 轻量对象:React VNode 是一个普通的 JS 对象,远比真实 DOM 轻量。
- 核心属性:
type和props是核心,定义了要渲染什么以及它的属性。 - 关键辅助:
key和ref是特殊的、不包含在props中的属性,用于优化和访问。 - 安全机制:
$$typeof是一个重要的安全特性。 - 树形结构:通过
props.children嵌套,形成描述 UI 的虚拟 DOM 树。
这个简洁而强大的数据结构,是 React 高效 Diff 算法和声明式编程模型的基础。在 React 16+ 的 Fiber 架构中,这个 Element 对象会被进一步转化为 Fiber 节点,但 Element 仍然是 React 用来描述 UI 的初级产物。
在 React 中,VNode(Virtual Node,虚拟节点) 是对真实 DOM 节点的轻量级 JavaScript 对象描述,它包含了渲染真实 DOM 所需的所有信息。React 通过操作 VNode 来减少直接操作真实 DOM 的开销,最终通过 patch 算法将 VNode 的变更映射到真实 DOM 上。
VNode 的核心数据结构
React 的 VNode 本质是一个 JavaScript 对象,不同类型的节点(如元素、文本、组件等)会有不同的属性,但核心结构一致。以下是 VNode 中最关键的属性(简化版):
javascript
运行
{
// 1. 节点类型(核心标识)
type: 'div', // 或组件构造函数、Symbol(如 Fragment 是 Symbol(react.fragment))
// 2. 节点属性(对应真实 DOM 的属性/ props)
props: {
className: 'container',
onClick: () => {},
children: [...] // 子节点列表(嵌套的 VNode)
},
// 3. 唯一标识(用于协调算法)
key: null, // 或字符串/数字(即我们在列表中指定的 key)
// 4. 其他内部属性(React 内部使用)
ref: null, // 用于获取真实 DOM 或组件实例
_owner: null, // 关联的组件实例(内部追踪用)
// ... 其他内部字段(如 __vIsRef、_store 等,避免直接操作)
}
不同类型的 VNode
根据 type 字段的不同,VNode 可分为以下几类,各自的结构略有差异:
-
原生 DOM 元素节点
type为字符串(如'div'、'span'、'input')。props包含 HTML 属性(className、style)、事件(onClick)、children等。示例:
javascript
运行
{ type: 'div', props: { className: 'box', children: [ { type: 'span', props: { children: 'Hello' } } ] }, key: null } -
组件节点
type为组件构造函数(类组件)或函数(函数组件)。props为组件接收的参数(包含children作为子组件)。示例(函数组件):
javascript
运行
function Button({ text }) { return <button>{text}</button>; } // 对应的 VNode { type: Button, // 函数组件本身 props: { text: 'Click me' }, key: null } -
文本节点
- 没有
type和props,直接用字符串或数字表示(是一种简化的 VNode)。示例:'Hello World'或42(在 React 中会被当作文本节点的 VNode)。
- 没有
-
特殊节点
- Fragment:
type为Symbol(react.fragment),用于表示片段(不渲染额外 DOM 容器),props可能包含key,children为子节点列表。 - Portal:
type为Symbol(react.portal),用于将子节点渲染到指定 DOM 容器,props包含container(目标容器)和children。 - 注释节点:
type为Symbol(react.comment),表示 HTML 注释。
- Fragment:
VNode 的核心作用
- 描述 UI 结构:用 JavaScript 对象抽象真实 DOM,避免频繁操作昂贵的真实 DOM。
- 支持跨平台:VNode 不依赖具体平台(浏览器 DOM、Native 等),React 通过不同的渲染器(如 ReactDOM、React Native)将 VNode 转换为对应平台的真实元素。
- 高效 diff 计算:通过对比新旧 VNode 的
type、key、props等信息,快速定位差异,减少更新成本。
总结:React 的 VNode 是一个包含 type、props、key 等核心属性的 JavaScript 对象,用于描述不同类型的节点(元素、组件、文本等)。它是虚拟 DOM 的基础,也是 React 高效更新和跨平台能力的关键。
react的vnode是生成一个树吗?
虚拟 DOM 树 vs Fiber 树
需要区分两个概念:
虚拟 DOM 树(React Element Tree)
- 用途:描述「UI 应该长什么样」
- 特点:不可变的、声明式的、每次渲染都会重新创建
- 生命周期:短暂存在,主要用于 Diff 比较
Fiber 树(React 16+)
- 用途:描述「组件的工作单元和状态」
- 特点:可变的、包含状态和副作用的、有明确指针连接的链表结构
- 生命周期:长期存在,贯穿组件整个生命周期
| 方面 | React Element Tree (虚拟DOM) | Fiber Tree |
|---|---|---|
| 目的 | 描述 UI 的期望状态 | 协调和调度渲染工作 |
| 结构 | 通过 props.children 嵌套的逻辑树 | 通过 child, sibling, return 连接的链表 |
| 可变性 | 不可变 - 每次渲染重新创建 | 可变 - 长期存在,可重用 |
| 创建时机 | 每次 render 时创建 | 在协调阶段创建和更新 |
| 用途 | Diff 算法的输入 | 渲染调度、状态管理、生命周期 |
结论:React 确实会生成虚拟 DOM 树,但这棵树是短暂的、不可变的、通过组合形成的逻辑树,主要用于 Diff 比较。而在底层,React 使用更复杂的 Fiber 树结构来管理组件的状态和渲染调度。
react的每个组件有一个vnode树吗?
为什么说「每个组件没有独立的 VNode 树」?
-
组件返回的是子树,不是完整的树
Header返回<h1>,不是一棵以Header为根的树TodoItem返回<li>,不是一棵以TodoItem为根的树
-
所有组件共同构建一棵应用级的虚拟 DOM 树
- 这棵树的根节点是你渲染的根组件(如
<App />) - 每个组件贡献这棵树的一个分支或叶子
- 这棵树的根节点是你渲染的根组件(如
-
组件的「边界」在虚拟 DOM 中是通过组件节点表示的
- 组件节点标记了「这里需要执行一个函数来获取真正的 UI」
- 但组件节点本身只是整棵树中的一个节点
类比理解
把虚拟 DOM 树想象成一棵公司组织架构树:
- 整个公司 = 完整的虚拟 DOM 树
- 每个部门 = 一个组件
- 部门经理 = 组件的 VNode(如
{ type: Header, props: ... }) - 部门成员 = 组件返回的 VNode(如
<h1>,<li>等)
每个部门(组件)负责管理自己的团队成员(返回的 UI),但所有部门共同组成了整个公司(应用)。你不能说每个部门都有一棵独立的公司树。
总结
| 概念 | 正确理解 | 错误理解 |
|---|---|---|
| 组件与 VNode | 组件是返回 VNode 的函数 | 组件「拥有」一棵 VNode 树 |
| 虚拟 DOM 树 | 整个应用有一棵虚拟 DOM 树 | 每个组件有一棵虚拟 DOM 树 |
| 组件边界 | 在虚拟 DOM 中用「组件节点」标记 | 在虚拟 DOM 中用「子树根」标记 |
| 渲染过程 | 递归地展开所有组件节点,形成最终的 DOM 树 | 分别渲染每棵组件树,然后组合 |
正确答案:React 应用只有一棵虚拟 DOM 树,每个组件通过返回 VNode 来贡献这棵树的一部分。组件的「边界」是通过虚拟 DOM 中的组件节点来表示的,而不是通过独立的子树。
如何减少无效渲染?什么是无效渲染?
在 React 中,无效渲染(Unnecessary Renders) 指的是组件在不需要更新时却触发了重新渲染(即 render 方法或函数组件重新执行),导致额外的虚拟 DOM 生成和对比操作,浪费性能。无效渲染是前端性能优化中常见的优化点,尤其在复杂组件树中可能显著影响体验。
一、什么是无效渲染?
当组件的 props 和 state 没有发生变化时,若该组件仍执行了渲染逻辑(生成新的 VNode),则称为无效渲染。例如:
- 父组件更新时,即使子组件的
props未变,子组件也可能被连带重新渲染。 - 组件依赖的状态未变化,但因其他不相关状态更新而触发重渲染。
这些情况会导致 React 做无意义的虚拟 DOM 对比(虽然最终不会更新真实 DOM,但生成 VNode 和对比的过程仍消耗 CPU)。
二、如何减少无效渲染?
减少无效渲染的核心思路是:让组件只在依赖的 props 或 state 真正变化时才重新渲染。常见方案如下:
1. 使用 React.memo 缓存组件(针对函数组件)
React.memo 是一个高阶组件,用于缓存函数组件的渲染结果。当组件的 props 未发生浅对比变化时,会直接复用上次的渲染结果,避免重新执行组件函数。
javascript
运行
// 未优化:父组件更新时,即使 props 不变,Child 也会重渲染
function Child({ name }) {
console.log('Child 渲染了');
return <div>{name}</div>;
}
// 优化后:使用 React.memo 包装,仅在 name 变化时重渲染
const MemoizedChild = React.memo(Child);
// 使用时直接替换为 MemoizedChild
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>计数 {count}</button>
<MemoizedChild name="不变的名字" /> {/* 父组件更新时,Child 不会重渲染 */}
</div>
);
}
-
注意:
React.memo默认对props做浅对比(比较引用类型的地址)。若props包含复杂对象(如数组、对象),需手动传入第二个参数(自定义对比函数):javascript
运行
// 自定义 props 对比逻辑(深对比关键字段) const MemoizedChild = React.memo(Child, (prevProps, nextProps) => { return prevProps.user.id === nextProps.user.id; // 仅当 user.id 变化时才重渲染 });
2. 类组件使用 shouldComponentUpdate 或 PureComponent
-
shouldComponentUpdate:手动控制组件是否重渲染,返回false则阻止渲染。javascript
运行
class Child extends React.Component { // 仅当 name 变化时才允许重渲染 shouldComponentUpdate(nextProps) { return nextProps.name !== this.props.name; } render() { console.log('Child 渲染了'); return <div>{this.props.name}</div>; } } -
PureComponent:是Component的子类,内置了对props和state的浅对比逻辑,相当于自动实现了shouldComponentUpdate(浅对比版本)。javascript
运行
class Child extends React.PureComponent { // 无需手动写 shouldComponentUpdate,自动浅对比 props 和 state render() { console.log('Child 渲染了'); return <div>{this.props.name}</div>; } }
3. 避免传递不稳定的 props(核心原则)
很多无效渲染的根源是父组件传递了每次渲染都会生成新引用的 props(如匿名函数、字面量对象 / 数组),导致 React.memo 或 PureComponent 的浅对比失效(认为 props 变化了)。
反例:
javascript
运行
function Parent() {
const [count, setCount] = useState(0);
// 每次 Parent 渲染,都会创建新的对象 { id: 1 } 和新的函数 () => {}
return <Child user={{ id: 1 }} onClick={() => {}} />;
}
即使 Child 被 React.memo 包装,因 user 和 onClick 的引用每次都变,Child 仍会无效渲染。
优化方案:
- 缓存函数:用
useCallback(函数组件)或bind(类组件)缓存函数引用。 - 缓存对象 / 数组:用
useMemo(函数组件)缓存复杂数据的引用。
javascript
运行
function Parent() {
const [count, setCount] = useState(0);
// 缓存函数:仅当依赖变化时才重新创建
const handleClick = useCallback(() => {}, []);
// 缓存对象:仅当依赖变化时才重新创建
const user = useMemo(() => ({ id: 1 }), []);
return <Child user={user} onClick={handleClick} />;
}
4. 拆分组件,隔离更新范围
将组件拆分为更小的单元,让状态变化仅影响局部组件,避免 “牵一发而动全身”。
例如,一个包含列表和筛选器的组件,可拆分为:
Filter组件(管理筛选状态)List组件(接收筛选后的数据,用React.memo缓存)
这样,Filter 状态变化时,仅 Filter 重渲染,List 因 props 未变而复用缓存。
5. 使用状态提升或 Context 优化(谨慎使用)
- 若多个组件依赖同一状态,可将状态提升到共同父组件,避免重复维护状态导致的无效更新。
- 若使用
Context,注意Context.Provider的value变化会导致所有消费该 Context 的组件重渲染。可将value拆分为多个独立 Context,或用useMemo缓存value引用。
三、如何检测无效渲染?
- React DevTools:在 “Profiler” 标签中记录渲染过程,查看哪些组件在不必要时重新渲染。
- 手动日志:在组件中添加
console.log,观察渲染触发时机。
总结:无效渲染是组件在 props/state 未变时的多余渲染,可通过 React.memo/PureComponent 缓存组件、useCallback/useMemo 稳定 props 引用、拆分组件等方式减少,核心是让组件只在依赖变化时更新。
react的shouldcomponentupdate调用的时候会发生哪些?有哪些生命周期钩子被调用?
在 React 类组件中,shouldComponentUpdate 是一个关键的生命周期方法,用于决定组件是否需要执行重新渲染。它的调用时机和后续触发的生命周期钩子有明确的顺序,理解这一过程有助于更好地控制组件更新逻辑。
一、shouldComponentUpdate 的调用时机
shouldComponentUpdate 在组件可能发生更新时被调用,具体场景包括:
- 组件自身的
state发生变化(调用setState后)。 - 父组件传递的
props发生变化(父组件重渲染导致子组件接收新props)。 - 强制更新(调用
forceUpdate时,shouldComponentUpdate会被跳过,直接进入重渲染)。
二、shouldComponentUpdate 调用时的流程
当组件触发更新(如 setState 或 props 变化),React 会按以下顺序执行生命周期钩子,shouldComponentUpdate 处于核心决策位置:
1. 触发更新前的钩子(准备阶段)
- **
componentWillReceiveProps(已废弃,不推荐使用)**旧版本中,当父组件传递新props时触发,用于在props变化时更新state。注意:React v16.3 后推荐用getDerivedStateFromProps替代,因其可能导致不可预测的副作用。 - **
static getDerivedStateFromProps(nextProps, prevState)**静态方法,在shouldComponentUpdate之前调用(无论更新来自props还是state)。作用:根据新props计算并返回新的state(用于同步props到state),返回null则不更新state。
2. 核心决策:shouldComponentUpdate(nextProps, nextState)
-
参数:
nextProps(即将接收的新props)、nextState(即将更新的新state)。 -
返回值:
boolean类型。- 返回
true(默认):允许组件重渲染,继续执行后续渲染相关钩子。 - 返回
false:阻止重渲染,后续所有渲染相关钩子(如render、componentDidUpdate)都不会执行。
- 返回
-
核心作用:通过对比
nextProps与当前props、nextState与当前state,决定是否需要更新,避免无效渲染。
3. 若 shouldComponentUpdate 返回 true(允许重渲染),则执行以下钩子:
- **
componentWillUpdate(已废弃,不推荐使用)**旧版本中在render前调用,用于准备更新(如获取 DOM 信息)。注意:此处不能调用setState(会触发无限循环),React 推荐用getSnapshotBeforeUpdate替代。 - **
render()**执行渲染逻辑,生成新的虚拟 DOM 树(VNode 树)。React 会对比新旧虚拟 DOM,计算差异并准备更新真实 DOM。 - **
getSnapshotBeforeUpdate(prevProps, prevState)**在真实 DOM 更新前调用(render之后,DOM变化之前)。作用:捕获 DOM 更新前的状态(如滚动位置),返回的值会作为参数传递给componentDidUpdate。 - React 执行真实 DOM 更新根据虚拟 DOM 的差异,批量更新真实 DOM(此过程用户不可见,由 React 内部处理)。
- **
componentDidUpdate(prevProps, prevState, snapshot)**在真实 DOM 更新完成后调用。作用:执行更新后的副作用(如操作 DOM、发起网络请求),可通过prevProps/prevState与当前值对比,避免重复操作。(snapshot为getSnapshotBeforeUpdate的返回值,可选)
4. 若 shouldComponentUpdate 返回 false(阻止重渲染):
上述第 3 步中的所有钩子(componentWillUpdate、render、getSnapshotBeforeUpdate、componentDidUpdate)都不会执行,组件保持当前状态。
三、总结:shouldComponentUpdate 相关生命周期顺序
当组件触发更新时,完整流程(含 shouldComponentUpdate)如下:getDerivedStateFromProps → shouldComponentUpdate(返回 true)→ render → getSnapshotBeforeUpdate →(DOM 更新)→ componentDidUpdate
若 shouldComponentUpdate 返回 false,则流程在其之后终止,不执行后续渲染步骤。
通过 shouldComponentUpdate 精准控制组件更新,是类组件性能优化的核心手段之一(类似函数组件中的 React.memo)。
| 场景 | 调用顺序 | 结果 |
|---|---|---|
正常更新 (SCU 返回 true) | getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → (DOM 更新) → componentDidUpdate | 完整更新 |
优化更新 (SCU 返回 false) | getDerivedStateFromProps → shouldComponentUpdate → 停止 | 跳过渲染和提交,但 props/state 已更新 |
强制更新 (forceUpdate()) | getDerivedStateFromProps → 跳过 SCU → render → getSnapshotBeforeUpdate → (DOM 更新) → componentDidUpdate | 强制完整更新,忽略 SCU |
关键要点:
shouldComponentUpdate是一个「阀门」:它决定是否继续后续的渲染和 DOM 更新。- 状态已更新:即使在
shouldComponentUpdate中返回false,组件实例的this.props和this.state也已经被新的值替换了。 getDerivedStateFromProps总是执行:它在每次渲染前都会被调用,无论shouldComponentUpdate返回什么。forceUpdate是例外:它会绕过优化阀门shouldComponentUpdate。
react的setstate之后会发生哪些操作?
阶段一:初始化和安排工作
-
setState调用- 你将新的状态数据传入
setState。它可以是一个对象,也可以是一个函数(prevState, props) => newState。
- 你将新的状态数据传入
-
状态更新入队与批处理
- React 不会立即更新状态和执行渲染。它会将这次更新放入一个更新队列中。
- 批处理:如果在同一个同步事件循环中(例如一个事件处理函数内)多次调用
setState,React 会聪明地将它们合并成一次更新,以避免不必要的重复渲染。
-
安排渲染工作
- React 通知 React Scheduler 有一个组件需要更新。
- Scheduler 会根据元素的优先级(由 React 18+ 的并发特性使用)和浏览器的空闲时间,来安排协调工作的执行时机。这确保了高优先级的更新(如用户输入)能更快得到响应。
阶段二:协调阶段 - 找出变化
这个阶段是 React 的核心,它创建了一棵新的虚拟 DOM 树(Fiber 树),并通过 Diffing 算法找出需要更新的部分。此阶段可以被中断,以允许浏览器处理更高优先级的任务。
-
开始协调
-
React 从组件的根节点开始,遍历 Fiber 树(React 内部用于描述组件树和工作的数据结构)。
-
对于类组件,React 会按顺序调用以下生命周期方法(如果定义了):
static getDerivedStateFromProps(props, state)- 一个不常用的生命周期,用于根据 props 派生 state。shouldComponentUpdate(nextProps, nextState)- 性能优化关键!你可以在这里通过返回false来阻止组件重新渲染。
-
-
渲染生成新的虚拟 DOM
- 如果
shouldComponentUpdate返回true(或未定义),React 会调用组件的render方法。 render方法返回新的 React Elements(虚拟 DOM) ,描述了组件最新的 UI 结构。
- 如果
-
Reconciliation / Diffing 算法
-
React 将新生成的虚拟 DOM 与旧的虚拟 DOM 进行对比。
-
它递归地比较新旧子元素,找出确切发生变化的部分(例如:文本内容改变、属性更新、元素增删等)。
-
React 不会直接操作真实 DOM,而是为发生变化的 Fiber 节点标记上 “副作用” ,例如:
Placement- 需要插入新节点Update- 需要更新现有节点Deletion- 需要删除节点
-
阶段三:提交阶段 - 应用变化到真实 DOM
此阶段 React 会同步地、不可中断地执行,将协调阶段收集到的所有副作用一次性应用到真实 DOM 上,以确保 UI 的一致性。
-
getSnapshotBeforeUpdate生命周期- 在真实的 DOM 更新之前,React 会调用这个生命周期方法。它允许你在 DOM 可能发生变化之前捕获一些信息(例如当前的滚动位置)。
-
将副作用刷新到真实 DOM
-
React 执行所有被标记的 DOM 操作:
- 插入新节点
- 更新节点的文本内容或属性(如
className,style) - 删除废弃的节点
-
注意:到这一步,浏览器的屏幕上还没有显示更新后的 UI。
-
-
componentDidUpdate生命周期-
在真实的 DOM 更新之后,React 会立即调用这个生命周期方法。
-
这是执行副作用操作的理想位置,例如:
- 基于新的 DOM 布局进行计算
- 发送网络请求
- 手动操作 DOM
-
阶段四:浏览器渲染
-
浏览器重绘
- 当 React 完成所有 DOM 操作后,浏览器会触发 重排 和 重绘,最终将更新后的像素点绘制到屏幕上。
- 此时,用户才真正看到了更新的界面。
关键特性总结
| 特性 | 描述 |
|---|---|
| 异步性 | setState 是异步的,状态更新不会立即生效。 |
| 批处理 | 多个 setState 调用可能在一次渲染中合并。 |
| 可中断的协调 | React 可以在渲染阶段暂停工作,以处理更高优先级的任务。 |
| 两阶段提交 | 协调阶段找出变化,提交阶段同步应用变化到 DOM。 |
简单比喻:
想象一个建筑师(协调阶段)先在一张蓝图上(虚拟DOM)修改设计,标记出所有需要改动的地方(副作用)。然后,施工队(提交阶段)拿着这张标记好的蓝图,一次性进入工地(真实DOM),同步完成所有改动,避免工地长时间处于混乱状态。
redux是如何让一个state只更新某个组件的?这些state是通过connect加进来
Redux 实现 “特定 state 变化只更新对应组件” 的核心逻辑,是通过 connect 高阶组件与状态订阅机制 共同实现的。connect 不仅负责将 Redux 的 state 映射到组件的 props,更关键的是它会精准控制组件是否需要重新渲染 —— 只有当组件依赖的那部分 state 发生变化时,才会触发更新。
一、核心原理:connect 如何 “过滤” 无关的 state 变化?
connect 的工作流程可以简化为 3 步,其中第 2 步是控制组件更新的关键:
-
映射 state 和 dispatch 到 props通过
mapStateToProps函数,从 Redux 的全局 state 中提取当前组件需要的部分(例如state.user或state.cart),并映射为组件的 props;通过mapDispatchToProps函数,将 dispatch 方法包装为组件可调用的 props(如this.props.addItem)。 -
判断 “依赖的 state 是否变化”
connect内部会缓存上一次mapStateToProps返回的结果(即组件依赖的那部分 state)。当 Redux 的全局 state 发生变化时:- 首先重新执行
mapStateToProps,得到新的 props; - 然后对比新 props 与缓存的旧 props(浅对比,即比较引用是否相同);
- 若两者相同(说明组件依赖的 state 没变化),则阻止组件重新渲染;
- 若不同(依赖的 state 变了),则触发组件更新,传入新的 props。
- 首先重新执行
-
订阅 Redux store 的变化
connect会让组件订阅 Redux store 的更新(通过store.subscribe)。当 store 中的 state 发生任何变化时,所有订阅了 store 的组件都会收到通知,但最终是否更新,由第 2 步的对比结果决定。
二、举例说明:为什么无关 state 变化不会触发组件更新?
假设 Redux 的全局 state 结构如下:
javascript
运行
{
user: { name: 'Alice' }, // 组件 A 依赖这部分
cart: { items: [] } // 组件 B 依赖这部分
}
-
组件 A 通过
connect关联user:javascript
运行
const mapStateToProps = (state) => ({ userName: state.user.name // 只依赖 state.user }); export default connect(mapStateToProps)(ComponentA); -
当
cart发生变化时(例如添加商品):- Redux store 触发更新,通知所有订阅的组件(包括组件 A 和 B);
- 组件 A 执行
mapStateToProps,得到的userName与上次相同(因为user没变化); connect对比后发现 props 未变,因此阻止组件 A 重新渲染;- 组件 B 因依赖的
cart变化,mapStateToProps返回新 props,触发重新渲染。
三、关键细节:mapStateToProps 与浅对比的影响
-
**
mapStateToProps必须返回 “稳定的引用”**如果mapStateToProps每次执行都返回新的对象 / 数组(即使内容相同),会导致浅对比失效,触发无效渲染。例如:javascript
运行
// 错误示例:每次调用都返回新对象(引用不同) const mapStateToProps = (state) => ({ user: { name: state.user.name } // 即使内容相同,对象引用也不同 });解决方式:尽量返回 state 中的原始引用(如
return { user: state.user }),或使用reselect库缓存计算结果。 -
reselect优化:避免重复计算与无效渲染reselect可以创建 “记忆化选择器”,只有当依赖的 state 片段变化时,才会重新计算结果,否则直接返回缓存值,确保mapStateToProps返回的引用稳定。javascript
运行
import { createSelector } from 'reselect'; // 基础选择器:获取 state.user const selectUser = (state) => state.user; // 记忆化选择器:只有 user 变化时才重新计算 const selectUserName = createSelector( [selectUser], (user) => user.name ); // 用记忆化选择器映射 props const mapStateToProps = (state) => ({ userName: selectUserName(state) });
四、总结
Redux 让 “特定 state 只更新对应组件” 的核心逻辑是:
connect让组件只订阅自己依赖的那部分 state(通过mapStateToProps提取);- 当全局 state 变化时,
connect对比组件依赖的新旧 state(浅对比); - 只有当依赖的 state 确实变化时,才触发组件重新渲染,否则跳过更新。
这种机制确保了 Redux 全局 state 的变化不会导致所有组件盲目更新,而是精准作用于依赖它的组件,避免性能浪费。
Redux 通过 React-Redux 的 connect 或 useSelector 实现了高效的更新。其核心是 「订阅 + 精细比较」 机制:组件只订阅它需要的数据,并且只有在该数据真正发生变化时才会重新渲染。这确保了全局状态管理下的高性能。
react的purecomponent是什么?
React.PureComponent 是 React 类组件的一个内置子类,它的核心作用是自动优化组件的重渲染逻辑,避免因 props 或 state 未发生实质变化而导致的无效渲染(Unnecessary Renders)。
1. PureComponent 的核心原理
PureComponent 与普通 Component 的最大区别在于:它内置了一个默认的 shouldComponentUpdate 生命周期方法,该方法会对组件的新 / 旧 props 和 state 进行浅对比(Shallow Comparison) ,只有当对比发现差异时,才允许组件重渲染。
-
浅对比规则:
- 对于基本类型(字符串、数字、布尔值等):直接比较值是否相等(如
1 === 1、'a' === 'a')。 - 对于引用类型(对象、数组、函数等):只比较引用地址是否相同(如
{a:1}与{a:1}因引用不同会被视为 “变化”)。
- 对于基本类型(字符串、数字、布尔值等):直接比较值是否相等(如
2. 与普通 Component 的对比
- 普通
Component:默认不会做任何对比,只要触发setState或父组件重渲染,就会执行render方法(即使props/state没变)。 PureComponent:通过内置的shouldComponentUpdate浅对比props和state,若两者均未变化,则跳过render,避免无效渲染。
3. 使用示例
javascript
运行
// 普通 Component:即使 props 未变,也会重渲染
class NormalComponent extends React.Component {
render() {
console.log('NormalComponent 渲染了');
return <div>{this.props.name}</div>;
}
}
// PureComponent:仅当 props 或 state 浅对比变化时才渲染
class PureComp extends React.PureComponent {
render() {
console.log('PureComp 渲染了');
return <div>{this.props.name}</div>;
}
}
// 父组件
class Parent extends React.Component {
state = { count: 0 };
handleClick = () => {
this.setState({ count: this.state.count + 1 }); // 仅更新 count,不影响子组件 props
};
render() {
return (
<div>
<button onClick={this.handleClick}>计数 {this.state.count}</button>
<NormalComponent name="测试" /> {/* 每次父组件更新都会重渲染 */}
<PureComp name="测试" /> {/* 因 name 未变,不会重渲染 */}
</div>
);
}
}
上述例子中,点击按钮时父组件的 count 变化会触发自身重渲染:
NormalComponent会被连带重渲染(即使name未变);PureComp因props.name未变化(浅对比相同),会跳过重渲染。
4. 注意事项(避坑点)
(1)浅对比的局限性
PureComponent 的浅对比无法检测引用类型内部的变化。例如:
javascript
运行
class MyPureComp extends React.PureComponent {
render() {
return <div>{this.props.user.name}</div>;
}
}
// 父组件中,若这样更新 props:
this.setState(prev => ({
user: { ...prev.user, name: 'New Name' } // 新对象(引用变化)→ PureComp 会更新
}));
// 但若直接修改原对象(引用不变):
this.state.user.name = 'New Name';
this.setState({ user: this.state.user }); // 引用未变 → PureComp 不会更新(错误!)
解决:更新引用类型时,应返回新的对象 / 数组(如使用扩展运算符 ...、map、filter 等),确保引用变化。
(2)避免传递不稳定的 props
若父组件传递的 props 包含 “每次渲染都会生成新引用” 的值(如匿名函数、字面量对象),会导致 PureComponent 误判为 “变化”,触发无效渲染:
javascript
运行
// 错误示例:每次渲染生成新函数和新对象
<PureComp
onClick={() => {}} // 新函数(引用变化)
data={{ id: 1 }} // 新对象(引用变化)
/>
解决:
- 用
bind在构造函数中绑定函数(固定引用); - 将对象 / 数组定义在组件外部(或用
state缓存),避免每次创建新引用。
(3)不适合频繁更新深层状态的组件
若组件依赖的状态嵌套较深(如 state.a.b.c),PureComponent 的浅对比无法高效检测变化,此时更适合手动实现 shouldComponentUpdate 做深层对比(但需注意性能成本)。
5. 与函数组件的 React.memo 对比
PureComponent用于类组件,自动对比props和state;React.memo用于函数组件,仅对比props(默认浅对比,可自定义对比函数)。两者本质都是通过 “对比依赖是否变化” 来避免无效渲染,只是适用的组件类型不同。
总结
React.PureComponent 是类组件的性能优化工具,通过内置的浅对比 props 和 state 自动阻止无效渲染。但需注意其浅对比的局限性,避免因引用类型处理不当导致的更新异常。对于简单组件或状态变化不复杂的场景,PureComponent 能显著提升性能。
react的hookS的useeffect第二个参数是什么?
在 React Hooks 中,useEffect 的第二个参数是一个依赖数组(dependency array) ,它用于控制 useEffect 回调函数的执行时机 —— 即决定回调函数在哪些情况下需要重新执行。
具体作用:
useEffect 的回调函数会在组件挂载后和依赖项变化时执行。第二个参数(依赖数组)的作用是指定 “哪些值发生变化时,需要重新运行回调” 。
- 若依赖数组为空
[]:回调函数只会在组件首次挂载后执行一次,后续组件更新时不再执行(相当于类组件的componentDidMount)。 - 若依赖数组包含特定值(如
[a, b]):回调函数会在组件挂载后执行一次,且每当数组中的a或b的值发生变化时(浅对比,即基本类型比较值,引用类型比较地址),都会重新执行。 - 若省略第二个参数:回调函数会在每次组件渲染后都执行(包括首次挂载和每次更新),相当于类组件的
componentDidMount + componentDidUpdate。
示例:
javascript
运行
function Example({ count, user }) {
// 1. 依赖数组为空:只在挂载后执行一次
useEffect(() => {
console.log('组件挂载了');
return () => console.log('组件卸载了'); // 清理函数
}, []);
// 2. 依赖 count:挂载后执行,且 count 变化时重新执行
useEffect(() => {
console.log(`count 变为:${count}`);
}, [count]);
// 3. 依赖 user:挂载后执行,且 user 引用变化时重新执行
useEffect(() => {
console.log('user 信息更新了');
}, [user]);
// 4. 省略依赖数组:每次渲染后都执行
useEffect(() => {
console.log('组件渲染了');
});
return <div>{count}</div>;
}
关键注意点:
-
浅对比规则:依赖数组中的值会进行浅对比。对于引用类型(如对象、数组),仅比较引用地址是否变化,而非内部属性。例如:
javascript
运行
const [user, setUser] = useState({ name: 'Alice' }); // 错误:直接修改原对象,引用不变,useEffect 不会触发 const handleClick = () => { user.name = 'Bob'; setUser(user); // 引用未变,[user] 依赖不会被视为变化 }; // 正确:返回新对象,引用变化,useEffect 会触发 const handleClick = () => { setUser({ ...user, name: 'Bob' }); // 新对象,引用变化 }; -
必须包含所有依赖:回调函数中使用的所有组件内变量(如
props、state、函数等),都必须加入依赖数组,否则可能导致回调函数捕获到旧值(闭包问题)。例如,若回调中使用了count但未加入依赖数组,当count变化时,回调可能仍使用旧的count值。
总结:
useEffect 的第二个参数是依赖数组,用于控制回调函数的执行时机:
- 空数组
[]:仅挂载时执行一次; - 包含特定值:挂载时执行,且值变化时重新执行;
- 省略:每次渲染后都执行。
合理设置依赖数组是避免 useEffect 无效执行或逻辑错误的关键。
有看过谷歌的performance吗?有哪些东西?
1. 控制条 & 设置
- 录制按钮:开始/停止录制性能分析。
- 清除:清除当前记录。
- 录制设置:非常重要!可以配置 CPU 节流(模拟慢速设备)、网络节流、是否捕获截图等。
- 加载性能分析文件:可以加载并查看别人导出的性能文件。
2. 概览面板
这是一个高维度的时间线视图,让你快速了解页面在整个录制期间的性能表现。
- FPS:帧率图表。绿色越高越好,红色块表示帧率过低,可能存在卡顿。
- CPU:CPU 资源消耗图表。堆叠的不同颜色表示不同类型的任务( scripting-黄色, rendering-紫色, painting-绿色, loading-蓝色, etc.)。如果长时间满负荷,说明CPU是瓶颈。
- NET:网络请求图表。每条横杠代表一个请求,颜色表示资源类型(HTML-蓝,JS-黄,CSS-紫,IMG-绿)。横杠的长度表示请求的耗时。
3. 线程面板
这是性能分析的核心,通常显示为火焰图。
-
Frames:对于动画,可以展开看到每一帧的渲染情况。将鼠标悬停在上面可以看到该帧的 FPS。
-
Main:主线程的详细活动记录,这是分析 JavaScript 执行和浏览器任务的关键区域。
-
X轴:时间。
-
Y轴:调用栈深度。上面的函数调用下面的函数。
-
颜色含义:
- 黄色:JavaScript 执行。
- 紫色:布局 / 重排。
- 绿色:绘制 / 重绘。
- 蓝色:HTML 解析、样式计算等。
-
4. 统计面板
当你在线程面板中选择一个时间范围后,这里会显示该时间段内各种活动的耗时摘要。
- Bottom-Up:按单个函数的总耗时(包括其子函数)排序。
- Call Tree:按调用链显示耗时,帮助你理解是从哪里开始触发的。
- Event Log:按时间顺序列出所有发生的事件(如
click,setTimeout,Animation Frame Fired等)。
用 Performance 面板能发现什么性能问题?
1. JavaScript 执行问题
-
长任务:在火焰图中看到超过 50ms 的黄色块,它会阻塞主线程,导致页面无响应。
- 解决方案:代码拆分、优化算法、使用 Web Workers。
-
频繁的微任务:大量密集的
Promise回调。 -
低效的事件监听器:一个事件处理函数执行时间过长。
2. 布局抖动
-
症状:在火焰图中看到一连串密集的紫色(Layout)块,并且它们通常由黄色的 JavaScript 块触发。
-
根本原因:JavaScript 在循环中交替读写 DOM 样式,导致浏览器被迫反复执行计算布局。
javascript
复制下载
// 典型的布局抖动代码 for (let i = 0; i < 100; i++) { const width = element.offsetWidth; // 读 - 触发重排 element.style.width = width + 10 + 'px'; // 写 - 再次触发重排 }- 解决方案:批量读写 DOM,或使用
FastDOM类似的库。
- 解决方案:批量读写 DOM,或使用
3. 强制同步布局
-
症状:在一个 JavaScript 任务中,突然插入了一个紫色的布局块。
-
根本原因:在修改样式之后、下一帧之前,读取了需要最新布局信息的属性(如
offsetTop,scrollHeight,getComputedStyle),迫使浏览器立即计算布局,而不是等到下一个布局阶段。- 解决方案:集中读、集中写。
4. 高昂的渲染成本
-
症状:大量的绿色(Paint)和紫色(Layout)区域。
-
根本原因:
- 过多/过大区域的绘制:使用
Layer面板查看绘制区域。 - 复杂的 CSS 选择器 或触发了昂贵的 CSS 属性(如
box-shadow,border-radius,filter)。 - 不必要的重排/重绘。
- 解决方案:使用
transform和opacity来实现动画(它们不会触发布局和绘制),提升动画元素到合成层(will-change: transform)。
- 过多/过大区域的绘制:使用
5. 内存泄漏
-
虽然 Memory 面板更适合,但 Performance 面板也能提供线索:
- 症状:录制期间,JS堆内存(在概览面板中)持续上升,且没有回落(垃圾回收的锯齿形不明显)。
- 根本原因:无用的对象引用没有被释放。
基本使用流程
-
准备:打开无痕模式(避免插件干扰),打开 DevTools → Performance 面板。
-
配置:点击齿轮图标,开启
4x或6xCPU 节流来模拟移动端性能。 -
录制:
- 点击“录制”按钮。
- 在页面上执行你想要分析的操作(如:点击按钮、滚动页面、触发动画)。
- 操作完成后,点击“停止”按钮。
-
分析:
- 首先看概览:有没有红色的 FPS 低谷?CPU 是否长时间满负荷?
- 放大问题区域:在概览面板上拖动选择一个你觉得有问题的短时间范围(比如一个卡顿处)。
- 查看火焰图:在主线程(Main)中寻找长任务(宽的黄色块)。
- 追溯根源:点击展开长任务,看看是哪个函数调用占据了大部分时间。
- 查看摘要:在统计面板中查看该时间段内哪种活动(Scripting, Rendering, Painting)耗时最多。
总结:Chrome Performance 面板是前端性能优化的“显微镜”,它能将模糊的“感觉卡顿”转化为精确的、可量化的、可定位的代码级问题,是每一个前端开发者必须掌握的核心调试工具。
有用过lighthouse吗?
Lighthouse,它是 Google 开源的自动化网页质量测评工具,常被用于前端开发中的性能优化、无障碍适配等场景,能从多维度检测网页并生成带改进建议的报告。下面从常用使用方式、核心检测内容和报告解读这几个实用角度详细说明:
-
主流使用方式
- Chrome DevTools 内置(最常用) :打开 Chrome 浏览器访问目标网页,按 F12 打开开发者工具,找到 Lighthouse 面板(若隐藏可点击 >> 调出)。勾选要检测的类别、选择模拟设备(Mobile/Desktop),点击 “Analyze page load”,等待 10 - 30 秒即可生成报告,适合快速调试开发中的页面。
- 命令行:先安装 Node.js,再通过
npm install -g lighthouse全局安装 Lighthouse。执行lighthouse https://example.com --view命令,会自动测评网页并在浏览器打开 HTML 格式报告,适合集成到 CI/CD 流程做自动化检测。 - Chrome 插件:在 Chrome 应用商店搜索 Lighthouse 安装后,点击浏览器右上角插件图标,选择 “generate report” 就能生成报告,无需打开开发者工具,操作更快捷。
-
核心检测维度该工具主要围绕四大核心维度测评,基本覆盖网页质量的关键指标:
检测维度 核心检测内容 关键指标 / 检测点 Performance(性能) 网页加载速度、交互流畅度 LCP(最大内容渲染,≤2.5s 最优)、CLS(布局偏移,≤0.1 最优)、TBT(主线程阻塞时间,≤200ms 最优)等 Accessibility(无障碍) 适配特殊用户使用需求 语义化标签是否规范、文字与背景对比度是否达标、是否支持屏幕阅读器等 Best Practices(最佳实践) 网页开发是否符合行业规范 是否使用 HTTPS、资源加载方式是否合理、是否避免使用过时 API 等 SEO(搜索引擎优化) 网页被搜索引擎收录的友好度 标题和 meta 标签是否完整、页面内容是否可爬取、适配移动设备等 -
报告解读与实用技巧
- 分数与问题定位:报告中每个维度会给出 0 - 100 分,90 - 100 分为优秀,50 - 89 分为一般,0 - 49 分为差。点击各维度可查看具体问题,比如 “图片未压缩”“脚本阻塞渲染” 等。
- 优化建议:报告不仅指出问题,还会附带具体改进方案。例如检测到 “图片未懒加载”,会建议添加
loading="lazy"属性;针对 JS 体积过大,会推荐代码分割或压缩。 - 报告保存与对比:可通过右上角 “Export” 按钮将报告保存为 HTML 或 JSON 格式。优化前后可各保存一份报告,对比分数和指标变化,验证优化效果。
- 避坑要点:测试时建议用无痕模式,避免浏览器插件干扰结果;因网络和缓存波动,建议多次测试取平均值;本地开发环境分数可能偏低,优先测试生产构建版本。
白屏时间是首字节时间吗?
不是,白屏时间和首字节时间(First Byte Time, FBT)是两个完全不同的性能指标,核心区别在于测量的对象和阶段不同。
核心结论
- 首字节时间(FBT):测量 “网络请求到服务器返回第一个字节数据” 的耗时,反映网络传输和服务器响应速度。
- 白屏时间(First Contentful Paint, FCP 常被等同于白屏时间):测量 “页面开始加载到首次出现可见内容” 的耗时,反映页面渲染启动速度。
具体区别
1. 首字节时间(FBT)
- 定义:从浏览器发起请求开始,到接收服务器返回的第一个字节数据为止的时间。
- 核心关注:网络链路(如 DNS 解析、TCP 连接)和服务器处理能力(如数据库查询、接口计算)。
- 无内容关联:只关心 “数据开始返回”,不涉及页面是否显示内容。
- 典型场景:接口响应慢、网络延迟高时,FBT 会变长。
2. 白屏时间(FCP)
- 定义:从页面开始加载,到屏幕上首次出现任何可见内容(文字、图片、图标等)为止的时间。
- 核心关注:浏览器接收数据后的解析渲染启动,包括 HTML 解析、关键 CSS 加载、首次 DOM 渲染。
- 直接影响用户体验:白屏时间越长,用户感知到的 “页面加载慢” 越明显。
- 依赖 FBT:FBT 是 FCP 的前置条件 —— 只有服务器返回数据(FBT 结束),浏览器才开始解析渲染,因此 FBT 变长会间接导致白屏时间变长,但两者并非同一概念。
关系与示例
- 时序关系:FBT 发生在白屏时间之前,是白屏时间的 “基础耗时”。
- 示例 1:FBT=300ms(网络 + 服务器快),但关键 CSS 未内联、JS 阻塞渲染,白屏时间 = 2s(解析渲染慢)。
- 示例 2:FBT=1.5s(网络差),但 HTML 内联关键 CSS、无阻塞资源,白屏时间 = 1.7s(解析渲染快)。
总结
两者是 “前置指标” 与 “用户感知指标” 的关系:首字节时间衡量网络和服务器的响应效率,白屏时间衡量浏览器渲染的启动效率,前者会影响后者,但并非同一指标。
加一个div会更加快白屏时间?首屏时间呢?
在大多数情况下,增加一个 div 不会加快白屏时间或首屏时间,反而可能轻微幅延长(影响微乎其微),具体取决于这个 div 的位置、是否携带样式 / 脚本依赖等因素。
1. 对白屏时间(FCP)的影响
白屏时间(首次内容绘制,FCP)的核心是 “浏览器首次渲染出可见内容的时间”,关键依赖:
- HTML 文档的下载与解析速度;
- 关键 CSS 的加载与解析(无 CSS 时内容可能无法渲染);
- 首屏内核心内容的 DOM 构建速度。
增加一个 div 的可能影响:
- 若
div是空标签(无样式、无复杂结构),仅会增加少量 HTML 字节(几个字符),对 HTML 下载和解析速度的影响可忽略不计,白屏时间基本不变。 - 若
div带有内联样式(如<div style="color:red">),或依赖外部 CSS/JS,可能轻微增加解析时间(但通常毫秒级,用户无感知)。 - 若
div是首屏核心内容(如替代了原本需要异步加载的内容),且能被浏览器快速解析渲染,可能略微提前 FCP(极端场景,如原本首屏无内容,加个简单div让浏览器更早渲染)。
结论:通常无影响,极端情况下可能微幅波动,但不会 “显著加快”。
2. 对首屏时间(LCP)的影响
首屏时间通常指最大内容绘制(LCP) ,即首屏中最大的可见元素(如主标题、主图)完成渲染的时间,关键依赖:
- 首屏核心资源(图片、大段文本)的加载速度;
- 渲染这些资源所需的 DOM/CSS 准备时间。
增加一个 div 的可能影响:
- 若
div是非首屏元素(在首屏外,如隐藏或在滚动区域下方),不影响首屏核心资源的加载和渲染,LCP 不变。 - 若
div是首屏元素但非最大内容(如小文本块),不影响 LCP 计算(LCP 关注 “最大” 元素),时间不变。 - 若
div是首屏最大内容(如大尺寸带背景图的div),且其加载 / 渲染速度比原本的 LCP 元素更快,则可能提前 LCP 时间(例如:原本 LCP 是一张需要加载的图片,新增的div用内联背景图且体积更小,更快渲染完成)。
结论:仅在新增 div 成为首屏最大内容且渲染更快时,可能加快首屏时间,否则无影响。
关键逻辑:性能指标的核心是 “资源与渲染效率”
白屏时间和首屏时间的瓶颈通常是:
- 网络延迟(资源下载慢);
- 阻塞渲染的 CSS/JS(解析渲染被卡住);
- 大资源加载(如未压缩的图片、冗余的代码)。
一个额外的 div 本身不会解决这些核心问题,因此通常不会对性能指标产生 “加快” 效果。只有当新增元素减少了关键资源体积、避免了渲染阻塞,或替代了更慢的渲染内容时,才可能带来微小优化。
总结:单纯加一个 div 几乎不会加快白屏或首屏时间,性能优化的核心仍在于减少资源体积、优化加载顺序、避免渲染阻塞等关键环节。
哪些资源要用强缓存?哪些用协商缓存?
在前端性能优化中,强缓存和协商缓存的选择取决于资源的更新频率、重要性以及是否允许用户获取旧版本。合理分配可最大化缓存收益,同时避免缓存不一致问题。
一、强缓存(优先使用,性能最优)
强缓存通过 Cache-Control 或 Expires 头字段让浏览器直接从本地缓存读取资源,不发送请求到服务器,性能消耗极低。适合长期不变、更新频率极低的资源。
适用场景:
-
静态资源(几乎不更新)
- 第三方库(如
react.min.js、lodash.min.js):版本固定后几乎不会变,可设置超长缓存(如 1 年)。 - 图标字体(
iconfont.ttf)、雪碧图(sprites.png):一旦生成,很少修改。 - 静态 HTML 模板(非首屏关键 HTML)、固定样式的 CSS(如
reset.css)。
- 第三方库(如
-
带哈希值的资源
- 前端构建工具(Webpack、Vite 等)打包后的资源(如
app.8f3d2.js、logo.a7c3b.png):文件名包含内容哈希,内容变化则哈希变化,可安全设置强缓存(如max-age=31536000)。 - 原理:哈希值确保资源更新时文件名变化,浏览器会视为新资源重新请求,避免旧缓存生效。
- 前端构建工具(Webpack、Vite 等)打包后的资源(如
二、协商缓存(需要服务器参与,平衡新鲜度和性能)
协商缓存通过 Last-Modified/If-Modified-Since 或 ETag/If-None-Match 机制,每次请求时让服务器判断资源是否更新:若未更新,返回 304 Not Modified,浏览器使用本地缓存;若已更新,返回新资源。适合频繁更新或需保证时效性的资源。
适用场景:
-
动态内容或频繁更新的静态资源
- 首屏 HTML:HTML 通常是入口文件,可能包含动态数据(如用户信息)或频繁调整的结构,需通过协商缓存确保用户获取最新版本。
- 频繁更新的 CSS/JS(未加哈希):如未使用构建工具的项目,资源文件名固定但内容常变,协商缓存可避免每次全量下载。
- 用户上传的内容(如用户头像、动态生成的图片):更新频率不确定,需服务器判断是否变化。
-
对时效性要求高的资源
- 接口数据(非静态资源,但可配合缓存策略):如商品列表、新闻内容,需保证用户看到最新数据,但可通过协商缓存减少重复传输(例如设置
Cache-Control: no-cache强制协商)。 - 活动页、营销页:内容可能按周期更新(如每日活动),需在更新后立即生效,协商缓存可确保用户及时获取变化。
- 接口数据(非静态资源,但可配合缓存策略):如商品列表、新闻内容,需保证用户看到最新数据,但可通过协商缓存减少重复传输(例如设置
三、核心原则总结
| 维度 | 强缓存 | 协商缓存 |
|---|---|---|
| 更新频率 | 极低(数月 / 年不变) | 较高(天 / 周 / 随时可能变) |
| 文件名特性 | 通常带哈希(内容变则文件名变) | 文件名固定(内容变但文件名不变) |
| 用户体验要求 | 允许使用旧版本(更新时换文件名) | 需实时获取最新版本 |
| 典型资源 | 第三方库、哈希资源、静态图标 | 首屏 HTML、动态内容、频繁更新资源 |
四、避坑点
- 强缓存不适合首屏 HTML:若 HTML 被强缓存,用户可能始终看到旧页面(即使 JS/CSS 已更新)。
- 协商缓存需服务器配合:需正确配置
ETag或Last-Modified,否则可能导致缓存失效(如ETag计算逻辑不合理)。 - 混合策略:大部分项目采用 “强缓存哈希资源 + 协商缓存非哈希资源” 的组合,兼顾性能和新鲜度。
例如:Webpack 打包的 app.[hash].js 用强缓存,而 index.html 用协商缓存,确保 HTML 每次请求都能触发最新 JS/CSS 的加载。
现代最佳实践
-
所有静态资源都加哈希指纹,然后设置长期强缓存。
-
HTML文件使用协商缓存或很短的最大年龄。
-
使用Service Worker进行更精细的缓存控制。
-
第三方库使用固定版本CDN + 强缓存。
-
API响应根据数据特性灵活选择:
- 实时数据:
Cache-Control: no-cache - 半静态数据:
Cache-Control: max-age=300(5分钟) - 静态数据:
Cache-Control: max-age=3600(1小时)
- 实时数据:
总结:记住「内容变,URL就变;URL不变,用协商」这个核心原则,你就能为绝大多数资源制定出合理的缓存策略。
后端如何做etag?
在后端实现 ETag(实体标签)主要用于 HTTP 缓存机制,帮助客户端验证资源是否发生变化,减少不必要的数据传输。以下是实现 ETag 的核心思路和步骤:
1. ETag 的基本原理
ETag 是服务器为资源(如文件、API 响应)生成的唯一标识,通常基于资源内容计算。当客户端再次请求该资源时,会在请求头中携带 If-Match 或 If-None-Match 字段,服务器通过比对 ETag 来判断资源是否修改:
- 若 ETag 匹配,返回
304 Not Modified(无响应体,节省带宽); - 若不匹配,返回新资源和新 ETag(
200 OK)。
2. 实现步骤
步骤 1:生成 ETag
ETag 的生成需基于资源的实际内容(或内容的哈希),确保内容变化时 ETag 也会变化。常见生成方式:
- 哈希计算:对资源内容(如字符串、文件二进制)计算 MD5、SHA-1 等哈希值(推荐截断为短字符串,如前 16 位)。
- 文件元信息:结合文件的修改时间(mtime)和大小(size)生成(适用于静态文件,简单但可能有误差)。
- 版本号 / 唯一标识:若资源有数据库版本号(如
version字段),可直接使用(适用于动态 API)。
示例代码(Python/Flask):
python
运行
import hashlib
from flask import Flask, make_response
app = Flask(__name__)
def generate_etag(content):
# 对内容进行 MD5 哈希,取前16位作为 ETag
hash_obj = hashlib.md5(content.encode('utf-8'))
return hash_obj.hexdigest()[:16]
@app.route('/data')
def get_data():
data = "Hello, ETag!" # 实际场景可能是数据库查询结果或文件内容
etag = generate_etag(data)
# 检查客户端请求头的 If-None-Match
client_etag = request.headers.get('If-None-Match')
if client_etag == etag:
return '', 304 # 资源未修改
# 返回新资源和 ETag
response = make_response(data)
response.headers['ETag'] = etag
return response
步骤 2:处理客户端请求头
客户端会通过以下头信息传递 ETag 给服务器:
If-None-Match:用于GET/HEAD请求,服务器若匹配则返回 304。If-Match:用于PUT/DELETE等修改请求,确保修改的是预期版本(避免并发冲突)。
示例(处理并发修改,Node.js/Express):
javascript
运行
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// 模拟资源存储
let resource = { content: "初始内容", version: 1 };
function generateETag(content) {
return crypto.createHash('md5').update(content).digest('hex').slice(0, 16);
}
// 更新资源(需验证 ETag)
app.put('/resource', (req, res) => {
const clientETag = req.headers['if-match'];
const currentETag = generateETag(resource.content);
if (clientETag !== currentETag) {
return res.status(412).send('资源已被修改,请重新获取'); // 412 Precondition Failed
}
// 验证通过,更新资源
resource.content = req.body.newContent;
resource.version++;
const newETag = generateETag(resource.content);
res.set('ETag', newETag).send('资源更新成功');
});
步骤 3:注意事项
-
强验证 vs 弱验证:
- 强验证:ETag 基于内容精确计算(如哈希),内容哪怕有微小变化(如空格)也会改变(默认用
"包裹,如"abc123")。 - 弱验证:允许内容轻微变化(如注释)不改变 ETag,格式为
W/"abc123"(W 表示弱验证),适用于静态资源。
- 强验证:ETag 基于内容精确计算(如哈希),内容哪怕有微小变化(如空格)也会改变(默认用
-
性能优化:
- 避免对大文件全量哈希,可结合修改时间 + 大小(但需注意:相同大小和时间的文件可能内容不同)。
- 缓存生成的 ETag,避免重复计算。
-
与 Last-Modified 配合:ETag 可弥补
Last-Modified的不足(如毫秒级修改无法识别),两者可同时使用,服务器优先判断 ETag。
3. 框架内置支持
多数后端框架已内置 ETag 功能,可直接启用:
- Express:通过
etag中间件自动生成(默认启用)。 - Nginx:静态文件可配置
etag on;自动生成。 - Django:
CommonMiddleware支持自动生成 ETag。
手动实现时,核心是确保 ETag 与资源内容强关联,并正确处理客户端的条件请求头。
浏览器如何看资源是用了强缓存?协商缓存?
核心结论:浏览器通过「开发者工具 Network 面板」查看响应头 / 请求头,结合状态码就能区分 —— 强缓存直接用本地缓存(200 OK 且 Size 显示 from disk/memory cache),协商缓存需向服务器验证(304 Not Modified 或 200 OK 且带 If-None-Match/If-Modified-Since 请求头)。
1. 先打开浏览器开发者工具(通用步骤)
- 按 F12 或 Ctrl+Shift+I(Mac 用 Cmd+Opt+I)。
- 切换到「Network」面板,勾选上方「Disable cache」(仅测试时用,正常查看需取消勾选)。
- 刷新页面(普通刷新 F5,强制刷新 Ctrl+F5 会跳过缓存,不适合查看)。
- 点击任意资源(如 JS、CSS、图片),查看右侧「Headers」标签页。
2. 识别强缓存(无需请求服务器)
强缓存由响应头控制,资源未过期时,浏览器直接读取本地缓存,不会发送 HTTP 请求(或仅发送极简请求)。
关键判断依据
-
响应头有以下字段之一:
Cache-Control:值含max-age=xxx(单位秒,如max-age=86400表示缓存 1 天)、public(允许代理缓存)、private(仅浏览器缓存)、immutable(禁止协商缓存)。Expires:具体过期时间(如Wed, 20 Jul 2024 12:00:00 GMT),优先级低于Cache-Control。
-
网络面板特征:
- 状态码:200 OK(部分浏览器显示 200 OK (from disk cache) 或 200 OK (from memory cache))。
- Size 列:显示「from disk cache」(硬盘缓存,持久化)或「from memory cache」(内存缓存,关闭标签页失效)。
3. 识别协商缓存(需请求服务器验证)
协商缓存需先向服务器发送请求,携带本地缓存标识,由服务器判断是否使用缓存,会发送 HTTP 请求。
关键判断依据
-
场景 1:缓存有效(服务器返回 304)
- 响应头:无响应体,状态码
304 Not Modified。 - 请求头:携带
If-None-Match(对应 ETag)或If-Modified-Since(对应 Last-Modified)。
- 响应头:无响应体,状态码
-
场景 2:缓存无效(服务器返回新资源)
- 响应头:状态码
200 OK,同时携带ETag和Last-Modified(更新本地缓存标识)。 - 请求头:仍会携带
If-None-Match或If-Modified-Since(证明是协商缓存请求)。
- 响应头:状态码
4. 快速区分对照表
| 类型 | 核心响应头 | 核心请求头 | 状态码 | Size 列特征 |
|---|---|---|---|---|
| 强缓存 | Cache-Control (max-age) / Expires | 无(或无实际请求) | 200 OK | from disk/memory cache |
| 协商缓存(命中) | 无(或仅基础响应头) | If-None-Match / If-Modified-Since | 304 Not Modified | 很小(仅响应头大小) |
| 协商缓存(未命中) | ETag / Last-Modified | If-None-Match / If-Modified-Since | 200 OK | 资源实际大小 |
5. 常见误区提醒
- 强制刷新(Ctrl+F5)会跳过所有缓存,直接请求新资源,此时看不到缓存效果,需用普通刷新(F5)。
- 部分资源(如 HTML)默认可能禁用强缓存(Cache-Control: no-cache),优先走协商缓存,避免页面更新不及时。
Cache-Control: no-cache不是不缓存,而是强制触发协商缓存;Cache-Control: no-store才是完全不缓存。