优化网页性能指标:提升用户体验的关键

性能指标

交互到下一次绘制 Interaction to Next Paint (INP)

定义:交互到下一次绘制(Interaction to Next Paint),衡量用户与页面交互后,浏览器响应并渲染下一个帧的时间。

关键点:

  • 测量范围:从用户触发交互事件开始,到浏览器完成下一次绘制为止
  • 目标值:小于200毫秒为优秀,200-500毫秒为可接受,大于500毫秒为需要优化
  • 影响因素:JavaScript执行时间、CSS计算、布局重排、绘制等

优化建议:

  1. 减少长时间运行的JavaScript任务
  2. 优化CSS选择器和样式计算
  3. 避免布局重排和重绘
  4. 使用requestIdleCallback处理非紧急任务
  5. 增加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)

分页组件在数据加载前后也会因为总条数的不确定而出现高度变化,导致布局偏移

为了减少布局偏移,我们可以采取以下措施:

  1. 为表格容器设置一个最小高度,这样在加载数据时,表格区域的高度不会从0变成有数据的高度,从而减少布局偏移。
  2. 为分页组件设置一个固定高度,并在数据加载前就预留出空间,这样分页组件的位置不会因为数据加载而移动。

另外,我们还可以考虑使用骨架屏来完全模拟表格的布局,这样在加载过程中,骨架屏的高度和表格高度一致,就不会有布局偏移。

由于我们表格每页显示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>
相关推荐
卸任4 小时前
Vibe coding工具使用心得
前端·ai编程
brien95 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance5 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M6 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写6 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李6 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易6 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马6 小时前
前沿视界
前端
kyriewen6 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易6 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员