Vue 懒挂载组件设计
Vue 懒挂载组件设计:LazyMountComponent 核心逻辑解析
在长页面或复杂表单里,一次性渲染所有模块,容易带来首屏慢、滚动卡顿的问题。LazyMountComponent 的作用,就是把真实内容的渲染时机延后到“用户快看到它的时候”。
它的核心目标有 3 个:
- 默认先不渲染真实内容
- 进入可视区后再挂载
- 卸载时保留高度,避免页面抖动
一、组件做了什么
模板很简单:
<div :id="compId" ref="containerRef" :style="containerStyle">
<div v-if="isMounted" ref="contentRef">
<slot></slot>
</div>
<div v-else class="placeholder">
<el-skeleton :rows="5" animated />
</div>
</div>
只有两种状态:
isMounted = true:显示真实内容
isMounted = false:显示骨架屏
所以整个组件的关键,就是 控制 isMounted 什么时候切换。
二、核心逻辑
- 用 IntersectionObserver 判断是否进入可视区
const initObserver = () => {
observer = new IntersectionObserver(
(entries) => {
entries.forEach(handleIntersection);
},
{
root: null,
rootMargin: props.rootMargin,
threshold: props.threshold,
}
);
observer.observe(containerRef.value);
};
当组件容器进入视口时,触发挂载逻辑;离开视口时,触发卸载逻辑。
这是整个懒加载机制的入口。
2 . 进入可视区后,不立即挂载,而是延迟挂载
const scheduleMount = (entry: IntersectionObserverEntry) => {
clearMountTimer();
mountTimer = setTimeout(() => {
if (!entry.isIntersecting) return;
mountComponent();
}, MOUNT_DELAY);
};
这里的设计重点是:
不是一进入视口就立刻渲染,而是延迟一段时间再挂载。
这样做的好处是,用户快速滚动时,不会把一闪而过的模块也渲染出来,能减少无效渲染。
3. 真正挂载时,只做必要操作
const mountComponent = () => {
isMounted.value = true;
clearMountTimer();
stopObservingWhenNeeded();
};
这里最核心的是:
1.把 isMounted 改成 true
2.清理定时器
3.如果后续不需要卸载,就停止继续监听可视区
也就是说,这个组件既支持“懒加载一次”,也支持“可见时挂载,不可见时卸载”。
4. 用 ResizeObserver 缓存真实内容高度
const cacheContentHeight = () => {
const currentHeight = contentRef.value?.offsetHeight ?? 0;
if (currentHeight > 0) {
containerMinHeight.value = currentHeight;
}
};
这一步非常关键。因为内容挂载后高度不一定固定,可能会因为表格、异步数据、折叠面板展开而变化。所以组件会持续记录真实内容高度,并把它保存到容器的 min-height。
目的只有一个:后续即使内容被卸载,页面高度也不要突然塌掉。
5. 离开可视区时,可按配置卸载内容
const unmountComponent = () => {
if (!props.unmountWhenInvisible || !isMounted.value) return;
cacheContentHeight();
unobserveContent();
isMounted.value = false;
};
这里的顺序不能乱:
先缓存高度
再取消监听
最后卸载内容如果先卸载,再取高度,就拿不到真实尺寸了,页面容易抖动。
三、总结
这个组件可以概括成一句话:
用 IntersectionObserver 决定什么时候渲染,用 ResizeObserver 记录真实高度,用占位高度保证页面不抖。
如果页面里有很多重模块,比如表格、折叠区、复杂表单,这类组件很适合拿来做首屏和滚动性能优化。
四、技术亮点
- 设计并实现了高性能可视区懒加载组件,支持组件级按需渲染
- 创新引入 1500ms 延迟挂载机制,优化快速滚动场景下的渲染抖动
- 设计了动态高度缓存 + ResizeObserver 联动方案,实现占位高度自适应
- 支持 unmountWhenInvisible 可配置策略,在滚动离开视口后自动卸载 DOM 并回收内存
- 封装了 immediate 即时渲染 + triggerMount 手动控制的双模式接口
五、完整代码
<template>
<div :id="compId" ref="containerRef" class="lazy-container" :style="containerStyle">
<div v-if="isMounted" ref="contentRef">
<slot></slot>
</div>
<div v-else class="placeholder">
<el-skeleton :rows="5" animated />
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
threshold?: number;
rootMargin?: string;
placeholderHeight?: number;
immediate?: boolean;
unmountWhenInvisible?: boolean;
compId: string;
}
const DEFAULT_PLACEHOLDER_HEIGHT = 400;
const MOUNT_DELAY = 1500;
const props = withDefaults(defineProps<Props>(), {
threshold: 0.2,
rootMargin: '0px',
placeholderHeight: 0, //占位符高度
immediate: false, //是否立即挂载组件
unmountWhenInvisible: false, //是否在不可见时卸载组件(当组件滚出可视区后再次卸载组件)
compId: '',
});
const containerRef = ref<HTMLElement>();
const contentRef = ref<HTMLElement>();
const isMounted = ref(props.immediate);
const containerMinHeight = ref(props.placeholderHeight || DEFAULT_PLACEHOLDER_HEIGHT);
const containerStyle = computed(() => ({
minHeight: `${containerMinHeight.value}px`,
}));
let observer: IntersectionObserver | null = null;
let resizeObserver: ResizeObserver | null = null;
let mountTimer: ReturnType<typeof setTimeout> | null = null;
// 清理延迟挂载定时器,避免重复触发或组件销毁后继续执行。
const clearMountTimer = () => {
if (!mountTimer) return;
clearTimeout(mountTimer);
mountTimer = null;
};
// 记录当前内容高度,供骨架屏和卸载后的容器占位复用,避免页面抖动。
const cacheContentHeight = () => {
const currentHeight = contentRef.value?.offsetHeight ?? 0;
if (currentHeight > 0) {
containerMinHeight.value = currentHeight;
}
};
// 当内容挂载后不再需要反复监听可视区时,停止交叉观察。
const stopObservingWhenNeeded = () => {
if (props.unmountWhenInvisible) return;
observer?.disconnect();
};
// 取消对真实内容高度的监听,通常在内容卸载或组件销毁时调用。
const unobserveContent = () => {
if (!resizeObserver || !contentRef.value) return;
resizeObserver.unobserve(contentRef.value);
};
// 在真实内容完成渲染后开始监听高度变化,并立即缓存一次当前高度。
const observeContent = async () => {
if (!resizeObserver || !isMounted.value) return;
await nextTick();
if (!contentRef.value) return;
resizeObserver.observe(contentRef.value);
cacheContentHeight();
};
// 初始化尺寸观察器,确保内容高度变化时同步更新容器占位高度。
const initResizeObserver = () => {
if (resizeObserver) return;
resizeObserver = new ResizeObserver(() => {
if (!isMounted.value) return;
cacheContentHeight();
});
};
// 执行真实内容挂载,并在必要时停止继续监听可视区变化。
const mountComponent = () => {
isMounted.value = true;
clearMountTimer();
stopObservingWhenNeeded();
};
// 在允许“离开视口即卸载”时,先缓存高度再卸载真实内容。
const unmountComponent = () => {
if (!props.unmountWhenInvisible || !isMounted.value) return;
cacheContentHeight();
unobserveContent();
isMounted.value = false;
};
// 进入可视区后延迟挂载,减少快速滚动场景下的无效渲染。
const scheduleMount = (entry: IntersectionObserverEntry) => {
clearMountTimer();
mountTimer = setTimeout(() => {
if (!entry.isIntersecting) return;
mountComponent();
}, MOUNT_DELAY);
};
// 处理进入可视区事件:已挂载则补充监听高度,未挂载则安排延迟挂载。
const handleVisibleEntry = (entry: IntersectionObserverEntry) => {
if (isMounted.value) {
void observeContent();
return;
}
scheduleMount(entry);
};
// 处理离开可视区事件:取消待执行挂载,并按配置决定是否卸载内容。
const handleHiddenEntry = () => {
clearMountTimer();
unmountComponent();
};
// 统一分发交叉观察结果,根据可见状态执行挂载或卸载流程。
const handleIntersection = (entry: IntersectionObserverEntry) => {
if (entry.isIntersecting) {
handleVisibleEntry(entry);
return;
}
handleHiddenEntry();
};
// 初始化交叉观察器,用容器是否进入视口来驱动懒挂载逻辑。
const initObserver = () => {
if (!containerRef.value) return;
observer?.disconnect();
observer = new IntersectionObserver(
(entries) => {
entries.forEach(handleIntersection);
},
{
root: null,
rootMargin: props.rootMargin,
threshold: props.threshold,
}
);
observer.observe(containerRef.value);
};
// 根据挂载状态补充或移除内容高度监听,保持占位高度准确。
watch(isMounted, (mounted) => {
if (mounted) {
void observeContent();
return;
}
unobserveContent();
});
// 组件挂载后初始化观察器;若要求立即渲染,则直接挂载真实内容。
onMounted(() => {
initResizeObserver();
initObserver();
if (props.immediate) {
mountComponent();
}
});
// 组件销毁前清理定时器和各类观察器,避免残留引用。
onUnmounted(() => {
clearMountTimer();
unobserveContent();
observer?.disconnect();
resizeObserver?.disconnect();
});
// 暴露给父组件的手动挂载入口,用于跳过滚动检测直接显示内容。
const triggerMount = () => {
mountComponent();
};
defineExpose({
triggerMount,
});
</script>
更多推荐



所有评论(0)