优化网页性能指标:提升用户体验的关键
性能指标
交互到下一次绘制 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左右),并且每行之间有间距。
表格相关
改进表格容器样式
.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"确保布局稳定 - 强制表头和表体使用相同的列宽计算方式
::v-deep .el-table__header,
::v-deep .el-table__body {
width: 100% !important;
table-layout: fixed !important;
}
预计算表格高度
// 在组件挂载时计算并设置表格高度
mounted() {this.calculateTableHeight()window.addEventListener('resize', this.calculateTableHeight)
},
beforeDestroy() {window.removeEventListener('resize', this.calculateTableHeight)
},
methods: {calculateTableHeight() {// 根据窗口高度动态计算表格容器高度const windowHeight = window.innerHeightconst 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. 固定分页容器尺寸
.pagination-container {height: 60px; /* 固定高度 */min-width: 400px; /* 预留足够宽度 */display: flex;align-items: center;justify-content: flex-end;position: relative;
}
2. 预分配分页组件空间
/* 确保分页组件有稳定的容器 */
.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. 优化分页数据加载
// 在获取员工列表时预计算分页宽度
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.rowsthis.total = data.total// 数据加载后确保分页布局稳定await this.$nextTick()} finally {this.tableLoading = false}
}
4. 添加过渡效果
/* 分页组件过渡效果 */
.pagination-container {transition: all 0.2s ease-in-out;
}.el-pagination {transition: opacity 0.2s ease;
}/* 防止突然的布局变化 */
.el-pagination * {box-sizing: border-box;
}
5. 预渲染分页占位符
<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-paginationv-elsebackgroundlayout="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. 优化分页组件配置
<el-paginationbackground:pager-count="5" <!-- 固定显示的页码按钮数量 -->layout="total, prev, pager, next":total="total":current-page="queryParams.page":page-size="queryParams.pagesize"@current-change="handleCurrentChange"
/>
7.固定分页组件位置
.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; /* 确保没有外边距影响 */
}
预分配空间
/* 为动态内容预分配空间 */
.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;
}
优化数据加载逻辑
// 在获取数据前先设置加载状态
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.rowsthis.total = data.total} finally {// 添加短暂延迟确保平滑过渡setTimeout(() => {this.tableLoading = false}, 100)}
}
添加过渡效果
<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>
