Element Plus 的 el-tab-pane:lazy 的说明

代码示例:

TypeScript 复制代码
    <template #main>
      <!-- 标签页 -->
      <el-tabs class="capital-options-tabs" v-model="activeName">
        <el-tab-pane v-for="tab in tabs" :key="tab.name" :label="tab.label" :name="tab.name" lazy>
          <div class="tab-content" v-loading="tab.loadingRef?.value">
            <CapitalOptionTable
              :key="tab.name"
              :data="tab.dataRef.value"
              @add="handleAdd"
              @save="handleSave"
              @delete="handleDelete"
              @remove="handleDelete" />
          </div>
        </el-tab-pane>
      </el-tabs>
    </template>

lazy 是 el-tab-pane 的专用 prop

它跟 Vue 3 本身没关系,是 Element Plus 组件库 为 el-tab-pane 定义的一个属性。

分清楚几层概念

名称 归属 含义
lazy(el-tab-pane 的 prop) Element Plus 该 pane 首次激活前不渲染
v-if / v-show Vue 3 核心 条件渲染 / 条件显示
lazy(<Suspense> 相关、路由懒加载等) Vue 3 / Vue Router 另一套概念,跟这里无关

所以这里的 lazy 是组件库 API ,不是 Vue 的语言特性。你换成别的 UI 库(比如 Ant Design Vue 的 a-tab-pane),它可能没有这个 prop,行为也不同。

Vue 3 里 lazy 出现在哪些地方

顺带理一下,避免混淆------Vue 3 生态里叫 lazy 的有好几处,彼此独立:

  1. <Suspense> + 异步组件 :defineAsyncComponent 本身不算 lazy,但常被叫"懒加载组件"

  2. Vue Router :() => import('./Foo.vue') 叫路由懒加载,也没有名为 lazy 的配置项

  3. v-model.lazy :这是修饰符,作用是"change 时才同步,而不是 input 时",跟渲染时机毫无关系

  4. Element Plus el-tab-pane 的 lazy:就是你现在用的这个,控制"要不要延迟渲染"

Element Plus 里 lazy 的等价写法

el-tab-pane 的 lazy 本质是把内部渲染逻辑变成:

vue

复制代码
<!-- 概念示意 -->
<div v-if="!lazy || loaded || active" v-show="active">
  <slot />
</div>

所以它也可以被理解为:"首次激活前用 v-if 挡着" 。如果你哪天想手动控制,完全可以不用 lazy,自己在 pane 里包一层:

vue

复制代码
<el-tab-pane v-for="tab in tabs" :name="tab.name" :label="tab.label">
  <div v-if="visited[tab.name]">
    <CapitalOptionTable ... />
  </div>
</el-tab-pane>

效果类似,只是 Element Plus 帮你把 visited(loaded)这套状态管好了。

小结

  • lazy 属于 Element Plus 的 el-tab-pane,不是 Vue 3 语言能力

  • 想确认一个 prop 是谁的,看它写在哪个标签上、查对应组件文档即可

  • 换库就可能没有这个 prop,但都可以用 v-if 自己实现同样的"延迟渲染"


1. lazy 是什么意思?

el-tab-pane 默认(不写 lazy)时,即使没被激活,也会被渲染到 DOM 中 ,只是用 v-show 隐藏。

加上 lazy 之后,逻辑等价于:

vue

复制代码
<!-- Element Plus 内部大致实现 -->
<div v-if="shouldBeRender" v-show="active">
  <slot />
</div>

其中 shouldBeRender = !lazy || loaded || active。

也就是说:

  • 未激活前 :不渲染(v-if 为 false,DOM 里没有)

  • 第一次激活后 :渲染并标记 loaded = true

  • 切走后 :不再销毁,改用 v-show 隐藏(保留 DOM 和组件状态)

所以 lazy 是"延迟到首次激活才渲染",不是"每次切换都重建"。

2. 可以不 lazy 吗?

可以,去掉就行。但结合你代码里的注释,要权衡:

不 lazy lazy(当前)
首屏渲染 7 个 el-table 全实例化 + 渲染(用 v-show 藏起来) 只渲染当前 1 个
首屏性能 差(el-table 不轻) 好
切换闪烁 少(组件早已存在,列宽已计算) 可能有一次布局抖动(首次挂载)
内存占用 7 份表格实例常驻 用过才常驻

你代码里这段注释:

表格切换闪烁的原因:el-table-column 没有固定宽度,导致切换标签页时表格重新计算列宽产生视觉变化

说明你已经踩到过切换抖动的坑 。lazy 会加剧这个问题------首次切到某个 tab 时组件才 mounted,el-table 要重新测量列宽,闪烁感更明显。

所以这里其实是一个取舍:

  • 想要首屏快、内存省 → 保留 lazy,用"固定列宽 + 占位空列"这套方案压抖动(你已经在做了)

  • 想要切换绝对顺滑、不闪 → 去掉 lazy,但接受首屏渲染 7 个表格的成本

以"资金设置"这种配置页(7 个 tab、每个表也就几十行)来看,去掉 lazy 也是完全可接受的,甚至体验更好。你不妨去掉试试,对比一下切换和首屏表现。

3. v-loading 能直接放在 el-tab-pane 上吗?

能挂,但不建议,实际上会失效或看不见。

原因有三层:

① 指令绑定到组件时,作用在"根元素"上

Vue 3 里给组件加指令,指令的 el 拿到的是组件根元素。而 el-tab-pane 的根元素是条件渲染的:

text

复制代码
<div v-if="shouldBeRender" v-show="active"> ... </div>

lazy 未激活时 v-if 为 false,根本没有真实 DOM,指令初始化时拿到的可能是注释节点,遮罩插不进去。

② 即使挂上了,容器高度可能是 0

v-loading 的遮罩是按容器尺寸撑开的。如果 el-tab-pane 的根元素里没内容(数据还没回来),高度就是 0,遮罩"看不见"。

③ 加上 v-show 时挂载没意义

未激活的 pane 是 display: none,loading 遮罩自然也看不见。

所以正确做法:包一层容器

vue

复制代码
<el-tab-pane
  v-for="tab in tabs"
  :key="tab.name"
  :label="tab.label"
  :name="tab.name"
  lazy>
  <div class="tab-content" v-loading="tab.loadingRef.value">
    <CapitalOptionTable ... />
  </div>
</el-tab-pane>

scss

复制代码
.tab-content {
  height: 100%;
  min-height: 200px; // 关键:保证空数据时也有高度,遮罩才可见
}

这样 v-loading 作用在一个稳定存在的 div 上,且这个 div 有明确高度,遮罩才能正常工作。

小结

  • lazy = 首次激活才渲染,之后用 v-show 保留

  • 去掉 lazy 可行,但权衡首屏性能 vs 切换顺滑度;你的"固定列宽"方案已经缓解了抖动,去掉 lazy 会进一步变好

  • v-loading 别直接挂 el-tab-pane:组件根元素条件渲染 + 高度不定,会导致指令失效或遮罩不可见;包一层有高度的 div 是最稳妥的写法


这里加 lazy 的必要性

TypeScript 复制代码
<script setup lang="ts">
/**
 * 进度监控-选项卡
 */
defineOptions({
  name: "progress-ctrl-tabs"
});

import type { TabsPaneContext } from "element-plus";
import {
  ProgressCtrlTabsApply,
  ProgressCtrlTabsAssay,
  ProgressCtrlTabsHandover,
  ProgressCtrlTabsReport
} from "../components";
import { useProgressCtrlStore } from "../stores";

// ========== 状态管理(Store) ==========
const progressCtrlStore = useProgressCtrlStore();

// ========== 事件处理 ==========
/**
 * 切换 tab 页
 * @param tab tab 页
 * @param event 事件
 */
const handleTabClick = async (tab: TabsPaneContext, event: Event) => {
  progressCtrlStore.activeTabName = tab.paneName as typeof progressCtrlStore.activeTabName;
};
</script>

<template>
  <div class="tabs">
    <el-tabs :model-value="progressCtrlStore.activeTabName" type="border-card" @tab-click="handleTabClick">
      <el-tab-pane label="受理信息" name="apply">
        <ProgressCtrlTabsApply />
      </el-tab-pane>
      <el-tab-pane label="交接信息" name="handover">
        <ProgressCtrlTabsHandover />
      </el-tab-pane>
      <el-tab-pane label="检测信息" name="assay">
        <ProgressCtrlTabsAssay />
      </el-tab-pane>
      <el-tab-pane label="报告信息" name="report">
        <ProgressCtrlTabsReport />
      </el-tab-pane>
    </el-tabs>
  </div>
</template>

<style scoped lang="scss">
.tabs {
  .el-tabs {
    border: none;
    // 高度需要减去上下边框宽度
    // height: calc(100% - 2px);
    // 使用 box-sizing: border-box; 就不用考虑边框宽度
    box-sizing: border-box;
    height: 528px;
    :deep(.el-tabs__content) {
      margin: 0;
      padding: 0;
    }

    .el-tab-pane {
      height: 500px;
    }
  }
}
</style>

需要加,而且收益比之前那个页面更大

为什么这里更值得加

对比一下两个页面的差异:

资金设置页 这个进度监控页
每个 tab 内容 一个 el-table 四个业务子组件(Apply / Handover / Assay / Report)
子组件复杂度 中等 通常更重(表单、明细、表格、可能还有各自请求)
是否固定高度 否 是(528px / 500px)
未激活 tab 是否可见 否 否

不加 lazy 的后果 :4 个子组件会在 tabs 挂载时全部实例化并渲染 ,哪怕用户只看了第一个。这些子组件的 onMounted、内部 watch、immediate 逻辑会一起触发------如果 ProgressCtrlTabsHandover 等在 onMounted 里发请求,就会一次性打 4 个接口。

所以这里加 lazy 的收益(首屏性能、减少无谓请求)比资金设置页更明显。

怎么加

直接加 lazy 即可,行为和之前讲的一样:首次激活才渲染,之后用 v-show 保留。

vue

复制代码
<el-tab-pane label="受理信息" name="apply" lazy>
  <ProgressCtrlTabsApply />
</el-tab-pane>
<el-tab-pane label="交接信息" name="handover" lazy>
  <ProgressCtrlTabsHandover />
</el-tab-pane>
<!-- ...其余同理 -->

需要注意的点

1. 首次切换会有一次渲染成本

因为 lazy 是"延迟到首次激活才挂载",用户第一次点"交接信息"时,ProgressCtrlTabsHandover 才 mounted。如果这个组件内部在 onMounted 里请求数据、或渲染较重,会有短暂空白/卡顿。

建议 :配合 loading 或骨架屏。比如子组件自己管 loading,或者在切换时给内容区一个占位。这跟前面资金设置页加的 v-loading 是同一思路。

2. 这里用 :model-value + @tab-click,不是 v-model

vue

复制代码
<el-tabs :model-value="progressCtrlStore.activeTabName" @tab-click="handleTabClick">

这是"受控"写法------activeName 存在 store 里,点击时通过 handleTabClick 写回 store,再由 :model-value 驱动视图。

这个写法能工作 ,但有一处要留意:@tab-click 只在用户点击标签时触发,不会 在代码里直接改 progressCtrlStore.activeTabName 时同步(其实会------因为 :model-value 绑的就是它)。所以受控本身没问题。

不过更推荐直接用 v-model,它内部会自动处理 update:modelValue:

vue

复制代码
<el-tabs v-model="progressCtrlStore.activeTabName" ...>

这样就不用写 handleTabClick 了,TabsPaneContext 的导入也能删掉。当然如果 activeTabName 的类型需要收窄(你用了 as typeof ...),或者有额外的埋点/日志逻辑,那就保留 @tab-click。

补充:lazy 只影响首次渲染时机,与"受控 / 非受控"写法无关,两种情况都能加。

3. 固定高度下,未渲染的 pane 不影响布局

你给 .el-tab-pane 设了 height: 500px。加了 lazy 后,未激活的 pane 里没有子组件 DOM,但 pane 本身仍在(el-tabs__content 里),高度由内容决定------空的 pane 高度可能是 0。不过这没关系,因为同一时间只有一个 pane 可见。

如果你担心初次切换时高度跳变,可以给 .el-tab-pane 保留 min-height,或让子组件自己撑满。

结论

加 lazy,尤其这里 4 个子组件都可能是重组件。配合:

  • 子组件内部的 loading / 骨架屏,缓解首次切换的空白感

  • 顺手考虑把 :model-value + @tab-click 简化为 v-model(非必须)

相关推荐
卸任19 小时前
Vibe coding工具使用心得
前端·ai编程
brien920 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance20 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M20 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写20 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李20 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易20 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding21 小时前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马21 小时前
前沿视界
前端
kyriewen21 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员