Skip to content

前端性能优化专题

聚合 framework / browser / engineering / css 四处散点,按「渲染层 → 网络层 → 构建层 → 框架层 → CSS 层」组织


一、核心度量指标(Core Web Vitals)

指标全称含义优秀标准
FPFirst Paint首次绘制像素时间
FCPFirst Contentful Paint首次内容绘制< 2s
LCPLargest Contentful Paint最大内容绘制< 2.5s
FIDFirst Input Delay首次用户交互响应延迟< 100ms
CLSCumulative Layout Shift累积布局偏移< 0.1
TTITime to Interactive完全可交互时间

长任务:执行时间超过 50ms 的任务。用户交互响应推荐在 100ms 内


二、渲染层优化

源头:浏览器 · 进程与渲染

1. 关键渲染路径(CRP)

  1. 解析 HTML 构建 DOM 树
  2. 解析 CSS 生成 CSSOM
  3. 合并 DOM + CSSOM → Render 树
  4. 布局(Layout/reflow)→ 绘制(paint)→ 合成(composite)

优化策略

  • 优化 DOM:删除不必要代码/注释/空格,GZIP 压缩
  • 优化 CSSOM:减少关键 CSS 体积,非关键 CSS 标记为 media 查询
  • 优化 JS:消除阻塞渲染的 JS(async/defer),将非关键 JS 延迟执行
  • 优化关键字节:缩小压缩文件减少下载时间

2. 复合层与硬件加速

  • 开启硬件加速transform: translate3d()will-change)可将节点提升为独立复合图层
  • 复合图层间绘制互不干扰,由 GPU 直接控制
  • 仅触发合成的 CSS 属性(不回流不重绘):只有 transformopacity

内存占用:每个复合层需缓存图像数据(宽 × 高 × 4 字节),不可滥用

动画优化技巧

  1. 避免隐式合成:保持动画对象 z-index 尽可能高
  2. 动画中只使用 transformopacity
  3. will-change 慎用,滥用会大幅增加内存

3. 虚拟滚动

  • 原理:只渲染可视区域的列表项,滚动时动态更新 startIndex/endIndex
  • 适用:大量数据列表(1000+ 条),性能提升可达 78%
  • 插件vue-virtual-scroller(固定行高 RecycleScroller / 动态行高 DynamicScroller)

4. Web Worker 优化长任务

js
// worker.js
onmessage = function(e) {
  let sum = e.data
  for (let i = 0; i < 200000; i++) { sum += Math.random() }
  postMessage(sum)
}
// 主线程
const worker = new Worker('worker.js')
worker.postMessage(0)
worker.onmessage = (e) => console.log(e.data)

使用前提:运算时长 - 通信时长 > 50ms 才值得使用

5. 骨架屏

  • 白屏原因:CSR 项目等待 JS 加载/CSSOM 构建期间的灰白屏
  • 对比 SSR 和预渲染:骨架屏成本最低,适合所有 CSR 页面
  • 实现:手动编写 / killblanks 插件(构建时自动生成)/ 封装 <skeleton> 组件

三、网络层优化

源头:浏览器 · 网络协议与缓存

1. gzip 压缩

  • 压缩率通常可达 70%(1.4MB → 573KB)
  • Nginxgzip on; gzip_types text/css application/javascript application/json;
  • Node(Express)app.use(compression())
  • Webpack 预压缩CompressionWebpackPlugin(threshold: 10KB,ratio: 0.8)

2. CDN 优化(externals)

将大型第三方库(vue/element-ui/axios)通过 CDN 引入,webpack 构建时用 externals 排除打包:

js
externals: {
  'vue': 'Vue',
  'element-ui': 'ELEMENT',
  'axios': 'axios',
}

效果:vendor.js 从 959KB → 12KB,加载时间 3s → 1s

3. 前端缓存策略(Vue + Nginx)

资源类型缓存策略原因
index.html不缓存(Cache-Control: no-store入口文件,每次获取最新版本引用
JS/CSS/图片强制缓存(30天)文件名含 contenthash,内容变则文件名变

4. 预加载策略(Preload / Prefetch)

  • preload:告诉浏览器当前页面马上要用到的文件,高优先级下载
  • prefetch:告诉浏览器空闲时提前下载用户接下来可能用到的文件
  • 策略:preload chunk-vendors + 入口 chunk;prefetch 常用 async chunks
  • 注意:preload 要放在 prefetch 之前;prefetch 不应滥用(占用带宽)
  • 如果 <script>crossorigin 属性,prefetch/preload 也必须有,否则资源会重复下载

5. JS 六种加载方式对比

方式阻塞 DOM执行时机有序性适用场景
普通 <script>阻塞立即按顺序必须同步的核心脚本
async不阻塞加载完立即执行无序埋点统计等无依赖脚本
defer不阻塞DOMContentLoaded 前按顺序需控制执行顺序的脚本
type="module"不阻塞同 defer按 import 顺序Vite 开发模式
preload不阻塞预加载(不执行)当前页关键资源
prefetch不阻塞空闲时加载其他页面所需资源

四、构建层优化

源头:工程化 · Webpack 原理

1. 分片优化(splitChunks 实战)

分片原则

  1. node_modules 中的文件打包进 chunk-vendors,变动频率低,可利用 304 缓存
  2. 被引用少且体量大的库单独分一个 chunk(如 echarts、tinymce)
  3. 公共代码分成多个 chunk,避免从某入口访问时下载全部公共代码
  4. 体量很小的异步 chunk 合并进相关 chunk 或入口 chunk

路由懒加载决策矩阵

使用频率高使用频率低
文件小合并进入口 chunk单独一个 chunk
文件大单独一个 chunk(加 prefetch)单独一个 chunk

cacheGroups 配置示例

js
splitChunks: {
  maxInitialRequests: 5,
  maxAsyncRequests: 6,
  cacheGroups: {
    vendors: {
      name: 'chunk-vendors',
      test: /[\\/]node_modules[\\/]/,
      priority: -10,
      minChunks: 2,
      chunks: 'all',
    },
    echarts: {
      name: 'chunk-echarts',
      test: /[\\/]node_modules[\\/]echarts[\\/]/,
      priority: 0,
      chunks: 'all',
    },
    common: {
      name: true,
      minChunks: 2,
      minSize: 60000,
      priority: -20,
      chunks: 'initial',
      reuseExistingChunk: true,
    },
  },
}

chunk-vendors vs chunk-common 策略不同:vendors 是第三方库,变动少可用 304 长期缓存;common 是项目代码,经常变动缓存易失效,因此 common 应拆分而非合并

2. 路由懒加载

js
{
  path: '/user/XXX',
  component: () => import(/* webpackChunkName: "chunk-user-XXX" */ 'src/pages/XXX'),
}

路由懒加载原理(Webpack JSONP 机制)

import() 编译后,Webpack 生成以下核心机制:

  1. window["webpackJsonp"]:全局数组,各 chunk 通过 .push() 注册自身模块
  2. webpackJsonpCallback:拦截 push 方法,将 chunk 中的模块缓存到 modules 对象,并更新 installedChunks[chunkId] = 0(标记已加载)
  3. __webpack_require__(moduleId):执行模块,带缓存(installedModules)避免重复执行
  4. __webpack_require__.e(chunkId)(核心):通过 JSONP 动态加载 chunk
    • 创建 <script> 标签,src 指向对应 chunk 文件
    • 三种加载状态:0(已完成)、undefined(未加载/失败/超时)、Promise(加载中)
    • 超时处理:120s 超时后 reject Promise,抛出 ChunkLoadError

完整流程

用户访问路由 → import() 触发 → webpackAsyncContext 查 map 找到 chunkId
→ __webpack_require__.e(chunkId) 创建 <script> 加载 JS
→ chunk 加载完成执行 webpackJsonpCallback → 模块注册到 modules
→ __webpack_require__(moduleId) 执行模块 → 渲染页面

3. Tree Shaking

  • 仅对命名导出(export function xxx())生效export default 导出对象无法静态分析
  • Vue3 全面拥抱函数式编程、按需导入的原因之一
  • 前提条件:必须使用 ES 模块规范(静态分析才能确定依赖关系)

4. 组件懒加载

js
// 懒加载 → 独立打包为 dialogInfo.js,点击时才加载
const dialogInfo = () => import(/* webpackChunkName: "dialogInfo" */ '@/components/dialogInfo')

五、框架层优化

源头:框架 · 组件封装

1. Vue 项目性能优化

  • v-if vs v-show:频繁切换用 v-show,首次渲染用 v-if
  • v-for 的 key:列表变化时用唯一不变 key 借助本地复用
  • 多用 computed:依赖不变不重新计算
  • 合理使用 destroyed 清理事件/定时器;动态组件用 keep-alive 缓存
  • 不需要响应式的数据用 Object.freeze 冻结
  • 第三方插件按需加载
  • 使用运行时版本(vue.runtime.esm.js)比完整版小约 30%

2. SPA 首屏优化

  • 减小入口文件体积(splitChunks + 路由懒加载)
  • 静态资源本地缓存(浏览器缓存 + HTTP 缓存控制)
  • UI 框架按需加载
  • 图片资源压缩
  • 开启 GZip 压缩
  • 使用 SSR
  • CDN externals 减小 vendor.js

3. Vue3 大型表格性能优化

场景:20行 × 180列 el-table,switch 切换耗时 7-8s(Vue3 响应式开销)

优化手段

  1. shallowRef 替代 ref(减少 17-20%)

    • ref() 深层递归代理所有属性,shallowRef() 仅代理 .value 本身
    js
    const data = shallowRef([])  // 仅 data.value = xxx 触发更新
  2. 渲染函数中避免响应式依赖(减少 7-20%)

    js
    // 提取为普通数组,避免重复代理
    const plainCols = columns.value.map(item => ({ realWidth: item.realWidth }))
  3. v-if 替代 disabled(减少 80%)

    • el-tooltip 等复杂组件会成倍增加 patch 比对耗时
    html
    <!-- 仅需要时才创建 tooltip 组件 -->
    <span v-if="!showTooltip">{{ text }}</span>
    <el-tooltip v-else>...</el-tooltip>

六、CSS 层优化

源头:CSS · 工程化

1. 选择器匹配机制(从右向左)

  • 浏览器解析选择器时从右向左匹配
  • 最右侧部分决定匹配效率,应尽可能精确

2. 加载性能

  • CSS 压缩、减少 @import(用 link 代替)
  • 避免通配符选择器 *
  • 选择器层级不超过三层,多用 class 少用标签

3. 渲染性能

  • 减少重排重绘(避免频繁读取 offset*/scroll*/client*
  • 去除空规则、属性值 0 不加单位
  • CSS 雪碧图(小图标)
  • 减少昂贵属性:box-shadowborder-radiusfilter:nth-child
  • will-change 提前告知浏览器(不可滥用)

相关