性能指标
交互到下一次绘制 Interaction to Next Paint (INP)
定义:交互到下一次绘制(Interaction to Next Paint),衡量用户与页面交互后,浏览器响应并渲染下一个帧的时间。
关键点:
- 测量范围:从用户触发交互事件开始,到浏览器完成下一次绘制为止
- 目标值:小于200毫秒为优秀,200-500毫秒为可接受,大于500毫秒为需要优化
- 影响因素:JavaScript执行时间、CSS计算、布局重排、绘制等
优化建议:
- 减少长时间运行的JavaScript任务
- 优化CSS选择器和样式计算
- 避免布局重排和重绘
- 使用
requestIdleCallback处理非紧急任务 - 增加Web Workers分担主线程压力
首次内容绘制 FCP (First Contentful Paint)
- 定义:首次内容绘制,衡量页面首次渲染任何文本、图片或非空白内容的时间
- 目标值:小于1.8秒为优秀,1.8-3秒为可接受,大于3秒为需要优化
首次与页面交互时的响应时间 FID (First Input Delay)
- 定义:首次输入延迟,衡量用户首次与页面交互时的响应时间
- 目标值:小于100毫秒为优秀,100-300毫秒为可接受,大于300毫秒为需要优化
页面主要内容可见的时间 Largest Contentful Paint (LCP)
- 定义:最大内容绘制,衡量页面主要内容可见的时间
- 目标值:小于2.5秒为优秀,2.5-4秒为可接受,大于4秒为需要优化
布局偏移 Cumulative Layout Shift (CLS)
主要布局偏移问题
表格高度不固定
分页组件高度变化
骨架屏到表格的切换
在表格加载过程中,由于表格的高度不确定,可能会导致布局偏移(CLS)
分页组件在数据加载前后也会因为总条数的不确定而出现高度变化,导致布局偏移
为了减少布局偏移,我们可以采取以下措施:
- 为表格容器设置一个最小高度,这样在加载数据时,表格区域的高度不会从0变成有数据的高度,从而减少布局偏移。
- 为分页组件设置一个固定高度,并在数据加载前就预留出空间,这样分页组件的位置不会因为数据加载而移动。
另外,我们还可以考虑使用骨架屏来完全模拟表格的布局,这样在加载过程中,骨架屏的高度和表格高度一致,就不会有布局偏移。
由于我们表格每页显示10条数据,我们可以让骨架屏也生成10行,每行的高度与表格行高相同。
修改骨架屏的代码,使其有10行,每行高度与表格行高相同(55px左右),并且每行之间有间距。
表格相关
改进表格容器样式
css
.table-container {
min-height: 400px; /* 更合理的最小高度 */
position: relative;
}
/* 确保骨架屏和表格高度一致 */
.skeleton-placeholder {
min-height: 400px;
}
固定表格容器高度
表格列宽优化
- 为所有列设置了固定宽度
- 为所有列设置了固定宽度:80+120+130+110+110+150+120+220 = 1040px
- 使用
min-width: 1040px确保表格最小宽度
- 使用
table-layout="fixed"确保布局稳定 - 强制表头和表体使用相同的列宽计算方式
css
::v-deep .el-table__header,
::v-deep .el-table__body {
width: 100% !important;
table-layout: fixed !important;
}
预计算表格高度
js
// 在组件挂载时计算并设置表格高度
mounted() {
this.calculateTableHeight()
window.addEventListener('resize', this.calculateTableHeight)
},
beforeDestroy() {
window.removeEventListener('resize', this.calculateTableHeight)
},
methods: {
calculateTableHeight() {
// 根据窗口高度动态计算表格容器高度
const windowHeight = window.innerHeight
const tableContainer = document.querySelector('.table-container')
if (tableContainer) {
const rect = tableContainer.getBoundingClientRect()
const newHeight = windowHeight - rect.top - 120 // 减去表头、分页等高度
tableContainer.style.height = `${Math.max(newHeight, 400)}px`
}
}
}
分页容器相关
1. 固定分页容器尺寸
css
.pagination-container {
height: 60px; /* 固定高度 */
min-width: 400px; /* 预留足够宽度 */
display: flex;
align-items: center;
justify-content: flex-end;
position: relative;
}
2. 预分配分页组件空间
css
/* 确保分页组件有稳定的容器 */
.el-pagination {
min-width: 350px; /* 预留足够空间 */
text-align: center;
}
/* 防止按钮数量变化导致的宽度变化 */
.el-pagination .btn-prev,
.el-pagination .btn-next,
.el-pagination .number {
width: 32px;
min-width: 32px;
}
3. 优化分页数据加载
javascript
// 在获取员工列表时预计算分页宽度
async getEmployeeList() {
this.tableLoading = true
// 保持分页组件可见,避免重新渲染时的布局偏移
await this.$nextTick()
try {
const [err, data] = await to(getEmployeeList(this.queryParams))
if (err) {
this.$message.error(err.message || '获取员工列表失败')
return
}
this.employeeList = data.rows
this.total = data.total
// 数据加载后确保分页布局稳定
await this.$nextTick()
} finally {
this.tableLoading = false
}
}
4. 添加过渡效果
css
/* 分页组件过渡效果 */
.pagination-container {
transition: all 0.2s ease-in-out;
}
.el-pagination {
transition: opacity 0.2s ease;
}
/* 防止突然的布局变化 */
.el-pagination * {
box-sizing: border-box;
}
5. 预渲染分页占位符
vue
<template>
<el-row class="pagination-container" type="flex" justify="end">
<!-- 在加载时显示占位符,保持布局稳定 -->
<div v-if="tableLoading" class="pagination-placeholder">
<span class="total-placeholder">共 100 条</span>
<div class="pager-placeholder">
<span v-for="i in 5" :key="i" class="page-btn-placeholder"></span>
</div>
</div>
<el-pagination
v-else
background
layout="total, prev, pager, next"
:total="total"
:current-page="queryParams.page"
@current-change="handleCurrentChange"
/>
</el-row>
</template>
<style>
.pagination-placeholder {
display: flex;
align-items: center;
gap: 10px;
min-width: 350px;
justify-content: flex-end;
}
.total-placeholder {
color: #909399;
font-size: 13px;
}
.pager-placeholder {
display: flex;
gap: 4px;
}
.page-btn-placeholder {
width: 32px;
height: 32px;
background: #f4f4f5;
border-radius: 4px;
}
</style>
6. 优化分页组件配置
vue
<el-pagination
background
:pager-count="5" <!-- 固定显示的页码按钮数量 -->
layout="total, prev, pager, next"
:total="total"
:current-page="queryParams.page"
:page-size="queryParams.pagesize"
@current-change="handleCurrentChange"
/>
7.固定分页组件位置
css
.pagination-container {
height: 60px;
min-height: 60px;
display: flex;
align-items: center;
justify-content: flex-end;
position: sticky;
bottom: 0;
background: white;
z-index: 10;
border-top: 1px solid #ebeef5; /* 添加分割线 */
margin-top: 0 !important; /* 确保没有外边距影响 */
}
预分配空间
css
/* 为动态内容预分配空间 */
.el-table {
min-height: 400px;
}
/* 确保头像列宽度固定 */
.el-table__header-wrapper th:first-child,
.el-table__body-wrapper td:first-child {
width: 80px !important;
min-width: 80px;
}
优化数据加载逻辑
javascript
// 在获取数据前先设置加载状态
async getEmployeeList() {
this.tableLoading = true
// 强制重绘以确保骨架屏显示
await this.$nextTick()
try {
const [err, data] = await to(getEmployeeList(this.queryParams))
if (err) {
this.$message.error(err.message || '获取员工列表失败')
return
}
this.employeeList = data.rows
this.total = data.total
} finally {
// 添加短暂延迟确保平滑过渡
setTimeout(() => {
this.tableLoading = false
}, 100)
}
}
添加过渡效果
vue
<template>
<transition name="fade" mode="out-in">
<div v-if="tableLoading" key="skeleton" class="skeleton-placeholder">
<!-- 骨架屏内容 -->
</div>
<el-table v-else key="table" :data="employeeList">
<!-- 表格内容 -->
</el-table>
</transition>
</template>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>