前端面试题

100 阅读32分钟

果肉教育:

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)

  1. 相同类型的组件生成相似的树结构,不同类型的组件会生成完全不同的树。

    • 如果两个组件的类型不同(例如从 <div> 变成了 <span>,或者从 ComponentA 变成了 ComponentB),React 会直接销毁整个旧子树并重建新子树,不会再去递归比较它们的子节点。这节省了大量计算。
  2. 开发者可以通过 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 提供了 shouldComponentUpdatePureComponentReact.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. 性能最优:

    • 只有 1次 DOM 插入(新任务),其他三个节点只是被移动。
    • 完美地复用了所有已有的 DOM 节点和组件实例。如果这些组件有内部状态(如复选框的选中状态、输入框的内容),状态会被完美保留。
  2. 正确性保证:

    • 完全避免了状态错乱的问题。因为 key 是基于数据本身的,而不是它在数组中的位置。

总结对比表

方面不用 key / 用 index 作为 key使用稳定的 key (如 id)
性能。列表顺序变化时,导致大量不必要的更新和重建。。最大程度复用节点,只处理新增/删除/移动。
状态保持会错乱。组件状态与错误的元素绑定。正确保持。状态始终与正确的数据绑定。
适用场景静态列表:永不重新排序、过滤或在中部增删的列表。动态列表:任何可能发生顺序变化、增删的列表。最佳实践是总是使用。

结论:  为了应用的性能和正确性,永远不要使用数组索引 index 作为 key,除非你能 100% 保证它是一个静态的、永不变化的列表。请始终使用数据中唯一且稳定的标识(如 id)作为 key

react的vnode是什么样的?数据结构是什么?

  1. 轻量对象:React VNode 是一个普通的 JS 对象,远比真实 DOM 轻量。
  2. 核心属性type 和 props 是核心,定义了要渲染什么以及它的属性。
  3. 关键辅助key 和 ref 是特殊的、不包含在 props 中的属性,用于优化和访问。
  4. 安全机制$$typeof 是一个重要的安全特性。
  5. 树形结构:通过 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 可分为以下几类,各自的结构略有差异:

  1. 原生 DOM 元素节点

    • type 为字符串(如 'div''span''input')。
    • props 包含 HTML 属性(classNamestyle)、事件(onClick)、children 等。示例:

    javascript

    运行

    {
      type: 'div',
      props: {
        className: 'box',
        children: [
          { type: 'span', props: { children: 'Hello' } }
        ]
      },
      key: null
    }
    
  2. 组件节点

    • type 为组件构造函数(类组件)或函数(函数组件)。
    • props 为组件接收的参数(包含 children 作为子组件)。示例(函数组件):

    javascript

    运行

    function Button({ text }) { return <button>{text}</button>; }
    
    // 对应的 VNode
    {
      type: Button, // 函数组件本身
      props: { text: 'Click me' },
      key: null
    }
    
  3. 文本节点

    • 没有 type 和 props,直接用字符串或数字表示(是一种简化的 VNode)。示例:'Hello World' 或 42(在 React 中会被当作文本节点的 VNode)。
  4. 特殊节点

    • Fragmenttype 为 Symbol(react.fragment),用于表示片段(不渲染额外 DOM 容器),props 可能包含 keychildren 为子节点列表。
    • Portaltype 为 Symbol(react.portal),用于将子节点渲染到指定 DOM 容器,props 包含 container(目标容器)和 children
    • 注释节点type 为 Symbol(react.comment),表示 HTML 注释。

VNode 的核心作用

  1. 描述 UI 结构:用 JavaScript 对象抽象真实 DOM,避免频繁操作昂贵的真实 DOM。
  2. 支持跨平台:VNode 不依赖具体平台(浏览器 DOM、Native 等),React 通过不同的渲染器(如 ReactDOM、React Native)将 VNode 转换为对应平台的真实元素。
  3. 高效 diff 计算:通过对比新旧 VNode 的 typekeyprops 等信息,快速定位差异,减少更新成本。

总结:React 的 VNode 是一个包含 typepropskey 等核心属性的 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 嵌套的逻辑树通过 childsiblingreturn 连接的链表
可变性不可变 - 每次渲染重新创建可变 - 长期存在,可重用
创建时机每次 render 时创建在协调阶段创建和更新
用途Diff 算法的输入渲染调度、状态管理、生命周期

结论:React 确实会生成虚拟 DOM 树,但这棵树是短暂的、不可变的、通过组合形成的逻辑树,主要用于 Diff 比较。而在底层,React 使用更复杂的 Fiber 树结构来管理组件的状态和渲染调度。

react的每个组件有一个vnode树吗?

为什么说「每个组件没有独立的 VNode 树」?

  1. 组件返回的是子树,不是完整的树

    • Header 返回 <h1>,不是一棵以 Header 为根的树
    • TodoItem 返回 <li>,不是一棵以 TodoItem 为根的树
  2. 所有组件共同构建一棵应用级的虚拟 DOM 树

    • 这棵树的根节点是你渲染的根组件(如 <App />
    • 每个组件贡献这棵树的一个分支或叶子
  3. 组件的「边界」在虚拟 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 在组件可能发生更新时被调用,具体场景包括:

  1. 组件自身的 state 发生变化(调用 setState 后)。
  2. 父组件传递的 props 发生变化(父组件重渲染导致子组件接收新 props)。
  3. 强制更新(调用 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:阻止重渲染,后续所有渲染相关钩子(如 rendercomponentDidUpdate)都不会执行。
  • 核心作用:通过对比 nextProps 与当前 propsnextState 与当前 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 步中的所有钩子(componentWillUpdaterendergetSnapshotBeforeUpdatecomponentDidUpdate)都不会执行,组件保持当前状态。

三、总结: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

关键要点:

  1. shouldComponentUpdate 是一个「阀门」:它决定是否继续后续的渲染和 DOM 更新。
  2. 状态已更新:即使在 shouldComponentUpdate 中返回 false,组件实例的 this.props 和 this.state 也已经被新的值替换了。
  3. getDerivedStateFromProps 总是执行:它在每次渲染前都会被调用,无论 shouldComponentUpdate 返回什么。
  4. forceUpdate 是例外:它会绕过优化阀门 shouldComponentUpdate

react的setstate之后会发生哪些操作?

image.png

阶段一:初始化和安排工作
  1. setState 调用

    • 你将新的状态数据传入 setState。它可以是一个对象,也可以是一个函数 (prevState, props) => newState
  2. 状态更新入队与批处理

    • React 不会立即更新状态和执行渲染。它会将这次更新放入一个更新队列中。
    • 批处理:如果在同一个同步事件循环中(例如一个事件处理函数内)多次调用 setState,React 会聪明地将它们合并成一次更新,以避免不必要的重复渲染。
  3. 安排渲染工作

    • React 通知 React Scheduler 有一个组件需要更新。
    • Scheduler 会根据元素的优先级(由 React 18+ 的并发特性使用)和浏览器的空闲时间,来安排协调工作的执行时机。这确保了高优先级的更新(如用户输入)能更快得到响应。

阶段二:协调阶段 - 找出变化

这个阶段是 React 的核心,它创建了一棵新的虚拟 DOM 树(Fiber 树),并通过 Diffing 算法找出需要更新的部分。此阶段可以被中断,以允许浏览器处理更高优先级的任务。

  1. 开始协调

    • React 从组件的根节点开始,遍历 Fiber 树(React 内部用于描述组件树和工作的数据结构)。

    • 对于类组件,React 会按顺序调用以下生命周期方法(如果定义了):

      • static getDerivedStateFromProps(props, state) - 一个不常用的生命周期,用于根据 props 派生 state。
      • shouldComponentUpdate(nextProps, nextState) - 性能优化关键!你可以在这里通过返回 false 来阻止组件重新渲染。
  2. 渲染生成新的虚拟 DOM

    • 如果 shouldComponentUpdate 返回 true(或未定义),React 会调用组件的 render 方法。
    • render 方法返回新的 React Elements(虚拟 DOM) ,描述了组件最新的 UI 结构。
  3. Reconciliation / Diffing 算法

    • React 将新生成的虚拟 DOM 与旧的虚拟 DOM 进行对比

    • 它递归地比较新旧子元素,找出确切发生变化的部分(例如:文本内容改变、属性更新、元素增删等)。

    • React 不会直接操作真实 DOM,而是为发生变化的 Fiber 节点标记上  “副作用” ,例如:

      • Placement - 需要插入新节点
      • Update - 需要更新现有节点
      • Deletion - 需要删除节点

阶段三:提交阶段 - 应用变化到真实 DOM

此阶段 React 会同步地、不可中断地执行,将协调阶段收集到的所有副作用一次性应用到真实 DOM 上,以确保 UI 的一致性。

  1. getSnapshotBeforeUpdate 生命周期

    • 在真实的 DOM 更新之前,React 会调用这个生命周期方法。它允许你在 DOM 可能发生变化之前捕获一些信息(例如当前的滚动位置)。
  2. 将副作用刷新到真实 DOM

    • React 执行所有被标记的 DOM 操作:

      • 插入新节点
      • 更新节点的文本内容或属性(如 classNamestyle
      • 删除废弃的节点
    • 注意:到这一步,浏览器的屏幕上还没有显示更新后的 UI。

  3. componentDidUpdate 生命周期

    • 在真实的 DOM 更新之后,React 会立即调用这个生命周期方法。

    • 这是执行副作用操作的理想位置,例如:

      • 基于新的 DOM 布局进行计算
      • 发送网络请求
      • 手动操作 DOM

阶段四:浏览器渲染
  1. 浏览器重绘

    • 当 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 步是控制组件更新的关键:

  1. 映射 state 和 dispatch 到 props通过 mapStateToProps 函数,从 Redux 的全局 state 中提取当前组件需要的部分(例如 state.user 或 state.cart),并映射为组件的 props;通过 mapDispatchToProps 函数,将 dispatch 方法包装为组件可调用的 props(如 this.props.addItem)。

  2. 判断 “依赖的 state 是否变化”connect 内部会缓存上一次 mapStateToProps 返回的结果(即组件依赖的那部分 state)。当 Redux 的全局 state 发生变化时:

    • 首先重新执行 mapStateToProps,得到新的 props;
    • 然后对比新 props 与缓存的旧 props(浅对比,即比较引用是否相同);
    • 若两者相同(说明组件依赖的 state 没变化),则阻止组件重新渲染;
    • 若不同(依赖的 state 变了),则触发组件更新,传入新的 props。
  3. 订阅 Redux store 的变化connect 会让组件订阅 Redux store 的更新(通过 store.subscribe)。当 store 中的 state 发生任何变化时,所有订阅了 store 的组件都会收到通知,但最终是否更新,由第 2 步的对比结果决定。

二、举例说明:为什么无关 state 变化不会触发组件更新?

假设 Redux 的全局 state 结构如下:

javascript

运行

{
  user: { name: 'Alice' }, // 组件 A 依赖这部分
  cart: { items: [] }      // 组件 B 依赖这部分
}
  1. 组件 A 通过 connect 关联 user

    javascript

    运行

    const mapStateToProps = (state) => ({
      userName: state.user.name // 只依赖 state.user
    });
    export default connect(mapStateToProps)(ComponentA);
    
  2. 当 cart 发生变化时(例如添加商品):

    • Redux store 触发更新,通知所有订阅的组件(包括组件 A 和 B);
    • 组件 A 执行 mapStateToProps,得到的 userName 与上次相同(因为 user 没变化);
    • connect 对比后发现 props 未变,因此阻止组件 A 重新渲染;
    • 组件 B 因依赖的 cart 变化,mapStateToProps 返回新 props,触发重新渲染。

三、关键细节:mapStateToProps 与浅对比的影响

  1. **mapStateToProps 必须返回 “稳定的引用”**如果 mapStateToProps 每次执行都返回新的对象 / 数组(即使内容相同),会导致浅对比失效,触发无效渲染。例如:

    javascript

    运行

    // 错误示例:每次调用都返回新对象(引用不同)
    const mapStateToProps = (state) => ({
      user: { name: state.user.name } // 即使内容相同,对象引用也不同
    });
    

    解决方式:尽量返回 state 中的原始引用(如 return { user: state.user }),或使用 reselect 库缓存计算结果。

  2. 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 只更新对应组件” 的核心逻辑是:

  1. connect 让组件只订阅自己依赖的那部分 state(通过 mapStateToProps 提取);
  2. 当全局 state 变化时,connect 对比组件依赖的新旧 state(浅对比);
  3. 只有当依赖的 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 不会更新(错误!)

解决:更新引用类型时,应返回新的对象 / 数组(如使用扩展运算符 ...mapfilter 等),确保引用变化。

(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>;
}

关键注意点:

  1. 浅对比规则:依赖数组中的值会进行浅对比。对于引用类型(如对象、数组),仅比较引用地址是否变化,而非内部属性。例如:

    javascript

    运行

    const [user, setUser] = useState({ name: 'Alice' });
    
    // 错误:直接修改原对象,引用不变,useEffect 不会触发
    const handleClick = () => {
      user.name = 'Bob';
      setUser(user); // 引用未变,[user] 依赖不会被视为变化
    };
    
    // 正确:返回新对象,引用变化,useEffect 会触发
    const handleClick = () => {
      setUser({ ...user, name: 'Bob' }); // 新对象,引用变化
    };
    
  2. 必须包含所有依赖:回调函数中使用的所有组件内变量(如 propsstate、函数等),都必须加入依赖数组,否则可能导致回调函数捕获到旧值(闭包问题)。例如,若回调中使用了 count 但未加入依赖数组,当 count 变化时,回调可能仍使用旧的 count 值。

总结:

useEffect 的第二个参数是依赖数组,用于控制回调函数的执行时机:

  • 空数组 []:仅挂载时执行一次;
  • 包含特定值:挂载时执行,且值变化时重新执行;
  • 省略:每次渲染后都执行。

合理设置依赖数组是避免 useEffect 无效执行或逻辑错误的关键。

有看过谷歌的performance吗?有哪些东西?

image.png

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:按时间顺序列出所有发生的事件(如 clicksetTimeoutAnimation 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 类似的库。
3. 强制同步布局
  • 症状:在一个 JavaScript 任务中,突然插入了一个紫色的布局块。

  • 根本原因:在修改样式之后、下一帧之前,读取了需要最新布局信息的属性(如 offsetTopscrollHeightgetComputedStyle),迫使浏览器立即计算布局,而不是等到下一个布局阶段。

    • 解决方案:集中读、集中写。
4. 高昂的渲染成本
  • 症状:大量的绿色(Paint)和紫色(Layout)区域。

  • 根本原因

    • 过多/过大区域的绘制:使用 Layer 面板查看绘制区域。
    • 复杂的 CSS 选择器 或触发了昂贵的 CSS 属性(如 box-shadowborder-radiusfilter)。
    • 不必要的重排/重绘
    • 解决方案:使用 transform 和 opacity 来实现动画(它们不会触发布局和绘制),提升动画元素到合成层(will-change: transform)。
5. 内存泄漏
  • 虽然 Memory 面板更适合,但 Performance 面板也能提供线索:

    • 症状:录制期间,JS堆内存(在概览面板中)持续上升,且没有回落(垃圾回收的锯齿形不明显)。
    • 根本原因:无用的对象引用没有被释放。

基本使用流程

  1. 准备:打开无痕模式(避免插件干扰),打开 DevTools → Performance 面板。

  2. 配置:点击齿轮图标,开启 4x 或 6x CPU 节流来模拟移动端性能。

  3. 录制

    • 点击“录制”按钮。
    • 在页面上执行你想要分析的操作(如:点击按钮、滚动页面、触发动画)。
    • 操作完成后,点击“停止”按钮。
  4. 分析

    • 首先看概览:有没有红色的 FPS 低谷?CPU 是否长时间满负荷?
    • 放大问题区域:在概览面板上拖动选择一个你觉得有问题的短时间范围(比如一个卡顿处)。
    • 查看火焰图:在主线程(Main)中寻找长任务(宽的黄色块)。
    • 追溯根源:点击展开长任务,看看是哪个函数调用占据了大部分时间。
    • 查看摘要:在统计面板中查看该时间段内哪种活动(Scripting, Rendering, Painting)耗时最多。

总结:Chrome Performance 面板是前端性能优化的“显微镜”,它能将模糊的“感觉卡顿”转化为精确的、可量化的、可定位的代码级问题,是每一个前端开发者必须掌握的核心调试工具。

有用过lighthouse吗?

Lighthouse,它是 Google 开源的自动化网页质量测评工具,常被用于前端开发中的性能优化、无障碍适配等场景,能从多维度检测网页并生成带改进建议的报告。下面从常用使用方式、核心检测内容和报告解读这几个实用角度详细说明:

  1. 主流使用方式

    • 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” 就能生成报告,无需打开开发者工具,操作更快捷。
  2. 核心检测维度该工具主要围绕四大核心维度测评,基本覆盖网页质量的关键指标:

    检测维度核心检测内容关键指标 / 检测点
    Performance(性能)网页加载速度、交互流畅度LCP(最大内容渲染,≤2.5s 最优)、CLS(布局偏移,≤0.1 最优)、TBT(主线程阻塞时间,≤200ms 最优)等
    Accessibility(无障碍)适配特殊用户使用需求语义化标签是否规范、文字与背景对比度是否达标、是否支持屏幕阅读器等
    Best Practices(最佳实践)网页开发是否符合行业规范是否使用 HTTPS、资源加载方式是否合理、是否避免使用过时 API 等
    SEO(搜索引擎优化)网页被搜索引擎收录的友好度标题和 meta 标签是否完整、页面内容是否可爬取、适配移动设备等
  3. 报告解读与实用技巧

    • 分数与问题定位:报告中每个维度会给出 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 头字段让浏览器直接从本地缓存读取资源,不发送请求到服务器,性能消耗极低。适合长期不变、更新频率极低的资源。

适用场景:
  1. 静态资源(几乎不更新)

    • 第三方库(如 react.min.jslodash.min.js):版本固定后几乎不会变,可设置超长缓存(如 1 年)。
    • 图标字体(iconfont.ttf)、雪碧图(sprites.png):一旦生成,很少修改。
    • 静态 HTML 模板(非首屏关键 HTML)、固定样式的 CSS(如 reset.css)。
  2. 带哈希值的资源

    • 前端构建工具(Webpack、Vite 等)打包后的资源(如 app.8f3d2.jslogo.a7c3b.png):文件名包含内容哈希,内容变化则哈希变化,可安全设置强缓存(如 max-age=31536000)。
    • 原理:哈希值确保资源更新时文件名变化,浏览器会视为新资源重新请求,避免旧缓存生效。

二、协商缓存(需要服务器参与,平衡新鲜度和性能)

协商缓存通过 Last-Modified/If-Modified-Since 或 ETag/If-None-Match 机制,每次请求时让服务器判断资源是否更新:若未更新,返回 304 Not Modified,浏览器使用本地缓存;若已更新,返回新资源。适合频繁更新或需保证时效性的资源。

适用场景:
  1. 动态内容或频繁更新的静态资源

    • 首屏 HTML:HTML 通常是入口文件,可能包含动态数据(如用户信息)或频繁调整的结构,需通过协商缓存确保用户获取最新版本。
    • 频繁更新的 CSS/JS(未加哈希):如未使用构建工具的项目,资源文件名固定但内容常变,协商缓存可避免每次全量下载。
    • 用户上传的内容(如用户头像、动态生成的图片):更新频率不确定,需服务器判断是否变化。
  2. 对时效性要求高的资源

    • 接口数据(非静态资源,但可配合缓存策略):如商品列表、新闻内容,需保证用户看到最新数据,但可通过协商缓存减少重复传输(例如设置 Cache-Control: no-cache 强制协商)。
    • 活动页、营销页:内容可能按周期更新(如每日活动),需在更新后立即生效,协商缓存可确保用户及时获取变化。

三、核心原则总结

维度强缓存协商缓存
更新频率极低(数月 / 年不变)较高(天 / 周 / 随时可能变)
文件名特性通常带哈希(内容变则文件名变)文件名固定(内容变但文件名不变)
用户体验要求允许使用旧版本(更新时换文件名)需实时获取最新版本
典型资源第三方库、哈希资源、静态图标首屏 HTML、动态内容、频繁更新资源

四、避坑点

  • 强缓存不适合首屏 HTML:若 HTML 被强缓存,用户可能始终看到旧页面(即使 JS/CSS 已更新)。
  • 协商缓存需服务器配合:需正确配置 ETag 或 Last-Modified,否则可能导致缓存失效(如 ETag 计算逻辑不合理)。
  • 混合策略:大部分项目采用 “强缓存哈希资源 + 协商缓存非哈希资源” 的组合,兼顾性能和新鲜度。

例如:Webpack 打包的 app.[hash].js 用强缓存,而 index.html 用协商缓存,确保 HTML 每次请求都能触发最新 JS/CSS 的加载。

现代最佳实践

  1. 所有静态资源都加哈希指纹,然后设置长期强缓存。

  2. HTML文件使用协商缓存或很短的最大年龄。

  3. 使用Service Worker进行更精细的缓存控制。

  4. 第三方库使用固定版本CDN + 强缓存。

  5. 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,避免重复计算。
  • 与 Last-Modified 配合:ETag 可弥补 Last-Modified 的不足(如毫秒级修改无法识别),两者可同时使用,服务器优先判断 ETag。

3. 框架内置支持

多数后端框架已内置 ETag 功能,可直接启用:

  • Express:通过 etag 中间件自动生成(默认启用)。
  • Nginx:静态文件可配置 etag on; 自动生成。
  • DjangoCommonMiddleware 支持自动生成 ETag。

手动实现时,核心是确保 ETag 与资源内容强关联,并正确处理客户端的条件请求头。

浏览器如何看资源是用了强缓存?协商缓存?

核心结论:浏览器通过「开发者工具 Network 面板」查看响应头 / 请求头,结合状态码就能区分 —— 强缓存直接用本地缓存(200 OK 且 Size 显示 from disk/memory cache),协商缓存需向服务器验证(304 Not Modified 或 200 OK 且带 If-None-Match/If-Modified-Since 请求头)。

1. 先打开浏览器开发者工具(通用步骤)

  1. 按 F12 或 Ctrl+Shift+I(Mac 用 Cmd+Opt+I)。
  2. 切换到「Network」面板,勾选上方「Disable cache」(仅测试时用,正常查看需取消勾选)。
  3. 刷新页面(普通刷新 F5,强制刷新 Ctrl+F5 会跳过缓存,不适合查看)。
  4. 点击任意资源(如 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 OKfrom disk/memory cache
协商缓存(命中)无(或仅基础响应头)If-None-Match / If-Modified-Since304 Not Modified很小(仅响应头大小)
协商缓存(未命中)ETag / Last-ModifiedIf-None-Match / If-Modified-Since200 OK资源实际大小

5. 常见误区提醒

  • 强制刷新(Ctrl+F5)会跳过所有缓存,直接请求新资源,此时看不到缓存效果,需用普通刷新(F5)。
  • 部分资源(如 HTML)默认可能禁用强缓存(Cache-Control: no-cache),优先走协商缓存,避免页面更新不及时。
  • Cache-Control: no-cache 不是不缓存,而是强制触发协商缓存;Cache-Control: no-store 才是完全不缓存。