陌上人如玉
公子世无双

5.12 Profiler 深入

5.12 Profiler 深入

一句话:Profiler 的价值是「把『这次交互渲染了哪些组件、各花了多久、为什么渲染』量出来」——它有三个视图(火焰图 / 排名 / 时间线)与两个编程式 API(<Profiler onRender>),关键概念是「actualDuration vs baseDuration」,后者能直接告诉你「记忆化到底省了多少」。

一、四个视图:各自回答什么问题

⭐⭐ DevTools 的 Profiler 面板有四个视图,**不要只盯着火焰图**:

① 【Flamegraph(火焰图)】——回答「**这一次 commit 里,时间花在哪棵子树上**」
   · 横轴:组件树的嵌套关系(左侧是父、右侧是子)
   · 宽度:该组件「渲染耗时」占本次 commit 的比例
   · 颜色:灰=本次未渲染;蓝→黄→橙:耗时递增

② 【Ranked(排名)】——回答「**哪些组件最贵**」
   · 按「自渲染耗时」从高到低排序(不是按树结构)
   · 可以切换为「按渲染次数排序」
   · ⭐ 优化从这里看最直接:排第一的就是优先优化对象

③ 【Timeline(时间线)】——回答「**哪一次交互触发了哪些渲染**」
   · 横轴是时间,每一次 commit 是一个色块
   · 点某个色块 → 看到那次 commit 的火焰图
   · ⭐ 用来定位「某个操作为什么触发了多次渲染」

④ 【Interactions(交互)】——回答「**这次交互一共花了多久**」(实验性)
   · 把「一次点击导致的多个 commit」聚合成一个「交互」
   · 适合看「用户操作到界面稳定的总时长」
⭐ 火焰图里的三种「条形」要分清:

① 【实心条 + 较宽】
   → 这个组件「渲染了,且耗时显著」(优化重点)

② 【灰条(没有填充)】
   → 这个组件「本次没有渲染」(被 memo 跳过了,或者不在更新路径上)
   → ⭐ 一片灰是好事(说明传播被阻断了)

③ 【细边框 + 内部空白("未渲染但遍历过")】
   → 这个组件「被 React 访问了,但因为它自己的 state/props 没变而跳过」
   → 与「完全没访问」不同(后者连遍历都不会)
   ⚠️ 这个区别很细,但对「理解 memo 的实际效果」有用
⭐⭐⭐ 「Why did this render?」——Profiler 最有价值的功能

   选中一个组件,右侧会显示它「为什么渲染」,常见四类原因:

① 【Props changed】——并列出「具体哪个 prop 变了」
   → 如果那个 prop 是「内联函数/对象」,就是「引用不稳定」的典型症状(见 5.8)
② 【State changed】——自己的 state 变了(合理)
③ 【Parent re-rendered】——⭐ 父组件重渲染带上来的(**最常见的优化点**)
   → 说明需要 memo / children 透传 / 状态下移(见 5.4)
④ 【Context changed】——订阅的 Context 变了(见 5.9)
⑤ 【Hooks changed】——自定义 Hook 的返回值变了

⭐ 用法:录制 → 选中一个「不该渲染的组件」→ 看它为什么渲染 → 对症优化

二、录制技巧:怎么录才有意义

⭐⭐ 五条「录制纪律」(不遵守的话,测出来的全是噪音):

① 【只录目标交互】
   ❌ 录一段「随便点点」的长片段(几百个 commit,无法分析)
   ✅ 明确目标:「输入 10 个字符」「打开 Modal」「切换 Tab」,然后精确录那一下

② 【多轮取中位数】
   ⚠️ 同一次操作,单次测量波动可能 ±30%(GC、JIT、后台任务)
   ✅ 同一交互重复 3~5 次,看「耗时分布」而不是「单次数值」

③ 【注意 dev 构建的失真】
   ⚠️ 开发构建有 StrictMode 双调用、更详细的警告、未压缩代码、
      `useDebugValue` 记录等 → 渲染次数与耗时都显著高于 production
   ✅ 「正确性」在 dev 看(双调用暴露不纯/缺清理),
      「性能数字」在 production build 看

④ 【忽略「极短渲染」】
   ⚠️ 0.05ms 的渲染在火焰图里也会显示,但优化它没有意义
   ✅ 关注「单个组件 > 1ms」或「某组件占本次 commit > 20%」

⑤ 【固定其他变量】
   · 关掉 DevTools 的 "Highlight updates"(它本身有开销)
   · 保持数据量一致(1000 条 vs 100 条,结论完全不同)
   · 关掉其它标签页的重负载(无关的 CPU 竞争会污染结果)
// ✅ 「只录目标交互」的操作顺序(推荐)
// 1) 打开 Profiler → 点「录制」
// 2) 立刻做那一次交互(如输入一个字符)
// 3) 立刻停止录制(不要多做动作)
// 4) 在 Ranked 视图里找最贵的组件
// 5) 选中它 → 看 "Why did this render?"

三、<Profiler onRender>:编程式采集

import { Profiler } from 'react';

<Profiler
  id="ProductList"
  onRender={(id, phase, actualDuration, baseDuration, startTime, commitTime) => {
    // id             → "ProductList"
    // phase          → 'mount' | 'update' | 'nested-update'
    // actualDuration → 本次实际渲染耗时(含子树)
    // baseDuration   → ⭐「没有记忆化时,完全重渲染整棵子树」的估计耗时
    // startTime      → 本次渲染开始
    // commitTime     → 本次提交时间
  }}
>
  <ProductList />
</Profiler>
⭐⭐⭐ `actualDuration` 与 `baseDuration` 的区别(这是本篇最重要的知识点):

   · **`actualDuration`** = 本次渲染「实际」花了多久
     (含子树,但**被 memo/useMemo 跳过的部分不计入**)

   · **`baseDuration`** = 「**假设整棵子树每个组件都重新渲染**」的估计耗时
     (由「子树里每个组件最近一次渲染的耗时」累加得出,**不含记忆化收益**)

   ⭐ 由此得出一个非常有用的判据:

     记忆化收益 ≈ baseDuration / actualDuration

     · 比值接近 1  → 记忆化几乎没起作用(要么没有 memo,要么依赖不稳定)
     · 比值很大(如 10)→ 记忆化省掉了大量工作(说明结构 + memo 生效了)

   ⚠️ `baseDuration` 是「估计值」(它不重新渲染,只是累加缓存的历史耗时),
      所以不要把它当成「真实的重渲染耗时」。

⭐ React 18 起 `onRender` 只有 6 个参数——
   React 17 曾多一个 `interactions` 参数(用于关联「这次渲染由哪个交互触发」),
   后来被移除了。
// ✅ 三个「最小侵入」的使用姿势

// ① 包在「整个页面」外面:看「每次交互的总渲染成本」
<Profiler id="Page" onRender={reportRender}>
  <App />
</Profiler>

// ② 包在「可疑组件」外面:看它自己+子树的耗时
<Profiler id="DataTable" onRender={reportRender}>
  <DataTable rows={rows} />
</Profiler>

// ③ 嵌套多个 Profiler:得到「分层的耗时归因」
<Profiler id="Page" onRender={reportRender}>
  <Header />
  <Profiler id="Sidebar" onRender={reportRender}><Sidebar /></Profiler>
  <Profiler id="Main" onRender={reportRender}><Main /></Profiler>
</Profiler>
// ⭐ 嵌套后能算出「Main 占 Page 的比例」
// ⚠️ 每个 Profiler 都有少量开销,别包太多(生产环境尤其)

四、完整实现:一个 220 行的「渲染性能采集器」

// perf-probe.tsx —— 用 <Profiler> 做「慢渲染采集 + 分组件聚合 + 上报」
import React, { Profiler, useRef, useCallback, useContext, createContext } from 'react';

/** ============ 1. 采集上下文(统一上报出口) ============ */
export interface RenderSample {
  id: string;
  phase: 'mount' | 'update' | 'nested-update';
  actualDuration: number;
  baseDuration: number;
  commitTime: number;
  /** 记忆化收益比(baseDuration / actualDuration) */
  memoRatio: number;
  /** 本次 commit 里同批次上报的其它 Profiler(用于还原「这一次 commit 全貌」) */
  commitId: number;
}

interface PerfOptions {
  /** 超过这个耗时才上报(默认 8ms) */
  thresholdMs?: number;
  /** 上报函数(默认在开发环境打 console) */
  onSample?: (sample: RenderSample) => void;
  /** 是否总是上报 mount(默认 true) */
  reportMounts?: boolean;
}

const PerfContext = createContext<PerfOptions>({});

export function PerfProvider({ children, ...options }: PerfOptions & { children: React.ReactNode }) {
  return <PerfContext.Provider value={options}>{children}</PerfContext.Provider>;
}

/** ============ 2. 组件级探针:<PerfProbe name="X"> ============ */
export function PerfProbe({ name, children }: { name: string; children: React.ReactNode }) {
  const options = useContext(PerfContext);
  const commitIdRef = useRef(0);

  const handleRender = useCallback(
    (
      id: string,
      phase: 'mount' | 'update' | 'nested-update',
      actualDuration: number,
      baseDuration: number,
      _startTime: number,
      commitTime: number
    ) => {
      const threshold = options.thresholdMs ?? 8;
      const shouldReport =
        actualDuration >= threshold || (options.reportMounts !== false && phase === 'mount');
      if (!shouldReport) return;

      const sample: RenderSample = {
        id,
        phase,
        actualDuration: Number(actualDuration.toFixed(3)),
        baseDuration: Number(baseDuration.toFixed(3)),
        commitTime: Number(commitTime.toFixed(1)),
        // ⭐ 记忆化收益:> 1 说明「如果不做记忆化会更慢」
        memoRatio: actualDuration > 0 ? Number((baseDuration / actualDuration).toFixed(2)) : 1,
        commitId: commitIdRef.current++,
      };

      if (options.onSample) options.onSample(sample);
      else if (process.env.NODE_ENV !== 'production') {
        console.warn(
          `[perf] ${sample.id} ${sample.phase}:` +
          `实际 ${sample.actualDuration}ms / 未记忆化估计 ${sample.baseDuration}ms ` +
          `(收益 ${sample.memoRatio}×)`
        );
      }
    },
    [options]
  );

  return (
    <Profiler id={name} onRender={handleRender}>
      {children}
    </Profiler>
  );
}
/** ============ 3. 聚合分析:把「慢渲染」聚合成「优化清单」 ============ */
export interface AggregateItem {
  id: string;
  count: number;             // 渲染次数
  totalMs: number;           // 累计耗时
  maxMs: number;             // 单次最大耗时
  avgMs: number;
  avgMemoRatio: number;      // 平均记忆化收益
  /** 优化建议(基于 memoRatio 自动生成) */
  advice: string;
}

export function aggregateSamples(samples: RenderSample[]): AggregateItem[] {
  const map = new Map<string, RenderSample[]>();
  for (const s of samples) {
    const arr = map.get(s.id) ?? [];
    arr.push(s);
    map.set(s.id, arr);
  }

  const items: AggregateItem[] = [];
  map.forEach((list, id) => {
    const totalMs = list.reduce((sum, s) => sum + s.actualDuration, 0);
    const maxMs = Math.max(...list.map((s) => s.actualDuration));
    const avgMemoRatio = list.reduce((sum, s) => sum + s.memoRatio, 0) / list.length;

    // ⭐ 自动给建议:这是「采集 → 行动」的桥
    let advice = '正常';
    if (avgMemoRatio < 1.2 && list.length > 20) {
      advice = '渲染频繁且「记忆化收益低」→ 检查 props 引用稳定性 / 是否需要 memo';
    } else if (maxMs > 16) {
      advice = '存在超过一帧预算的渲染 → 拆分组件 / useMemo 缓存昂贵计算 / 虚拟滚动';
    } else if (list.length > 100) {
      advice = '渲染次数过多 → 检查是否状态下移(状态放太高会引起大范围重渲染)';
    } else if (avgMemoRatio > 5) {
      advice = '记忆化收益很高 → 说明当前结构是对的,保持';
    }

    items.push({
      id,
      count: list.length,
      totalMs: Number(totalMs.toFixed(2)),
      maxMs: Number(maxMs.toFixed(3)),
      avgMs: Number((totalMs / list.length).toFixed(3)),
      avgMemoRatio: Number(avgMemoRatio.toFixed(2)),
      advice,
    });
  });

  // ⭐ 按「累计耗时」排序(优化优先级 = 累计影响,而不是单次最大)
  return items.sort((a, b) => b.totalMs - a.totalMs);
}
/** ============ 4. 使用示例:把探针接进一个真实页面 ============ */
import { createReporter } from './rum';       // 见 5.11

const reportRum = createReporter('/api/rum', __APP_VERSION__);
const samples: RenderSample[] = [];

export function App() {
  const handleSample = (sample: RenderSample) => {
    samples.push(sample);

    // ⭐ 只上报「明显异常」的样本(避免把线上监控打爆)
    if (sample.actualDuration > 50) {
      reportRum({
        name: 'LCP',                                    // 复用 RUM 的上报通道(示例)
        value: sample.actualDuration,
        target: `${sample.id}:${sample.phase}`,
        breakdown: { baseDuration: sample.baseDuration, memoRatio: sample.memoRatio },
        url: location.pathname,
        release: __APP_VERSION__,
        device: { connection: 'unknown', memory: null, cores: null },
        firstVisit: false,
        ts: Date.now(),
      });
    }

    // 开发期:把聚合结果打出来(每 20 个样本刷新一次)
    if (process.env.NODE_ENV !== 'production' && samples.length % 20 === 0) {
      console.table(aggregateSamples(samples).slice(0, 8));
    }
  };

  return (
    <PerfProvider thresholdMs={8} onSample={handleSample}>
      <PerfProbe name="App">
        <Page />
      </PerfProbe>
    </PerfProvider>
  );
}

function Page() {
  return (
    <>
      <PerfProbe name="Header"><Header /></PerfProbe>
      <PerfProbe name="ProductList"><ProductList /></PerfProbe>
      <PerfProbe name="Recommendations"><Recommendations /></PerfProbe>
    </>
  );
}
⭐⭐ 为什么「累计耗时」比「单次最大」更适合排优先级?

   优化是「投入产出比」的选择:
   · 某组件单次 30ms,但只渲染 1 次(总影响 30ms)→ 收益有限
   · 某组件单次 3ms,但每次输入都渲染(100 次 → 总影响 300ms)→ ⭐ 更值得优化

   → 所以「按 totalMs 排序」通常比「按 maxMs 排序」更能指导优化。
   (当然,「单次超长」如果发生在关键路径上,也要单独处理,比如 > 50ms 会阻塞输入。)
⭐⭐ 三个「自动建议」的规则(把数据变成行动):

① 「渲染次数多 + 记忆化收益低」→ 检查 props 引用稳定性(内联函数/对象)或是否该 memo
② 「单次耗时 > 一帧预算(16ms)」→ 拆分组件 / 缓存昂贵计算 / 虚拟滚动
③ 「渲染次数极多(>100 次)」→ 状态下移(状态放太高导致大范围重渲染)

五、与 Chrome Performance 面板的分工

维度 React DevTools Profiler Chrome Performance 面板
视角 组件级(哪个组件慢) 浏览器级(JS/样式/布局/绘制)
能看到 组件渲染耗时、渲染原因、记忆化收益 长任务、Layout 抖动、绘制、GC、FPS
看不到 DOM/样式/布局的具体成本 「哪个 React 组件」在耗时
适合 定位「哪个组件该优化」 定位「慢在哪一类工作」
时间精度 组件渲染耗时(含子树) 调用栈级(可以细到函数)
⭐⭐ 正确的配合顺序:

① 先用 **Profiler** 找到「哪个组件的渲染耗时占比高」
② 如果那个组件的耗时「看起来不该那么贵」(代码很简单),
   就用 **Performance 面板**看「它渲染时到底在干什么」:
   · 是「Layout 抖动」?(读尺寸 + 改样式交替)
   · 是「样式重算」?(大量 DOM 节点变化)
   · 是「Paint 太重」?(阴影/滤镜/大图)
③ 如果两次都指向「框架代码占比高」→ 检查「渲染次数是否过多」(用 Profiler 的 Ranked 按次数排序)

⭐ 一句话:
   Profiler 回答「**哪个组件慢**」,Performance 回答「**为什么慢**」。

六、本篇特有的坑

// ① 在开发构建里测「性能数字」(StrictMode 双调用 + 未压缩代码 → 数字偏高)
// ✅ 性能看 production build;正确性(不纯渲染/缺清理)看 development

// ② 只录「一大段随便点的操作」
// ✅ 精确录「一次目标交互」,重复多次取中位数

// ③ 只看「火焰图」不看「Ranked」
// ⚠️ 火焰图受嵌套影响(看起来「父组件很宽」可能只是因为包含子树)
// ✅ Ranked 的「自耗时排序」更直接

// ④ 把「灰条」当成「有性能问题」
// ⚠️ 灰色 = 本次未渲染 = 好事(说明传播被阻断了)

// ⑤ 优化「0.05ms 的渲染」
// ✅ 关注 > 1ms 或占本次 commit > 20% 的组件

// ⑥ 忽略 `baseDuration`(不知道记忆化到底有没有用)
// ✅ 用 baseDuration / actualDuration 判断「记忆化收益」

// ⑦ 把 `baseDuration` 当成「真实的重渲染耗时」
// ⚠️ 它是「累加历史耗时」的估计值,不是「重新渲染一遍」的真实耗时

// ⑧ 在生产环境「包一大堆 `<Profiler>`」
// ⚠️ 每个 Profiler 都有开销(且 Profiler 在生产默认仍会计时)
// ✅ 只包关键路径,或通过环境变量按需启用

// ⑨ 用 Profiler 测「非 React 的性能问题」
// ⚠️ 它看不到「布局抖动、DOM 数量、网络」→ 用 Performance 面板

// ⑩ 认为「渲染次数多 = 有性能问题」
// ⚠️ 渲染 ≠ DOM 更新(见 4.6);要看「耗时」
//    「渲染 10 次 × 0.1ms」完全不值得优化

// ⑪ 忽略「Why did this render?」里的「Props changed」细节
//    ⚠️ 它会指出「具体哪个 prop 变了」→ 这是找「引用不稳定」的最快路径

// ⑫ 用「一次录制」的结论做架构决策
//    ⚠️ 单次测量波动可达 ±30% → 必须多次取样

// ⑬ 在「输入框快速输入」时录制(会录到几十个 commit,无法分析)
//    ✅ 录「输入一个字符」这一下

// ⑭ 忘记「DevTools 自身会拖慢页面」
//    ✅ 测量时关掉「Highlight updates」等重功能

// ⑮ 把 Profiler 的结论「直接当优化清单」而不验证
//    ✅ 优化后必须「重新录制」确认改善(见 5.1 的决策流程)
// ⑧ 的完整处理:只在需要时启用 Profiler
const ENABLE_PERF_PROBE = import.meta.env.VITE_ENABLE_PERF === '1';

export function PerfProbe({ name, children }: { name: string; children: React.ReactNode }) {
  if (!ENABLE_PERF_PROBE) return <>{children}</>;      // ⭐ 生产默认关闭
  return <Profiler id={name} onRender={handleRender}>{children}</Profiler>;
}

七、面试延伸

  1. 「你怎么用 Profiler 定位性能问题?」
    分四步:① 只录目标交互(如「输入一个字符」),并重复 3~5 次取中位数(单次波动可达 ±30%);② 在 Ranked 视图里按「耗时」或「渲染次数」排序,找出累计影响最大的组件;③ 选中它看「Why did this render?」——如果是 Parent re-rendered,说明需要 memo/children 透传/状态下移(见 5.4);如果是 Props changed,看是具体哪个 prop(通常是内联函数/对象);④ 优化后重新录制对比。注意「性能数字看 production build」,开发构建有 StrictMode 双调用与额外检查,数字会偏高。

  2. 「火焰图和 Ranked 视图有什么区别?」
    ① 火焰图按「组件树结构」组织(横轴是嵌套关系),宽度表示耗时占本次 commit 的比例——它的优点是「能看出时间花在哪棵子树上」,缺点是「父组件的宽度包含了子树,容易误判」(看起来父组件很宽,其实它自己很便宜);② Ranked 按「自渲染耗时或渲染次数」排序,直接给出「谁最贵」的清单,是优化优先级的直接依据。实践上「先用 Ranked 找目标,再用火焰图看它在树里的位置」。

  3. 「actualDuration 和 baseDuration 有什么区别?」
    actualDuration 是「本次渲染实际耗时」(被 memo 跳过的部分不计入);baseDuration 是「假设整棵子树每个组件都重新渲染」的估计耗时(由子树里每个组件「最近一次渲染的耗时」累加得出)。价值在于比值:baseDuration / actualDuration ≈ 记忆化收益——接近 1 说明「记忆化几乎没起作用」(没有 memo 或依赖不稳定),比值大说明「省掉了大量工作」。注意 baseDuration 是估计值(不重新渲染),不能当「真实重渲染耗时」。

  4. 「Profiler 的『Why did this render?』怎么用?」
    它把渲染原因分成五类:Props changed(并列出具体哪个 prop)、State changed、Parent re-rendered(最常见)、Context changed、Hooks changed。用法是「录制 → 选中一个不该渲染的组件 → 看原因 → 对症优化」:① Parent re-rendered → 用 memo + 稳定 props,或用 children 透传 / 状态下移(结构性手段优先);② Props changed 且是函数/对象 → 引用不稳定(useCallback/useMemo/提到模块外);③ Context changed → 拆分 Context。

  5. 「Profiler 看不到什么?」
    它只看 React 的渲染耗时,看不到:① DOM/样式/布局/绘制的成本(如「布局抖动」「大量节点导致样式重算」——要用 Chrome Performance 面板);② 网络与资源加载(用 Network 面板 / RUM,见 5.11);③ 非 React 的 JS(第三方库的同步计算、长任务);④ 内存与 GC(用 Memory 面板,见 5.13)。所以正确顺序是:Profiler 找「哪个组件慢」→ Performance 面板看「为什么慢」→ 如果两次都指向框架,就查「渲染次数是否过多」。

  6. 「怎么用 <Profiler> 做线上监控?」
    三步:① 用 <Profiler onRender> 包裹关键路径组件(不要包太多,每个都有开销,建议用环境变量控制);② 在 onRender 里只在超过阈值时上报(如 actualDuration > 50,或 phase === 'mount'),并带上「组件 id / phase / actualDuration / baseDuration / 路由 / 版本」;③ 上报后按组件聚合(累计耗时、次数、平均记忆化收益),并按累计耗时排序得到优化清单。注意:线上数据要「分级采样」(差的全报、好的采样),避免把监控打爆。

  7. 「为什么『按累计耗时排序』比『按单次最大耗时排序』更好?」
    因为优化是「投入产出比」的选择:① 某组件单次 30ms 但只渲染 1 次 → 总影响 30ms,优化收益有限;② 某组件单次 3ms 但每次输入都渲染 100 次 → 总影响 300ms,更值得优化。所以「按 totalMs 排序」通常更能指导优化。例外是「单次超长且出现在关键路径上」(如 > 50ms 会阻塞输入、影响 INP)——这类问题即使只发生一次也要单独处理。

  8. 「开发环境和生产环境测出来的数字为什么差别大?」
    因为开发构建包含大量额外工作:① StrictMode 双调用(组件函数执行两次、Effect 也执行两次);② 更详细的警告与检查(如 useEffect 依赖校验、不可变检查);③ 未压缩的代码(体积大、执行慢);④ useDebugValue 的记录;⑤ source map;⑥ DevTools 的 hooks 追踪。所以「性能数字必须在 production build 里测」;而「正确性(不纯的渲染、缺失的清理)要在 development 里测」(因为只有开发环境会双调用暴露这些问题)。

一句话速记

Profiler 的四个视图各有分工:Flamegraph 看「时间花在哪棵子树」、Ranked 看「谁最贵」(优化优先级的直接依据)、Timeline 看「哪次交互触发了哪些 commit」、Interactions 看「一次交互的总时长」;灰条 = 本次未渲染 = 好事;「Why did this render?」把原因分为 Props changed / State changed / Parent re-rendered / Context changed / Hooks changed(Parent re-rendered 最常见 → 用 memo/children/状态下移);actualDuration 是实际耗时(跳过的不计入),baseDuration 是「不做记忆化时的估计」——两者的比值就是「记忆化收益」,接近 1 说明 memo 没起作用;录制要「**只录目标交互 + 多次取中位数 + 忽略 <1ms + 关掉 DevTools 重功能**」;**性能数字看 production build、正确性看 development build**;**Profiler 回答「哪个组件慢」,Chrome Performance 回答「为什么慢」**;线上采集要「**只包关键路径 + 超标才上报 + 按累计耗时排序**」(累计影响 > 单次峰值,除非峰值出现在关键路径)。

赞(0) 打赏
未经允许不得转载:陌上寒 » 5.12 Profiler 深入

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫