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>

lazyel-tab-pane 的专用 prop

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

分清楚几层概念

名称 归属 含义
lazyel-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-panelazy:就是你现在用的这个,控制"要不要延迟渲染"

Element Plus 里 lazy 的等价写法

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

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 帮你把 visitedloaded)这套状态管好了。

小结

  • 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 时组件才 mountedel-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、内部 watchimmediate 逻辑会一起触发------如果 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 是"延迟到首次激活才挂载",用户第一次点"交接信息"时,ProgressCtrlTabsHandovermounted。如果这个组件内部在 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(非必须)

相关推荐
爱勇宝1 小时前
《道德经》第 11 章:真正有用的,常常是你没写出来的那部分
前端·后端·产品
jearry1 小时前
WebView2 原生拖放的桥接之道:拆解 yyzTools 的 drop-zone.js
前端·c++
一位正在转型AI全栈的前端工程师1 小时前
AI 全栈学习之旅 -Week 11:从 CLI 到浏览器:用 FastAPI、SSE 和 Vue 3 做一个可审核的 ReAct Agent
前端·python
10share1 小时前
我为什么用 React 重写了一个 VitePress
前端·react.js
计算机魔术师1 小时前
GPT-6 Astra 发布后,Sebastian Raschka 解析 looped transformer 与隐藏推理链传闻
前端
不可能片场1 小时前
Electron 退化成 node:一个环境变量的锅
前端·electron
楚楚河河1 小时前
js 变量声明
前端
Cicada1281 小时前
Web 服务器怎么选
运维·服务器·前端
不可能片场1 小时前
接口返回 200 不代表活着:catch-all 路由的陷阱
前端·electron