前言
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生成