vue3的组件优化

前言

vue的组件优化大家可能不太关注,但是当一个组件的节点过多,更新一个数据的话就可能导致整个组件render,这是不必要的性能浪费,接下来就带大家直击现场

一、组件性能损耗?

全量patch

这里我写了一个案例,来直观感受一下render的消耗时长

javascript 复制代码
<template>
  <div style="padding: 20px">
    <h2>10,000 字段全量更新测试</h2>
    <p>请输入内容并观察控制台:</p>

    <div style="margin-bottom: 20px; color: red; font-weight: bold">
      检测到数据变更,正在触发全量 Diff...
    </div>

    <div v-for="key in keys" :key="key" style="margin-bottom: 5px">
      <label>{{ key }}: </label>
      <input
        :value="formData[key]"
        @input="(e) => onFieldInput(key, e.target.value)"
      />
    </div>
  </div>
</template>

<script setup>
import { reactive, onUpdated } from "vue";

// 1. 生成 10,000 个字段
const keys = Array.from({ length: 10000 }, (_, i) => `field_${i}`);
const formData = reactive(Object.fromEntries(keys.map((k) => [k, ""])));

// 使用普通变量,不要用 ref,避免触发额外的响应式追踪
let renderStartTime = 0;
let isBenchmarking = false;

const onFieldInput = (key, value) => {
  if (!isBenchmarking) {
    // 记录开始时间:数据变化前的一瞬间
    renderStartTime = performance.now();
    isBenchmarking = true;
    console.log("--- 渲染开始 ---");
  }

  // 改变数据,触发父组件重新渲染
  formData[key] = value;
};

onUpdated(() => {
  if (isBenchmarking) {
    const renderEndTime = performance.now();
    const duration = (renderEndTime - renderStartTime).toFixed(2);

    console.log(
      `%c[Result] 10,000个节点全量 Patch 耗时: ${duration}ms`,
      "color: white; background: red; padding: 4px 8px; font-weight: bold;",
    );

    // 渲染完成后重置锁
    isBenchmarking = false;
    renderStartTime = 0;
  }
});
</script>

我在同一个组件内渲染1w个input标签,给每一个input v-model一个数据

此时当我们修改任意一个input看看结果

我们发现父组件进行了全量patch,耗时大概十几毫秒

组件拆分

接下来,我们将这个input组件拆分成子组件

javascript 复制代码
<template>
  <div style="padding: 20px">
    <h2>10,000 字段【原子化优化】实验</h2>
    <p>请输入内容,你会发现父组件<b>完全不触发</b>更新:</p>

    <div style="margin-bottom: 20px; color: green; font-weight: bold">
      父组件状态:静默(不参与渲染比对)
    </div>

    <div v-for="key in keys" :key="key" style="margin-bottom: 5px">
      <AtomicInput :name="key" />
    </div>
  </div>
</template>

<script setup>
import { reactive, provide, onUpdated, ref } from "vue";
import AtomicInput from "./AtomicInput.vue"; // 引入下面的子组件
const a = ref(0);
const keys = Array.from({ length: 10000 }, (_, i) => `field_${i}`);
const formData = reactive(Object.fromEntries(keys.map((k) => [k, ""])));

// 1. 提供数据源和更新方法
provide("formContext", {
  state: formData,
  update: (key, val) => {
    formData[key] = val;
    handleChange();
  },
});
const handleChange = () => {
  console.log(formData);
};
// 2. 这里的 onUpdated 在输入时绝对不会触发
onUpdated(() => {
  console.log("%c[Warning] 父组件触发了重绘!", "color: orange");
});
</script>

AtomicInput .vue

javascript 复制代码
<template>
  <div class="field-item">
    <label>{{ name }}: </label>
    <input :value="localValue" @input="onInput" />
    <span v-if="renderTime > 0" style="color: blue; margin-left: 10px">
      耗时: {{ renderTime }}ms
    </span>
  </div>
</template>

<script setup>
import { inject, computed, ref, onUpdated } from "vue";

const props = defineProps(["name"]);
const { state, update } = inject("formContext");

const localValue = computed(() => state[props.name]);
const renderTime = ref(0);
let startTime = 0;

const onInput = (e) => {
  startTime = performance.now();
  update(props.name, e.target.value);
};

onUpdated(() => {
  if (startTime > 0) {
    renderTime.value = (performance.now() - startTime).toFixed(2);
    console.log(
      `%c[Atomic Result] 字段 ${props.name} 更新耗时: ${renderTime.value}ms`,
      "color: green",
    );
    startTime = 0;
  }
});
</script>

此时我们发现patch耗时仅仅只有几毫秒,且父组件没有触发onUpdated

二、原因

为什么会差距这么大呢

其实很简单:vue的更新是组件级别的更新,父组件并没有直接使用formData这个响应式数据,那么formData的单个数据变化不会影响这个父组件;
只要父组件使用到了这个formData,那么就会触发父组件的render

这句话什么意思呢?

我们看下面的代码

javascript 复制代码
<template>
  <div style="padding: 20px">
    <h2>10,000 字段【原子化优化】实验</h2>
    <p>请输入内容,你会发现父组件<b>完全不触发</b>更新:</p>

    <div style="margin-bottom: 20px; color: green; font-weight: bold">
      父组件状态:静默(不参与渲染比对)
    </div>
    {{ formData }}
    <div v-for="key in keys" :key="key" style="margin-bottom: 5px">
      <AtomicInput :name="key" />
    </div>
  </div>
</template>

<script setup>
import { reactive, provide, onUpdated, ref } from "vue";
import AtomicInput from "./AtomicInput.vue"; // 引入下面的子组件
const a = ref(0);
const keys = Array.from({ length: 10000 }, (_, i) => `field_${i}`);
const formData = reactive(Object.fromEntries(keys.map((k) => [k, ""])));

// 1. 提供数据源和更新方法
provide("formContext", {
  state: formData,
  update: (key, val) => {
    formData[key] = val;
    handleChange();
  },
});
const handleChange = () => {
  console.log(formData);
};
// 2. 这里的 onUpdated 在输入时绝对不会触发
onUpdated(() => {
  console.log("%c[Warning] 父组件触发了重绘!", "color: orange");
});
</script>

与上面的唯一区别就是父组件渲染了formData

此时我们输入内容

就会发现,父组件发生了render,耗时也变长了

javascript 复制代码
<template>
  <div style="padding: 20px">
    <h2>10,000 字段【原子化优化】实验</h2>
    <p>请输入内容,你会发现父组件<b>完全不触发</b>更新:</p>

    <div style="margin-bottom: 20px; color: green; font-weight: bold">
      父组件状态:静默(不参与渲染比对)
    </div>
    <!-- {{ formData }} -->
    <div v-for="key in keys" :key="key" style="margin-bottom: 5px">
      <AtomicInput :name="key" :value="formData[key]" />
    </div>
  </div>
</template>

<script setup>
import { reactive, provide, onUpdated, ref } from "vue";
import AtomicInput from "./AtomicInput.vue"; // 引入下面的子组件
const a = ref(0);
const keys = Array.from({ length: 10000 }, (_, i) => `field_${i}`);
const formData = reactive(Object.fromEntries(keys.map((k) => [k, ""])));

// 1. 提供数据源和更新方法
provide("formContext", {
  state: formData,
  update: (key, val) => {
    formData[key] = val;
    handleChange();
  },
});
const handleChange = () => {
  console.log(formData);
};
// 2. 这里的 onUpdated 在输入时绝对不会触发
onUpdated(() => {
  console.log("%c[Warning] 父组件触发了重绘!", "color: orange");
});
</script>

当把formData的某个值传给子组件呢

也触发了父组件的render,所以这就是上面的代码为什么用provide这种方式,就是为了避免在父组件的template中使用到这个响应式数据

总结

当然,也能看得出来其实哪怕数据量大,其实渲染时长也并不算特别大,但是如果单个组件过于复杂的话,可能会导致渲染卡顿

tips:以上代码由ai生成

相关推荐
卸任7 小时前
Vibe coding工具使用心得
前端·ai编程
brien98 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance8 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M8 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写9 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李9 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易9 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马9 小时前
前沿视界
前端
kyriewen9 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易9 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员