React 性能优化指南之性能分析与16种优化方法大总结

2023-11-09

本文分为两个部分

1 如何分析 React性能

  • 1.1 性能分析指标有哪些

  • 1.2 性能分析的两个阶段

  • 1.3 通过工具查看指标和度量

2 16个React 性能优化方法

  • 2.1 前端通用优化

  • 2.2 减少不必要的组件更新

  • 2.3 提交阶段优化

1 性能 分析

进行任何性能优化的首先你要知道有哪些衡量的指标?其次找出存在的问题?然后才能针对性地进行优化。

1.1 性能分析指标有哪些

定性:加载性能、运行性能:滚动&更新

定量:

加载性能指标:reponseStart、domInteractive、DomContentLoadedEventEnd、loadEventStart、FCP FSP FMP TTI

运行性能指标:FPS 、 内存 CPU I/O 网络 磁盘

治理:采集、运维

1.2 性能分析的两个阶段

分析阶段

  • 通过分析器(Profiler)找出重新渲染的组件、重新渲染的次数、以及重新渲染耗费的资源与时间

  • 变动检测,通过分析器我们可以知道:什么被重新渲染?重新渲染的代价?变动检测要回答的问题就是:为什么这些进行了重新渲染?

优化阶段

优化阶段我们针对分析阶段抛出的问题进行解决,下面简单列举下React 进行渲染性能优化的三个方向:

  • 1、前端通用优化。这类优化在所有前端框架中都存在,重点就在于如何将这些技巧应用在 React 组件中。

  • 2、减少不必要的组件更新。这类优化是在组件状态发生变更后,通过减少不必要的组件更新来实现,对应到 React 中就是:减少渲染的节点 、降低组件渲染的复杂度、充分利用缓存避免重新渲染(利用缓存可以考虑使用PureComponent、React.memo、hook函数useCallback、useMemo等方法)

  • 3 提交阶段优化。这类优化的目的是减少提交阶段耗时。

1.3 通过工具查看指标和度量

1、 React Dev Tools & Redux Dev Tools

React v16.5 引入了新的 Profiler 功能,让分析组件渲染过程变得更加简单,而且可以很直观地查看哪些组件被渲染.

6d5152c84905fdf7534d414d799b440c.png

高亮更新

首先最简单也是最方便的判断组件是否被重新渲染的方式是“高亮更新(Hightlight Updates)”,通过高亮更新,基本上可以确定哪些组件被重新渲染。

设置方式如下:

dac1261475bc714e1242bd585d4c9ae2.png

例如合理使用了React.memo的列表组件比不使用,性能更好,“纯组件”是 React 优化的第一张牌, 也是最有效的一张牌。

分析器

如果高亮更新无法满足你的需求,比如你需要知道具体哪些组件被渲染、渲染消耗多少时间、进行了多少次的提交(渲染)等等, 这时候就需要用到分析器了.

来了解一下 Profiler 面板的基本结构:64d086f0373af67882ce786dd67005f2.png

1、 commit 列表

commit 列表表示录制期间发生的 commit(可以认为是渲染) 操作,要理解 commit 的意思还需要了解 React 渲染的基本原理。在 v16 后 React 组件渲染会分为两个阶段,即 render 和 commit 阶段。

  • render 阶段决定需要进行哪些变更。这个阶段 React 会调用 render 函数,并将结果和上一次 render 的结果进行 diff, 计算出需要进行变更的操作队列

  • commit阶段。或者称为提交阶段, 在这个阶段会执行 render 阶段 diff 出来的变更请求。比如 DOM 插入、更新、删除、排序等等。在这个阶段 React 还会调用 componentDidMount 和 componentDidUpdate 生命周期函数.

2、选择其他图形展示形式

例如Ranked 视图,这个视图按照渲染消耗时间对组件进行排序:

188aae115536f8d9065cc20fbe9c424a.png

3、火焰图

这个图其实就是组件树,Profiler 使用颜色来标记哪些组件被重新渲染。和 commit 列表以及 Ranked 图一样,颜色在这里是有意义的,比如灰色表示没有重新渲染;从渲染消耗的时间上看的话: 黑色 > 黄色 > 蓝色, 通过 Ranked 图可以直观感受到不同颜色之间的意义

4、另外可以通过设置,筛选 Commit,以及是否显示原生元素:

4f32e01441098673e79e53b886a76e2d.png

5、当前选中组件或者 Commit 的详情, 双击具体组件可以详细比对每一次 commit 消耗的时间

49671d7efe9f7e4303d24b34c11d61d3.png

简单总结下查看流程:

1、改配置:排除影响因素,去掉无意义的 commit,开启 render 原因记录

c23e85717de2a629df050b2809414a05.png

2、横看缩略图

577c05d6d2d4e430eca604787aeebff1.png

3、纵看火焰图

469f2b3ebe017492837620f1f3ce81fa.png

4、跟踪单个组件

274f7b2c6db51c9f418af0df8eaccde6.png

2、Chrome Dev Tools

a、Performance

在 v16.5 之前,一般都是利用 Chrome 自带的 Performance 来进行 React 性能测量:

8d697300f908305f410f5f099e2e3d30.png

React 使用标准的User Timing API(所有支持该标准的浏览器都可以用来分析 React)来记录操作,所以我们在 Timings 标签中查看 React 的渲染过程。React 还特意使用 emoji 标记。

相对 React Devtool 而言 Performance 工具可能还不够直观,但是它非常强大, 使用 Performance 可以用来定位一些比较深层次的问题,这可能需要你对 React 的实现原理有一定了解, 就像使用 Wireshark 你需要懂点网络协议一样

所以说使用 Performance 工具有以下优势:

  • 可以测量分析整个渲染的过程细节. 它可以定位某些具体方法的调用过程和消耗, 方便定位一些深层次问题.

  • 可以测量分析底层 DOM 的绘制、布局、合成等细节。方便定位浏览器性能问题

具体应该怎么看?

  • 尽可能缩小范围

  • 主要看:时序图、火焰图、占比图

1cc61a5da3345051b17c526ae5ae6222.png

b、Memory

f59a092bdb0f0ce1ae81d4cc0a75011e.png

c、Light House

1f1a515ad151d04ee3ca59fd6b072f12.png

16个React 性能优化方法

之前的总结先来回顾下性能优化的三个方面:

1、前端通用优化。这类优化在所有前端框架中都存在,重点就在于如何将这些技巧应用在 React 组件中。

2、减少不必要的组件更新。这类优化是在组件状态发生变更后,通过减少不必要的组件更新来实现,对应到 React 中就是:减少渲染的节点 、降低组件渲染的复杂度、充分利用缓存避免重新渲染(利用缓存可以考虑使用PureComponent、React.memo、hook函数useCallback、useMemo等方法)

PureComponent 是对类组件的 Props 和 State 进行浅比较;React.memo 是对函数组件的 Props 进行浅比较

3、提交阶段优化。这类优化的目的是减少提交阶段耗时。

前端通用优化

这类优化在所有前端框架中都存在,本文的重点就在于将这些技巧应用在 React 组件中。

1、组件按需加载

组件按需加载优化又可以分为:懒加载、懒渲染、虚拟列表 三类。

懒加载

在 SPA 中,懒加载优化一般用于从一个路由跳转到另一个路由。还可用于用户操作后才展示的复杂组件,比如点击按钮后展示的弹窗模块。在这些场景下,可以结合 Code Split 实现。

懒加载的实现主要是通过 Webpack 的动态导入和 React.lazy 方法。注意,实现懒加载优化时,不仅要考虑加载态,还需要对加载失败进行容错处理。

import { lazy, Suspense, Component } from "react"
import "./styles.css"

// 对加载失败进行容错处理
class ErrorBoundary extends Component {
  constructor(props) {
    super(props)
    this.state = { hasError: false }
  }

  static getDerivedStateFromError(error) {
    return { hasError: true }
  }

  render() {
    if (this.state.hasError) {
      return <h1>这里处理出错场景</h1>
    }

    return this.props.children
  }
}

const Comp = lazy(() => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.5) {
        reject(new Error("模拟网络出错"))
      } else {
        resolve(import("./Component"))
      }
    }, 2000)
  })
})

export default function App() {
  return (
    <div className="App">
      <div style={{ marginBottom: 20 }}>
        实现懒加载优化时,不仅要考虑加载态,还需要对加载失败进行容错处理。
      </div>
      <ErrorBoundary>
        <Suspense fallback="Loading...">
          <Comp />
        </Suspense>
      </ErrorBoundary>
    </div>
  )
}

懒渲染

懒渲染指当组件进入或即将进入可视区域时才渲染组件。常见的组件 Modal/Drawer 等,当 visible 属性为 true 时才渲染组件内容,也可以认为是懒渲染的一种实现。

懒渲染的使用场景有:

  • 页面中出现多次的组件,且组件渲染费时、或者组件中含有接口请求。如果渲染多个带有请求的组件,由于浏览器限制了同域名下并发请求的数量,就可能会阻塞可见区域内的其他组件中的请求,导致可见区域的内容被延迟展示。

  • 需用户操作后才展示的组件。这点和懒加载一样,但懒渲染不用动态加载模块,不用考虑加载态和加载失败的兜底处理,实现上更简单。

判断组件是否出现在可视区域内是通过 react-visibility-observer 进行监听。

import { useState, useEffect } from "react"
import VisibilityObserver, {
  useVisibilityObserver,
} from "react-visibility-observer"

const VisibilityObserverChildren = ({ callback, children }) => {
  const { isVisible } = useVisibilityObserver()
  useEffect(() => {
    callback(isVisible)
  }, [callback, isVisible])

  return <>{children}</>
}

export const LazyRender = () => {
  const [isRendered, setIsRendered] = useState(false)

  if (!isRendered) {
    return (
      <VisibilityObserver rootMargin={"0px 0px 0px 0px"}>
        <VisibilityObserverChildren
          callback={isVisible => {
            if (isVisible) {
              setIsRendered(true)
            }
          }}
        >
          <span />
        </VisibilityObserverChildren>
      </VisibilityObserver>
    )
  }

  console.log("滚动到可视区域才渲染")
  return <div>我是 LazyRender 组件</div>
}

export default LazyRender

虚拟列表

虚拟列表是懒渲染的一种特殊场景。实现虚拟列表的组件有 react-window 和 react-virtualized。react-window 是 react-virtualized 的轻量版本,其 API 和文档更加友好。新项目中推荐使用 react-window。

使用 react-window 很简单,只需要计算每项的高度即可。如果每项的高度是变化的,可给 itemSize 参数传一个函数。

import { FixedSizeList as List } from "react-window"
const Row = ({ index, style }) => <div style={style}>Row {index}</div>

const Example = () => (
  <List
    height={150}
    itemCount={1000}
    itemSize={35} // 每项的高度为 35
    width={300}
  >
    {Row}
  </List>
)

2、批量更新

关于如何实现批量更新可参考之前的文章:葡萄zi:React函数式组件中实现批量更新的两种方式,看下你用对了吗?

在React18中会有并发模式,在并发模式中,将默认以批量更新方式执行 setState。到那时候,或许就不需要这个优化了。

3、按优先级更新,及时响应用户

优先级更新是批量更新的逆向操作,其思想是:优先响应用户行为,再完成耗时操作。常见的场景是:页面弹出一个 Modal,当用户点击 Modal 中的确定按钮后,代码将执行两个操作。a) 关闭 Modal。b) 页面处理 Modal 传回的数据并展示给用户。当 b) 操作需要执行 500ms 时,用户会明显感觉到从点击按钮到 Modal 被关闭之间的延迟。

4、利用debounce、throttle 避免重复回调

在搜索组件中,当 input 中内容修改时就触发搜索回调。当组件能很快处理搜索结果时,用户不会感觉到输入延迟。但实际场景中,中后台应用的列表页非常复杂,组件对搜索结果的 Render 会造成页面卡顿,明显影响到用户的输入体验。

在搜索场景中一般使用 useDebounce + useEffect 的方式获取数据。

例子参考:debounce-search。

import { useState, useEffect } from "react"
import { useDebounce } from "use-debounce"

export default function App() {
  const [text, setText] = useState("Hello")
  const [debouncedValue] = useDebounce(text, 300)

  useEffect(() => {
    // 根据 debouncedValue 进行搜索
  }, [debouncedValue])

  return (
    <div>
      <input
        defaultValue={"Hello"}
        onChange={e => {
          setText(e.target.value)
        }}
      />
      <p>Actual value: {text}</p>
      <p>Debounce value: {debouncedValue}</p>
    </div>
  )
}

为什么搜索场景中是使用 debounce,而不是 throttle 呢?

throttle 是 debounce 的特殊场景,throttle 给 debounce 传了 maxWait 参数,可参考 useThrottleCallback。在搜索场景中,只需响应用户最后一次输入,无需响应用户的中间输入值,debounce 更适合使用在该场景中。而 throttle 更适合需要实时响应用户的场景中更适合,如通过拖拽调整尺寸或通过拖拽进行放大缩小(如:window 的 resize 事件)。实时响应用户操作场景中,如果回调耗时小,甚至可以用 requestAnimationFrame 代替 throttle。

5、缓存优化

缓存优化往往是最简单有效的优化方式,在 React 组件中常用 useMemo 缓存上次计算的结果。当 useMemo 的依赖未发生改变时,就不会触发重新计算。一般用在「计算派生状态的代码」非常耗时的场景中,如:遍历大列表做统计信息。

  • React 官方并不保证 useMemo 一定会进行缓存,所以可能在依赖不改变时,仍然执行重新计算。参考 How to memoize calculations

  • useMemo 只能缓存最近一次函数执行的结果,如果想缓存更多次函数执行的结果,可使用 memoizee。

跳过不必要的组件更新

1、PureComponent、React.memo

在 React 工作流中,如果只有父组件发生状态更新,即使父组件传给子组件的所有 Props 都没有修改,也会引起子组件的 Render 过程。从 React 的声明式设计理念来看,如果子组件的 Props 和 State 都没有改变,那么其生成的 DOM 结构和副作用也不应该发生改变。当子组件符合声明式设计理念时,就可以忽略子组件本次的 Render 过程。PureComponent 和 React.memo 就是应对这种场景的,PureComponent 是对类组件的 Props 和 State 进行浅比较,React.memo 是对函数组件的 Props 进行浅比较。

2、 shouldComponentUpdate

在 React 刚开源的那段时期,数据不可变性还没有现在这样流行。当时 Flux 架构就使用的模块变量来维护 State,并在状态更新时直接修改该模块变量的属性值,而不是使用展开语法生成新的对象引用。例如要往数组中添加一项数据时,当时的代码很可能是 state.push(item),而不是 const newState = [...state, item]。这点可参考 Dan Abramov 在演讲 Redux 时演示的 Flux 代码。

5c13072a6812d880d8bcce46fbc850b5.png

在此背景下,当时的开发者经常使用 shouldComponentUpdate 来深比较 Props,只在 Props 有修改才执行组件的 Render 过程。如今由于数据不可变性和函数组件的流行,这样的优化场景已经不会再出现了。

接下来介绍另一种可以使用 shouldComponentUpdate 来优化的场景。在项目初始阶段,开发者往往图方便会给子组件传递一个大对象作为 Props,后面子组件想用啥就用啥。当大对象中某个「子组件未使用的属性」发生了更新,子组件也会触发 Render 过程。在这种场景下,通过实现子组件的 shouldComponentUpdate 方法,仅在「子组件使用的属性」发生改变时才返回 true,便能避免子组件重新 Render。

但使用 shouldComponentUpdate 优化第二个场景有两个弊端。

  • 如果存在很多子孙组件,「找出所有子孙组件使用的属性」就会有很多工作量,也容易因为漏测导致 bug。

  • 存在潜在的工程隐患。举例来说,假设组件结构如下。

<A data="{data}">
  {/* B 组件只使用了 data.a 和 data.b */}
  <B data="{data}">
    {/* C 组件只使用了 data.c */}
    <C data="{data}"></C>
  </B>
</A>

B 组件的 shouldComponentUpdate 中只比较了 data.a 和 data.b,目前是没任何问题的。之后开发者想在 C 组件中使用 data.c,假设项目中 data.a 和 data.c 是一起更新的,所以也没任何问题。但这份代码已经变得脆弱了,如果某次修改导致 data.a 和 data.c 不一起更新了,那么系统就会出问题。而且实际业务中代码往往更复杂,从 B 到 C 可能还有若干中间组件,这时就很难想到是 shouldComponentUpdate 引起的问题了。

第二个场景最好的解决方案是使用发布者订阅者模式,只是代码改动要稍多一些,可参考本文的优化技巧「发布者订阅者跳过中间组件 Render 过程」。

第二个场景也可以在父子组件间增加中间组件,中间组件负责从父组件中选出子组件关心的属性,再传给子组件。相比于 shouldComponentUpdate 方法,会增加组件层级,但不会有第二个弊端。

本文中的跳过回调函数改变触发的 Render 过程也可以用 shouldComponentUpdate 实现,因为回调函数并不参与组件的 Render 过程。

3、useMemo、useCallback 实现稳定的 Props 值

如果传给子组件的派生状态或函数,每次都是新的引用,那么 PureComponent 和 React.memo 优化就会失效。所以需要使用 useMemo 和 useCallback 来生成稳定值,并结合 PureComponent 或 React.memo 避免子组件重新 Render。

拓展知识

useCallback 是「useMemo 的返回值为函数」时的特殊情况,是 React 提供的便捷方式。在 React Server Hooks 代码 中,useCallback 就是基于 useMemo 实现的。尽管 React Client Hooks 没有使用同一份代码,但 useCallback 的代码逻辑和 useMemo 的代码逻辑仍是一样的。

4、发布者订阅者跳过中间组件 Render 过程

React 推荐将公共数据放在所有「需要该状态的组件」的公共祖先上,但将状态放在公共祖先上后,该状态就需要层层向下传递,直到传递给使用该状态的组件为止。每次状态的更新都会涉及中间组件的 Render 过程,但中间组件并不关心该状态,它的 Render 过程只负责将该状态再传给子组件。在这种场景下可以将状态用发布者订阅者模式维护,只有关心该状态的组件才去订阅该状态,不再需要中间组件传递该状态。当状态更新时,发布者发布数据更新消息,只有订阅者组件才会触发 Render 过程,中间组件不再执行 Render 过程。

只要是发布者订阅者模式的库,都可以进行该优化。比如:redux、use-global-state、React.createContext 等。例子参考:发布者订阅者模式跳过中间组件的渲染阶段,本示例使用 React.createContext 进行实现。

import { useState, useEffect, createContext, useContext } from "react"

const renderCntMap = {}
const renderOnce = name => {
  return (renderCntMap[name] = (renderCntMap[name] || 0) + 1)
}

// 将需要公共访问的部分移动到 Context 中进行优化
// Context.Provider 就是发布者
// Context.Consumer 就是消费者
const ValueCtx = createContext()
const CtxContainer = ({ children }) => {
  const [cnt, setCnt] = useState(0)
  useEffect(() => {
    const timer = window.setInterval(() => {
      setCnt(v => v + 1)
    }, 1000)
    return () => clearInterval(timer)
  }, [setCnt])

  return <ValueCtx.Provider value={cnt}>{children}</ValueCtx.Provider>
}

function CompA({}) {
  const cnt = useContext(ValueCtx)
  // 组件内使用 cnt
  return <div>组件 CompA Render 次数:{renderOnce("CompA")}</div>
}

function CompB({}) {
  const cnt = useContext(ValueCtx)
  // 组件内使用 cnt
  return <div>组件 CompB Render 次数:{renderOnce("CompB")}</div>
}

function CompC({}) {
  return <div>组件 CompC Render 次数:{renderOnce("CompC")}</div>
}

export const PubSubCommunicate = () => {
  return (
    <CtxContainer>
      <div>
        <h1>优化后场景</h1>
        <div>
          将状态提升至最低公共祖先的上层,用 CtxContainer 将其内容包裹。
        </div>
        <div style={{ marginTop: "20px" }}>
          每次 Render 时,只有组件A和组件B会重新 Render 。
        </div>

        <div style={{ marginTop: "40px" }}>
          父组件 Render 次数:{renderOnce("parent")}
        </div>
        <CompA />
        <CompB />
        <CompC />
      </div>
    </CtxContainer>
  )
}

export default PubSubCommunicate

5、状态下放,缩小状态影响范围

如果一个状态只在某部分子树中使用,那么可以将这部分子树提取为组件,并将该状态移动到该组件内部。如下面的代码所示,虽然状态 color 只在和

中使用,但 color 改变会引起重新 Render。

import { useState } from "react"

export default function App() {
  let [color, setColor] = useState("red")
  return (
    <div>
      <input value={color} onChange={e => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
      <ExpensiveTree />
    </div>
  )
}

function ExpensiveTree() {
  let now = performance.now()
  while (performance.now() - now < 100) {
    // Artificial delay -- do nothing for 100ms
  }
  return <p>I am a very slow component tree.</p>
}

通过将 color 状态、和

提取到组件 Form 中,结果如下。

export default function App() {
  return (
    <>
      <Form />
      <ExpensiveTree />
    </>
  )
}

function Form() {
  let [color, setColor] = useState("red")
  return (
    <>
      <input value={color} onChange={e => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
    </>
  )
}

这样调整之后,color 改变就不会引起组件 App 和 ExpensiveTree 重新 Render 了。

如果对上面的场景进行扩展,在组件 App 的顶层和子树中都使用了状态 color ,但仍然不关心它,如下所示。

import { useState } from "react"

export default function App() {
  let [color, setColor] = useState("red")
  return (
    <div style={{ color }}>
      <input value={color} onChange={e => setColor(e.target.value)} />
      <ExpensiveTree />
      <p style={{ color }}>Hello, world!</p>
    </div>
  )
}

在这种场景中,我们仍然将 color 状态抽取到新组件中,并提供一个插槽来组合,如下所示。

import { useState } from "react"

export default function App() {
  return <ColorContainer expensiveTreeNode={<ExpensiveTree />}></ColorContainer>
}

function ColorContainer({ expensiveTreeNode }) {
  let [color, setColor] = useState("red")
  return (
    <div style={{ color }}>
      <input value={color} onChange={e => setColor(e.target.value)} />
      {expensiveTreeNode}
      <p style={{ color }}>Hello, world!</p>
    </div>
  )
}

这样调整之后,color 改变就不会引起组件 App 和 ExpensiveTree 重新 Render 了。

该优化技巧来源于before-you-memo,Dan 认为这种优化方式在 Server Component 场景下更有效,因为可以在服务端执行。

6、列表项使用 key 属性

当渲染列表项时,如果不给组件设置不相等的属性 key,就会收到如下报警。

1d5b84ecbdbd8581b2c96dd5438adba0.png

相信很多开发者已经见过该报警成百上千次了,那 key 属性到底在优化了什么呢?举个 ,在不使用 key 时,组件两次 Render 的结果如下。

<!-- 前一次 Render 结果 -->
<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<!-- 新的 Render 结果 -->
<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

此时 React 的 Diff 算法会按照

  • 出现的先后顺序进行比较,得出结果为需要更新前两个

  • 并创建内容为 Villanova 的li,一共会执行两次 DOM 更新、一次 DOM 创建。

    如果加上 React 的 key 属性,两次 Render 结果如下。

    <!-- 前一次 Render 结果 -->
    <ul>
      <li key="2015">Duke</li>
      <li key="2016">Villanova</li>
    </ul>
    
    <!-- 新的 Render 结果 -->
    <ul>
      <li key="2014">Connecticut</li>
      <li key="2015">Duke</li>
      <li key="2016">Villanova</li>
    </ul>

    React Diff 算法会把 key 值为 2015 的虚拟 DOM 进行比较,发现 key 为 2015 的虚拟 DOM 没有发生修改,不用更新。同样,key 值为 2016 的虚拟 DOM 也不需要更新。结果就只需要创建 key 值为 2014 的虚拟 DOM。相比于不使用 key 的代码,使用 key 节省了两次 DOM 更新操作。

    如果把例子中的

  • 换成自定义组件,并且自定义组件使用了 PureComponent 或 React.memo 优化。那么使用 key 属性就不只节省了 DOM 更新,还避免了组件的 Render 过程。

    React 官方推荐将每项数据的 ID 作为组件的 key,以达到上述的优化目的。并且不推荐使用每项的索引作为 key,因为传索引作为 key 时,就会退化为不使用 key 时的代码。那么是否在所有列表渲染的场景下,使用 ID 都优于使用索引呢?

    答案是否定的,在常见的分页列表中,第一页和第二页的列表项 ID 都是不同,假设每页展示三条数据,那么切换页面前后组件 Render 结果如下。

    <-- 第一页的列表项虚拟 DOM -->
    <li key="a">dataA</li>
    <li key="b">dataB</li>
    <li key="c">dataC</li>
    
    <-- 切换到第二页后的虚拟 DOM -->
    <li key="d">dataD</li>
    <li key="e">dataE</li>
    <li key="f">dataF</li>

    切换到第二页后,由于所有

  • 的 key 值不同,所以 Diff 算法会将第一页的所有 DOM 节点标记为删除,然后将第二页的所有 DOM 节点标记为新增。整个更新过程需要三次 DOM 删除、三次 DOM 创建。如果不使用 key,Diff 算法只会将三个

  • 节点标记为更新,执行三次 DOM 更新。参考 Demo 没有添加、删除、排序功能的分页列表,使用 key 时每次翻页耗时约为 140ms,而不使用 key 仅为 70ms。

    尽管存在以上场景,React 官方仍然推荐使用 ID 作为每项的 key 值。其原因有两:

    在列表中执行删除、插入、排序列表项的操作时,使用 ID 作为 key 将更高效。而翻页操作往往伴随着 API 请求,DOM 操作耗时远小于 API 请求耗时,是否使用 ID 在该场景下对用户体验影响不大。

    使用 ID 做为 key 可以维护该 ID 对应的列表项组件的 State。举个例子,某表格中每列都有普通态和编辑态两个状态,起初所有列都是普通态,用户点击第一行第一列,使其进入编辑态。然后用户又拖拽第二行,将其移动到表格的第一行。如果开发者使用索引作为 key,那么第一行第一列的状态仍然为编辑态,而用户实际希望编辑的是第二行的数据,在用户看来就是不符合预期的。尽管这个问题可以通过将「是否处于编辑态」存放在数据项的数据中,利用 Props 来解决,但是使用 ID 作为 key 不是更香吗?

    7、useMemo 返回虚拟 DOM

    利用 useMemo 可以缓存计算结果的特点,如果 useMemo 返回的是组件的虚拟 DOM,则将在 useMemo 依赖不变时,跳过组件的 Render 阶段。该方式与 React.memo 类似,但与 React.memo 相比有以下优势:

    更方便。React.memo 需要对组件进行一次包装,生成新的组件。而 useMemo 只需在存在性能瓶颈的地方使用,不用修改组件。

    更灵活。useMemo 不用考虑组件的所有 Props,而只需考虑当前场景中用到的值,也可使用 useDeepCompareMemo 对用到的值进行深比较。

    例子参考:useMemo 跳过组件 Render 过程。该例子中,父组件状态更新后,不使用 useMemo 的子组件会执行 Render 过程,而使用 useMemo 的子组件不会执行。

    import { useEffect, useMemo, useState } from "react"
    import "./styles.css"
    
    const renderCntMap = {}
    
    function Comp({ name }) {
      renderCntMap[name] = (renderCntMap[name] || 0) + 1
      return (
        <div>
          组件「{name}」 Render 次数:{renderCntMap[name]}
        </div>
      )
    }
    
    export default function App() {
      const setCnt = useState(0)[1]
      useEffect(() => {
        const timer = window.setInterval(() => {
          setCnt(v => v + 1)
        }, 1000)
        return () => clearInterval(timer)
      }, [setCnt])
    
      const comp = useMemo(() => {
        return <Comp name="使用 useMemo 作为 children" />
      }, [])
    
      return (
        <div className="App">
          <Comp name="直接作为 children" />
          {comp}
        </div>
      )
    }

    8、跳过回调函数改变触发的 Render 过程

    React 组件的 Props 可以分为两类。a) 一类是在对组件 Render 有影响的属性,如:页面数据、getPopupContainer 和 renderProps 函数。b) 另一类是组件 Render 后的回调函数,如:onClick、onVisibleChange。b) 类属性并不参与到组件的 Render 过程,因为可以对 b) 类属性进行优化。当 b)类属性发生改变时,不触发组件的重新 Render ,而是在回调触发时调用最新的回调函数。

    Dan Abramov 在 A Complete Guide to useEffect 文章中认为,每次 Render 都有自己的事件回调是一件很酷的特性。但该特性要求每次回调函数改变就触发组件的重新 Render ,这在性能优化过程中是可以取舍的。

    例子参考:跳过回调函数改变触发的 Render 过程。Demo 中通过拦截子组件的 Props 实现,仅仅是因为笔者比较懒不想改了,这种实现方式也能开阔读者视野吧。实际上该优化思想应该通过 useMemo/React.memo 实现,且使用 useMemo 实现时也更容易理解。

    import { Children, cloneElement, memo, useEffect, useRef } from "react"
    import { useDeepCompareMemo } from "use-deep-compare"
    import omit from "lodash.omit"
    
    let renderCnt = 0
    
    export function SkipNotRenderProps({ children, skips }) {
      if (!skips) {
        // 默认跳过所有回调函数
        skips = prop => prop.startsWith("on")
      }
    
      const child = Children.only(children)
      const childProps = child.props
      const propsRef = useRef({})
      const nextSkippedPropsRef = useRef({})
      Object.keys(childProps)
        .filter(it => skips(it))
        .forEach(key => {
          // 代理函数只会生成一次,其值始终不变
          nextSkippedPropsRef.current[key] =
            nextSkippedPropsRef.current[key] ||
            function skipNonRenderPropsProxy(...args) {
              propsRef.current[key].apply(this, args)
            }
        })
    
      useEffect(() => {
        propsRef.current = childProps
      })
    
      // 这里使用 useMemo 优化技巧
      // 除去回调函数,其他属性改变生成新的 React.Element
      return useShallowCompareMemo(() => {
        return cloneElement(child, {
          ...child.props,
          ...nextSkippedPropsRef.current,
        })
      }, [omit(childProps, Object.keys(nextSkippedPropsRef.current))])
    }
    
    // SkipNotRenderPropsComp 组件内容和 Normal 内容一样
    export function SkipNotRenderPropsComp({ onClick }) {
      return (
        <div className="case">
          <div className="caseHeader">
            跳过『与 Render 无关的 Props』改变触发的重新 Render
          </div>
          Render 次数为:{++renderCnt}
          <div>
            <button onClick={onClick} style={{ color: "blue" }}>
              点我回调,回调弹出值为 1000(优化成功)
            </button>
          </div>
        </div>
      )
    }
    
    export default SkipNotRenderPropsComp

    9、 Hooks 按需更新

    如果自定义 Hook 暴露多个状态,而调用方只关心某一个状态,那么其他状态改变就不应该触发组件重新 Render。

    export const useNormalDataHook = () => {
      const [data, setData] = useState({ info: null, count: null })
      useEffect(() => {
        const timer = setInterval(() => {
          setData(data => ({
            ...data,
            count: data.count + 1,
          }))
        }, 1000)
    
        return () => {
          clearInterval(timer)
        }
      })
    
      return data
    }

    如上所示,useNormalDataHook 暴露了两个状态 info 和 count 给调用方,如果调用方只关心 info 字段,那么 count 改变就没必要触发调用方组件 Render。

    按需更新主要通过两步来实现,参考Hooks 按需更新

    根据调用方使用的数据进行依赖收集,Demo 中使用 Object.defineProperties 实现。只在依赖发生改变时才触发组件更新。

    10、动画库直接修改 DOM 属性

    这个优化在业务中应该用不上,但还是非常值得学习的,将来可以应用到组件库中。参考 react-spring 的动画实现,当一个动画启动后,每次动画属性改变不会引起组件重新 Render ,而是直接修改了 dom 上相关属性值。

    例子演示:CodeSandbox 在线 Demo
    
    import React, { useState } from "react"
    import { useSpring, animated as a } from "react-spring"
    import "./styles.css"
    
    let renderCnt = 0
    export function Card() {
      const [flipped, set] = useState(false)
      const { transform, opacity } = useSpring({
        opacity: flipped ? 1 : 0,
        transform: `perspective(600px) rotateX(${flipped ? 180 : 0}deg)`,
        config: { mass: 5, tension: 500, friction: 80 },
      })
    
      // 尽管 opacity 和 transform 的值在动画期间一直变化
      // 但是并没有组件的重新 Render
      return (
        <div onClick={() = set(state = !state)}>
          <div style={{ position: "fixed", top: 10, left: 10 }}>
            Render 次数:{++renderCnt}
          </div>
          <a.div
            class="c back"
            style={{ opacity: opacity.interpolate(o => 1 - o), transform }}
          />
          <a.div
            class="c front"
            style={{
              opacity,
              transform: transform.interpolate(t => `${t} rotateX(180deg)`),
            }}
          />
        </div>
      )
    }
    
    export default Card

    提交阶段优化

    这类优化的目的是减少提交阶段耗时,该分类中仅有一条优化技巧。

    1、避免在 didMount、didUpdate 中更新组件 State

    这个技巧不仅仅适用于 didMount、didUpdate,还包括 willUnmount、useLayoutEffect 和特殊场景下的 useEffect(当父组件的 cDU/cDM 触发时,子组件的 useEffect 会同步调用),本文为叙述方便将他们统称为「提交阶段钩子」。

    React 工作流提交阶段的第二步就是执行提交阶段钩子,它们的执行会阻塞浏览器更新页面。如果在提交阶段钩子函数中更新组件 State,会再次触发组件的更新流程,造成两倍耗时。

    一般在提交阶段的钩子中更新组件状态的场景有:

    React Profiler 定位 Render 过程瓶颈

    React Profiler 是 React 官方提供的性能审查工具,本文只介绍笔者的使用心得,详细的使用手册请移步官网文档。

    Profiler 只记录了 Render 过程耗时

    通过 React Profiler,开发者可以查看组件 Render 过程耗时,但无法知晓提交阶段的耗时。尽管 Profiler 面板中有 Committed at 字段,但这个字段是相对于录制开始时间,根本没有意义。所以提醒读者不要通过 Profiler 定位非 Render 过程的性能瓶颈问题。

    通过在 React v16 版本上进行实验,同时开启 Chrome 的 Performance 和 React Profiler 统计。如下图,在 Performance 面板中,调和阶段和提交阶段耗时分别为 642ms 和 300ms,而 Profiler 面板中只显示了 642ms,没有提交阶段的耗时信息。

    79621fdfec32e32d7578d5fcd55dc82b.pnga889e5edbcc0e6493677340aa9731b8e.png

    React 在 v17 版本后已移除 User Timing 统计功能,具体原因可参考 PR#18417。在 v17 版本上,笔者也通过测试代码验证了 Profiler 中的统计信息并不包含提交阶段,有兴趣的读者可以看看。

    开启「记录组件更新原因」

    点击面板上的齿轮,然后勾选「Record why each component rendered while profiling.」,如下图。

    384ad82c30f3d380a80e15d6a5ab26cf.png

    然后点击面板中的虚拟 DOM 节点,右侧便会展示该组件重新 Render 的原因。

    c2e86e7a99187fc3ea49f7ed307a7000.png

    定位产生本次 Render 过程原因

    由于 React 的批量更新(Batch Update)机制,产生一次 Render 过程可能涉及到很多个组件的状态更新。那么如何定位是哪些组件状态更新导致的呢?

    在 Profiler 面板左侧的虚拟 DOM 树结构中,从上到下审查每个发生了渲染的(不会灰色的)组件。如果组件是由于 State 或 Hook 改变触发了 Render 过程,那它就是我们要找的组件,如下图。

    1727658676e79b660e9703b64f9e8214.png

    总结

    通过本文,我们先掌握了 React 的工作流,React 组件一次状态更新分为调和阶段和提交阶段。React 项目的性能优化点主要集中在调和阶段,通过尽量避免不必要的组件进入 Render 过程达到优化的目的。在真实项目中,如果遇到性能问题,先通过 React Profiler 定位耗时原因,然后根据具体情况#选择优化手段。

    选择优化技巧

    • 如果是因为存在不必要更新的组件进入了 Render 过程,则选择跳过不必要的组件更新进行优化。

    • 如果是因为页面挂载了太多不可见的组件,则选择懒加载、懒渲染或虚拟列表进行优化。

    • 如果是因为多次设置状态,引起了多次状态更新,则选择批量更新或debounce、throttle 优化频繁触发的回调进行优化。

    • 如果组件 Render 逻辑的确非常耗时,我们需要先定位到耗时代码,并判断能否通过缓存优化它。如果能,则选择缓存优化,否则选择按优先级更新,及时响应用户,将组件逻辑进行拆解,以便更快响应用户

    • 计算并更新组件的派生状态(Derived State)。在该场景中,类组件应使用 getDerivedStateFromProps 钩子方法代替,函数组件应使用函数调用时执行 setState的方式代替。使用上面两种方式后,React 会将新状态和派生状态在一次更新内完成。

    • 根据 DOM 信息,修改组件状态。在该场景中,除非想办法不依赖 DOM 信息,否则两次更新过程是少不了的,就只能用其他优化技巧了。use-swr 的源码就使用了该优化技巧。当某个接口存在缓存数据时,use-swr 会先使用该接口的缓存数据,并在 requestIdleCallback 时再重新发起请求,获取最新数据。如果 use-swr 不做该优化的话,就会在 useLayoutEffect 中触发重新验证并设置 isValidating 状态为 true,引起组件的更新流程,造成性能损失。

最后, 送人玫瑰,手留余香,觉得有收获的朋友可以点赞,关注一波 ,我们组建了高级前端交流群,如果您热爱技术,想一起讨论技术,交流进步,不管是面试题,工作中的问题,难点热点都可以在交流群交流,为了拿到大Offer,邀请您进群,入群就送前端精选100本电子书以及 阿里面试前端精选资料 添加 下方小助手二维码或者扫描二维码 就可以进群。让我们一起学习进步.

2ee6476c01dfb1e1deac1bf33f8c564d.png

6b7042d007e90532bb1a58d15e80a693.png


推荐阅读

(点击标题可跳转阅读)

[极客前沿]-你不知道的 React 18 新特性

[极客前沿]-写给前端的 K8s 上手指南

[极客前沿]-写给前端的Docker上手指南

[面试必问]-你不知道的 React Hooks 那些糟心事

[面试必问]-一文彻底搞懂 React 调度机制原理

[面试必问]-一文彻底搞懂 React 合成事件原理

[面试必问]-全网最简单的React Hooks源码解析

[面试必问]-一文掌握 Webpack 编译流程

[面试必问]-一文深度剖析 Axios 源码

[面试必问]-一文掌握JavaScript函数式编程重点

[面试必问]-阿里,网易,滴滴,头条等20家面试真题

[面试必问]-全网最全 React16.0-16.8 特性总结

[架构分享]- 微前端qiankun+docker+nginx自动化部署

[架构分享]-石墨文档 Websocket 百万长连接技术实践

[自我提升]-Javascript条件逻辑设计重构

[自我提升]-送给React开发者十九条性能优化建议

[自我提升]-页面可视化工具的前世今生

[大前端之路]-连前端都看得懂的《Nginx 入门指南》

[软实力提升]-金三银四,如何写一份面试官心中的简历

觉得本文对你有帮助?请分享给更多人

关注「React中文社区」加星标,每天进步

1d59bb2462237eeb136c1a521ea5364b.png   

点个

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

React 性能优化指南之性能分析与16种优化方法大总结 的相关文章

  • 如何在 JPA 中使用枚举

    我有一个电影租赁系统的现有数据库 每部电影都有一个评级属性 在 SQL 中 他们使用约束来限制该属性的允许值 CONSTRAINT film rating check CHECK rating text text OR rating tex
  • 抽象超类的默认接口方法

    可以说我有以下结构 abstract class A abstract boolean foo interface B default boolean foo return doBlah class C extends A implemen
  • java替代Thread.stop()来中断特定调用

    我正在寻找一种方法来告诉这个调用 大约需要 20 120 秒 final Area image final AffineTransform transform new AffineTransform transform scale imag
  • 如何将列表转换为地图?

    最近我和一位同事讨论了转换的最佳方式是什么List to Map在 Java 中 这样做是否有任何具体的好处 我想知道最佳的转换方法 如果有人可以指导我 我将非常感激 这是个好方法吗 List
  • Eclipse 自动完成更改变量名称

    只是一个愚蠢的问题 但很难搜索 因为有很多关于 Eclipse 自动完成的主题 而且很难找到与我的问题匹配的内容 所以问题是 如果我写 MyClass MyVarName 然后按空格键 添加 new MyClass Eclipse 自动添加
  • 适用于 Solaris 的 Java 8 中缺少 javaws

    看起来 Oracle 从 Java 8 for Solaris 中删除了 Java Web Start javaws 在 Java 8u51 中不再可用 来自兼容性指南 http www oracle com technetwork jav
  • 我需要显式关闭连接吗?

    我持有一个实例MongoClient and DB在我的应用程序中 每次我想执行某些操作时 我都会调用getCollection 我想知道是否需要显式关闭连接 就像connection close 在 JDBC 中 强调一下 我只有一个Mo
  • ASM之前看一下maxStack指令吗?

    我正在尝试使用 ASM 库将字节代码转换为不同的格式 这可以使用 MethodVisitor 来完成 就像这个简单的测试代码一样 return new MethodVisitor ASM7 Override public void visi
  • Java 7 中 Object 和 int 的比较

    最近我偶然发现了一个问题 让我停下来思考 对我来说 下面的代码应该总是会触发错误 但是当我的一位同事问我为什么 Eclipse 没有显示错误时 我无法回答任何问题 class A public static void main String
  • 使用 Java 通过 HTTP 下载未知长度的文件

    我想用java下载一个HTTP查询 但是我下载的文件在下载时有一个未确定的长度 我认为这将是相当标准的 所以我搜索并找到了它的代码片段 http snipplr com view 33805 http snipplr com view 33
  • 多对多不检索映射数据

    Spring boot 2 5 6 我无法安装版本 概要文件 java Getter Setter NoArgsConstructor AllArgsConstructor EqualsAndHashCode FieldDefaults l
  • 链表中的虚拟节点

    问 什么时候使用它们 作业问题 列表中的第一个和最后一个节点 有时用作列表中的第一个和最后一个节点 从未用作列表中的第一个和最后一个节点 维基百科说 哨兵节点是与链接一起使用的专门指定的节点 列表和树作为遍历路径终止符 哨兵节点的作用是 不
  • 计算移动的球与移动的线/多边形碰撞的时间(2D)

    我有一个多边形 里面有一个移动的球 如果球撞到边界 它应该反弹回来 My current solution I split the polygon in lines and calculate when the ball hits the
  • 如何告诉 IntelliJ 使用 Java 1.6 JDK 启动 gradle?

    一个简单的问题 即使经过几个小时的尝试和搜索 我也无法弄清楚 我安装了 Java 6 和 7 如何告诉 IntelliJ 使用 JDK 版本 1 6 启动 Gradle 构建 无论我做什么 IntelliJ 都会以以下方式开始我的 grad
  • Apache HttpClient TCP Keep-Alive(套接字保持活动)

    我的 http 请求需要太多时间才能被服务器处理 大约 5 分钟 由于连接闲置 5 分钟 代理服务器将关闭连接 我正在尝试在 Apache DefaultHttpClient 中使用 TCP Keep Alive 来使连接长时间处于活动状态
  • Microsoft JDBC 中的 JTDS 属性相当于什么?

    我正在将 JTDS 连接更改为 Microsoft JDBC 并且我看到存在于http jtds sourceforge net faq html http jtds sourceforge net faq htmlMicrosoft JD
  • 如何使用 Nimbus LookAndFeel 更改 JToolTip 的背景颜色?

    在使用 Nimbus LookAndFeel 的基于 Swing 的 Java 应用程序中 我尝试设置工具提示的背景颜色 因此 我创建了 JToolTip 的子类 并通过重写 createToolTip 在我的组件中使用它 到目前为止一切正
  • Drools:为什么是无状态会话?

    Drools 使用会话来存储运行时数据 为此 有两种会话 无状态和有状态 与无状态会话相比 有状态会话允许迭代调用 并且似乎比无状态会话具有所有优势 那么为什么会有无状态会话呢 他们服务的目的是什么 与有状态会话相比 它们的优势是什么 谢谢
  • 升级到 Tomcat 8 时出现 ClassNotFoundException

    我最近将 NetBeans IDE 从 v7 3 升级到 v8 突然我的应用程序在连接到数据库时在服务器启动时抛出异常 这两个版本的 IDE 之间的唯一区别是后者使用 Tomcat 8 异常日志 javax naming NamingExc
  • 线程“main”中出现异常 java.lang.UnsatisfiedLinkError: ... \jzmq.dll: 找不到依赖库

    我有一个使用 ZMQ 的 java 应用程序 我已经能够在我的 Win7 PC 上运行它 我将 jzmq dll 放在 jar 可执行文件所在的同一文件夹中 然后通过命令 java jar myapp jar 运行它 我的下一步是将其移至服

随机推荐

  • HBuilder 制表符转换成空格

    在学习BootStrap时 看到 编码规范 by mdo 里面有一条关于编辑器配置的 用两个空格代替制表符 soft tab 即用空格代表 tab 符 避免常见的代码不一致和差异 然后找到了 HBuilder 制表符转换成空格 的方法 具体
  • Linux环境SVN用户权限修改

    1 查看SVN配置文件位置 系统环境 Linux 3 10 0 使用命令行查看SVN进程 ps ef grep svn 通过进程信息可以看到svnserve conf存放的目录 svnserve conf是svn配置文件 vim 目录 sv
  • OpenHarmony与HarmonyOS联系与区别

    目录 1 背景 2 OpenHarmony 3 HarmonyOS 4 鸿蒙生态 5 OpenHarmony与HarmonyOS的技术上实现区别 1 语言支持 2 SDK 的不同 3 运行调测方式不同 4 对APK的兼容性不同 5 包含关系
  • Android CheckBox 多选以及反选清除已选项

    前言 疫情随着这个春天的到来已悄然离去 你还记得填写问卷调查的那个时候么 话不多少 这篇文章要实现的就是一个问卷调查列表 即 Listview 嵌套 Listview 实现 checkbox 多选以及反选清除已选项 正文 思路就是定义一个
  • web服务选择lighttpd,采用fcgi组件技术扩展处理业务层

    目录 一 简介fcgi web和web服务器间数据传输的桥梁 2 二 源码编译配置ARM Lighttpd Fastcgi C 3 1 交叉编译 源文件都从官网下载 Fcgi lighttpd zlib 3 2 配置服务器server do
  • VMware上安装虚拟机的一些注意事项和VMware tools的安装

    VMware上安装虚拟机 VMware是windows上的一个应用程序 它可以虚拟出一个物理主机 pc机 在该虚拟机上可以安装linux系统 相关安装流程csdn上参考过多 这里不再赘述 虚拟机安装位置要求 1 不能和VMware放在同一个
  • 为AI而生的数据库:Milvus详解及实战

    1 向量数据库 1 1 向量数据库的由来 在当今数字化时代 人工智能AI正迅速改变着我们的生活和工作方式 从智能助手到自动驾驶汽车 AI正在成为各行各业的创新引擎 然而 这种AI的崛起也带来了一个关键的挑战 如何有效地处理和分析越来越丰富和
  • QSetting读取ini配置文件失败

    今天碰到一个问题 QSettings读取配置文件失败 同样的代码用5 13版本编译后读取正常 用5 7版本编译读取不到 排除了文件编码格式的问题 最终问题解决了 原因没有找到 解决方法是试错试出来的 解决方法是把相对路径换成了绝对路径 问题
  • 常见中间件漏洞复现

    目录 Tomcat 1 Tomcat 文件上传 CVE 2017 12615 2 Tomcat 代码执行 CVE 2020 1938 3 Tomcat弱口令登录获取后台 Weblogic 4 Weblogic反序列化漏洞获取服务器权限 CV
  • Vuex4(Module)+Typescript的基本使用

    一 Vuex4介绍 vuex 是一个专为 Vue js 应用程序开发的状态管理模式 库 它采用集中式存储管理应用的所有组件的状态 并以相应的规则保证状态以一种可预测的方式发生变化 vuex包括五大核心概念分别是State Getter Mu
  • Sql server 千万级大数据SQL查询优化的几点建议

    1 对查询进行优化 应尽量避免全表扫描 首先应考虑在 where 及 order by 涉及的列上建立索引 2 应尽量避免在 where 子句中对字段进行 null 值判断 否则将导致引擎放弃使用索引而进行全表扫描 如 select id
  • 应“云”而生的云数据库,让数据从“江河”到“大海”

    随着信息技术的发展 互联网应用的加速普及 人类进入了数字经济时代 进入二十一世纪以后 随着移动互联网技术 物联网技术 5G等技术的发展 全球数据圈 Global Datasphere 呈指数级递增 IDC预测全球数据将于2025年增长至17
  • [USACO Dec20 Bronze]Stuck in a Rut

    Farmer John 最近扩大了他的农场 从奶牛们的角度看来这个农场相当于是无限大了 奶牛们将农场上放牧的区域想作是一个由正方形方格组成的无限大二维方阵 每个方格中均有美味的草 将每个方格看作是棋盘上的一个方格 Farmer John 的
  • RANSAC算法实现图像全景拼接

    文章目录 一 全景拼接的原理 1 RANSAC算法介绍 2 使用RANSAC算法来求解单应性矩阵 3 拼接图像 二 全景拼接实验 1 针对固定点位拍摄多张图片 以中间图片为中心 实现图像的拼接融合 1 输入图片 2 代码 3 运行结果 4
  • 单向链表双向链表优缺点

    单向链表优缺点 1 优点 单向链表增加删除节点简单 遍历时候不会死循环 2 缺点 只能从头到尾遍历 只能找到后继 无法找到前驱 也就是只能前进 双向链表优缺点 1 优点 可以找到前驱和后继 可进可退 2 缺点 增加删除节点复杂 多需要分配一
  • 陈嘉哲:黄金原油跳水承压,日内或将延续,如何操作?附操作建议

    陈嘉哲 7 6黄金原油跳水承压 日内有望继续下行 如何操作 附操作建议 无论行情暴涨 暴跌 单边还是震荡 你是不是总是没把握住 就是所谓的一买就跌 一跌就割 一割就涨 一涨就追 一追又套 一套再割 这就像一个死套 资金不断的缩水 过程一直在
  • 银保监局315再点名元宇宙炒作,又见监管难题,立法是否当务之急

    3月15日 北京银保监局发布 理性消费不乱贷 美好青春不负债 风险提示指出 目前网络上出现一些 小游戏 假借 元宇宙 区块链 等概念进行炒作 这是继2月18日银保监会发布 关于防范以 元宇宙 名义进行非法集资的风险提示 后 监管部门再次点名
  • 6种常用开源协议介绍

    为什么要有开源协议呢 其一 保护原作者的知识成果 防止被恶意利用 开源协议中一般都包含有免责声明 可以防止原作者承担相应风险和后果 比如你开源了一个破解Windows秘钥的软件 而使用者却用来进行商业资料窃取 那么你是不需要为此承担责任的
  • vscode的eslint配置保存自动修复代码

    提示 本文展示了vue项目中配置eslint 在vscode编辑器中保存后可以自动修复 文章目录 前言 一 vscode配置 二 vue项目package json中与eslint相关的配置 总结 前言 本次配置达到的效果 vue代码格式有
  • React 性能优化指南之性能分析与16种优化方法大总结

    本文分为两个部分 1 如何分析 React性能 1 1 性能分析指标有哪些 1 2 性能分析的两个阶段 1 3 通过工具查看指标和度量 2 16个React 性能优化方法 2 1 前端通用优化 2 2 减少不必要的组件更新 2 3 提交阶段
Powered by Hwhale