前端性能优化

134 阅读11分钟

前端性能优化

  1. 哪里可以进行优化
  2. 性能指标
  3. 怎么测量性能
  4. 哪些层面进行优化

哪里可以进行优化

  1. SPA的首屏加载(老生常谈)
  2. 资源加载(图片,js,css,字体...)
  3. 渲染速度
  4. 用户体验(操作流畅性,ui界面反馈,帧率)

性能指标

当然不能凭感觉啊小老弟(妹)

  1. 渲染指标(浏览器画的速度)
  2. 交互指标(用户操作后页面反应速度)
  3. 视觉稳定性(画面拉扯度)
  4. 资源加载速度
  5. 帧率(fps)
  6. RAIL模型

渲染指标(FMP,LCP,FCP,TTI,FP)已按照重要程度排序

  1. FMP (First Meaningful Paint) - 首次有意义绘制
  • 定义:浏览器首次渲染有意义内容的时间点
  • 描述:衡量页面内容是否足够丰富,用户是否能快速理解页面内容
  • 测量:利用MutationObserver(等会讲)采集信息,计算FMP
  • 良好标准 < 1.8s
  1. LCP (Largest Meaningful Paint) - 最大内容绘制
  • 定义:可视区域内最大内容元素渲染完成的时间
  • 描述:Google核心Web指标之一,衡量主要内容加载完成时间
  • 测量:Chrome DevTools
  • 良好标准 < 2.5s
  1. FCP (First Meaningful Paint) - 首次内容绘制
  • 定义:浏览器首次渲染DOM内容的时刻(文本、图像、非白色canvas等)
  • 描述:用户感知页面开始加载的关键时间点
  • 测量:Chrome DevTools
  • 良好标准 < 1.8s
  1. TTI (Time to Interactive) - 可交互时间
  • 定义:页面完全可交互所需时间(主线程空闲且可响应用户输入)
  • 描述:页面开始加载到用户可以操作的时间
  • 测量:通过Lighthouse或WebPageTest测量
  • 良好标准 < 3.9s
  1. FP (First Paint) - 首次绘制
  • 定义:浏览器首次将像素渲染到屏幕上的时间点
  • 描述:同定义
  • 测量:Chrome DevTools 使用PerformanceObserver监听'paint'事件
  • 良好标准 < 1.8s

交互指标 (FID,INP)

  1. FID (First Input Delay) -首次输入延迟
  • 定义:用户首次与页面交互到浏览器实际响应该交互的时间
  • 描述:同定义
  • 测量:利用new PerformanceObserver(等会讲)采集信息
  • 良好标准 < 100ms
  1. INP (Interaction to Next Paint) - 下次绘制交互
  • 定义:用户交互开始到下一次绘制完成的总时间(取代FID)
  • 描述:2024年成为新的核心Web指标,衡量所有用户交互的响应能力
  • 测量:web-vitals , Lighthouse,WebPagetest
  • 良好标准 < 200ms

视觉稳定性 (CLS)

  1. CLS (Cumulative Layout Shift) - 累积布局偏移
  • 定义:页面生命周期内所有意外布局偏移得分的总和
  • 描述:预期外的布局偏移,元素偏移或者扩大缩小
  • 测量:布局偏移分数 = 影响范围 * 距离分数
  • 良好标准 < 0.1

资源加载速度 (TTFB)

  1. TTFB (Time to First Byte) - 首字节时间
  • 定义:从发起请求到接收到响应第一个字节的时间
  • 描述:输入url到浏览器获得第一个字节数据的时间
  • 测量:Chrome DevTools, WebPageTest,PerformanceObserver(api),performance(api)
  • 良好标准 < 200ms

帧率 (FPS)

  1. FPS (Frames Per Second) - 帧率
  • 定义:页面每秒渲染的帧数,衡量动画和交互的流畅度
  • 描述:同定义
  • 测量:Chrome DevTools(Rendering面板)
  • 良好标准 > 50fps

RAIL模型

  • R - Response(响应) 用户操作后,应用应在 50ms 内响应,确保交互流畅无延迟
  • A - Animation(动画) 动画和滚动应以 60fps(每帧约16ms)运行,保证视觉流畅
  • I - Idle(空闲)利用主线程空闲时间分批处理任务,每次任务不超过 50ms,避免阻塞关键交互
  • L - Load(加载)页面内容应在 1000ms 内完成主要渲染,提升首屏体验

怎么测量性能

知道了哪些指标可以优化了,但怎么知道是否优化了呢

测量:分两个层面

  1. 代码层面
  2. 工具层面

代码层面测量

performance api

时间测量

  1. 时间戳 performance.now()
  • 返回从页面开始加载到当前时刻的高精度时间戳(毫秒)
  • 例如 计算循环消耗的时间
  • 使用场景:测量函数执行时间、计算动画帧率、性能基准测试
// 获取高精度时间戳
const start = performance.now();

// 执行一些操作
for (let i = 0; i < 1000000; i++) {
  Math.sqrt(i);
}

const end = performance.now();
console.log(`操作耗时: ${(end - start).toFixed(4)} 毫秒`);
  1. 时间戳 performance.timeOrigin
  • 返回性能测量开始时的时间戳(Unix毫秒
  • 使用场景:精确时间同步、性能数据分析、跨设备时间校准
// 获取时间起点
const timeOrigin = performance.timeOrigin;

// 计算当前时间
const now = performance.now();
const currentTime = new Date(timeOrigin + now);

console.log('性能测量开始时间:', new Date(timeOrigin));
console.log('当前精确时间:', currentTime);

用户计时

1.performance.mark() 自定义时间戳标记

  • 在性能时间线上创建自定义时间戳标记
  • 使用场景:测量函数执行时间、计算动画帧率、性能基准测试
// 创建标记
performance.mark('script-start');

// 执行一些操作
loadData();
renderUI();

// 创建结束标记
performance.mark('script-end');

// 测量两个标记之间的时间
performance.measure('script-runtime', 'script-start', 'script-end');

// 获取测量结果
const measures = performance.getEntriesByName('script-runtime');
console.log(`脚本执行耗时: ${measures[0].duration.toFixed(2)}ms`);
  1. performance.measure() 测量两个标记之间的时间间隔
  • 使用场景:测量关键业务逻辑执行时间、监控组件渲染性能、分析异步操作耗时、自定义性能指标
// 测量页面初始化时间
performance.measure('init-time', undefined, 'dom-interactive');

// 获取所有测量结果
performance.getEntriesByType('measure').forEach(entry => {
  console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
});

资源计时

  1. performance.getEntriesByType('resource')
  2. 获取所有资源加载的性能数据
// 获取所有资源加载性能数据
const resources = performance.getEntriesByType('resource');

// 分析资源加载性能
resources.forEach(resource => {
  console.log(`
    ${resource.name}:
      类型: ${resource.initiatorType}
      DNS查询: ${resource.domainLookupEnd - resource.domainLookupStart}ms
      TCP连接: ${resource.connectEnd - resource.connectStart}ms
      请求响应: ${resource.responseEnd - resource.requestStart}ms
      总耗时: ${resource.duration}ms
  `);
});

导航计时

  1. performance.timing
  2. 供页面加载全过程的详细时间点
const timing = performance.timing;

// 计算关键性能指标
const metrics = {
  DNS查询: timing.domainLookupEnd - timing.domainLookupStart,
  TCP连接: timing.connectEnd - timing.connectStart,
  请求响应: timing.responseEnd - timing.requestStart,
  DOM解析: timing.domComplete - timing.domLoading,
  页面完全加载: timing.loadEventEnd - timing.navigationStart
};

console.log('页面加载性能指标:', metrics);

1752790164600.png

  1. performance.getEntriesByType('navigation')
  2. 获取页面导航的性能数据(现代替代方案)
// 获取页面导航性能数据
const [navigation] = performance.getEntriesByType('navigation');

console.log(`
  重定向次数: ${navigation.redirectCount}
  页面类型: ${navigation.type}
  卸载前页面时间: ${navigation.unloadEventEnd}ms
  DOM解析时间: ${navigation.domComplete - navigation.domInteractive}ms
`);

内存监控

  1. performance.memory
// 获取内存使用情况
if (performance.memory) {
  const memory = performance.memory;

  setInterval(() => {
    console.log(`
      已使用JS堆: ${(memory.usedJSHeapSize / 1048576).toFixed(2)} MB
      总JS堆大小: ${(memory.totalJSHeapSize / 1048576).toFixed(2)} MB
      堆大小限制: ${(memory.jsHeapSizeLimit / 1048576).toFixed(2)} MB
    `);
  }, 5000);
}

PerformanceObserver api

浏览器原生API,用于监测性能测量事件并异步接收性能指标数据。 它解决了传统 performance.getEntries 方法的局限性,提供了更高效、更灵活的性能监控方式

优点

  1. 异步监控:避免轮询性能数据,减少性能开销
  2. 精准监测:只收集你关心的性能指标
  3. 实时反馈:能事件发生时立即获取数据
// 创建 PerformanceObserver 实例
const observer = new PerformanceObserver((list) => {
  // 获取性能条目
  const entries = list.getEntries();

  // 处理性能数据
  entries.forEach(entry => {
    console.log(`[${entry.entryType}] ${entry.name}:`,
                entry.duration.toFixed(2), 'ms');
  });
});

// 配置观察的类型
observer.observe({
  // 监控资源加载性能
  //资源,绘制情况,长任务
  entryTypes: ['resource', 'paint', 'longtask']
});
// 停止监控
observer.disconnect();

PerformanceObserver可监控的指标

  1. paint
  • 监控内容: 首次绘制(FP)和首次内容绘制(FCP)
  • 关键属性: name, startTime
  • 使用场景: 测量页面渲染性能
observer.observe({ type: 'paint', buffered: true });
  1. resource
  • 监控内容: 所有资源加载时间(JS, CSS, 图片等)
  • 关键属性: duration, initiatorType
  • 使用场景: 优化资源加载性能
observer.observe({ entryTypes: ['resource'] });

3.Long Tasks

  • 监控内容: 执行时间超过50ms的任务
  • 关键属性: duration, attribution
  • 使用场景: 识别阻塞主线程的任务
observer.observe({ entryTypes: ['longtask'] });

4.Layout Shifts

  • 监控内容: 意外的布局偏移(CLS)
  • 关键属性: value, hadRecentInput
  • 使用场景: 改善视觉稳定性
observer.observe({ type: 'layout-shift', buffered: true });

5.Element Timing

  • 监控内容: 特定元素的渲染时间
  • 关键属性: renderTime, loadTime
  • 使用场景: 监控关键元素加载
observer.observe({ type: 'element', buffered: true });

5.Navigation Timing

  • 监控内容: 页面导航和加载时间
  • 关键属性: domComplete, loadEventEnd
  • 使用场景: 分析整页加载性能
observer.observe({ entryTypes: ['navigation'] });

6.其他

  • event:事件延迟
  • largest-contentful-paint:LCP
  • mark:用户自定义的标记

PerformanceObserver 与 Performance 对比

1752792449373.jpg 其实很简单PerformanceObserver基于事件驱动,而Performance是主动调用的

web-vitals

web-vitals是Google发起的,旨在提供各种质量信号的统一指南,其可获取三个关键指标(CLS、FID、LCP)和两个辅助指标(FCP、TTFB)。 指引

怎么用

  1. 先下载npm i web-vitals
  2. 使用
import {onLCP, onINP, onCLS } from 'web-vitals'; 
onCLS(console.log); 
onFID(console.log); 
onLCP(console.log);

利用PerformanceObserver和Performance和web-vitals做性能监控

  1. 利用这三个api获得数据并上传到后端
  2. 进行单台设备数据统计
  3. 再进行多台设备统计
  4. 还可以进行跨平台统计
  5. 利用图表或者可视化工具(canvas库)展示以达到性能监控的方案
  6. 具体细节这里就不再赘述了

工具层面测量

  1. 开发者工具(dev tools)
  • 利用Performance面板进行监控 1752794189689.jpg
  • 利用lighthouse(其实也是dev tools的一部分) 1752794741305.jpg

哪些层面进行优化(重头戏)

  1. 代码层面
  2. 工程配置层面
  3. 网络层面

代码层面

  1. js代码优化
  2. css代码优化
  3. 加载优化

js代码优化

  1. 用户操作相关
  2. 数据(展示)相关
  3. worker
  4. 内存管理
  5. 动画,渲染优化
  6. 代码分割和tree-shaking
用户操作相关
1. 事件相关
事件委派
  • 事件委派比较简单,这里一句话概括 利用事件冒泡特性,用父元素或上级元素的事件源对象判断当前触发事件对象元素(如果命中)则执行对应函数,从而减少多个子元素的事件绑定
//只需一个事件监听器在父元素上 
const list = document.getElementById('list'); 
list.addEventListener('click', function(event) { 
// 检查事件源是否是列表项 if (event.target.tagName === 'LI') { 
console.log('点击了:', event.target.textContent); 
event.target.classList.add('active'); } 
// 或者使用更精细的选择器匹配 
if (event.target.matches('.item')) { 
// 处理特定类名的元素 
} }); 
// 优点: // 1. 只需一个事件监听器 // 2. 自动处理动态添加的元素 // 3. 内存使用更高效
防抖节流
  • 防抖 (Debounce)-事件触发后延迟执行,若在延迟时间内再次触发则重新计时
  • 适用场景:输入框搜索、窗口大小调整
function debounce(func, delay) { 
let timer = null; return function(...args) { 
// 清除之前的定时器 
clearTimeout(timer); 
// 设置新的定时器 
timer = setTimeout(() => { func.apply(this, args); }, delay); 
}; 
} 
// 使用示例 
const searchInput = document.getElementById('search'); 
searchInput.addEventListener('input', debounce(function() { console.log('发送搜索请求'); }, 500) );
  • 节流 (Throttle)-固定时间间隔内只执行一次,稀释事件执行频率
  • 适用场景:滚动事件、鼠标移动
function throttle(func, interval) { 
let lastTime = 0; 
return function(...args) { 
const now = Date.now(); 
// 判断是否达到执行时间间隔 
if (now - lastTime >= interval) { 
func.apply(this, args); lastTime = now; } 
}; 
} 
// 使用示例 
window.addEventListener('scroll', throttle(function() { console.log('处理滚动事件'); }, 200) );
数据(展示)相关
虚拟滚动列表

虚拟滚动列表:当需要渲染的列表很长时,因为用户的视口或者查看列表的元素高度固定(或者根据内容变化,但是还是相对固定),所以可以只渲染一定数量列表nodes,然后在视口进行滚动复用

  • 网上的图

1752796369395.jpg -实现思路

  • 容器高度计算:计算整个列表的总高度(项目数量 × 项目高度)

  • 可见区域计算:根据滚动位置确定哪些项目在视口中

  • 动态渲染:只渲染可见区域内的项目,其他位置用空白占位

  • 滚动位置调整:使用transform调整列表位置,保持滚动体验

  • 示例代码就不写了,很多,自行搜索,算是作业

注意事项:

  • 请求数据,注意滑动速度与预取数据的长度和请求延迟的适配,尽量不出现空白,使滑动更顺畅
  • 每项高度如果不确定,可根据内容自动生成高度(或者直接用dom的高度)进行适配

使用场景

  1. 大型数据表格
  2. 聊天应用
  3. 地图标记
  4. 电商产品列表
  5. 图片库
时间切片
  • 时间切片(Time Slicing)是一种将大型任务分解为多个小任务并在多个帧中执行的技术,避免长时间阻塞主线程。
  • 核心思想:  将JavaScript任务分解为小片段,在浏览器空闲时间执行,保证UI流畅响应
  • 实现机制
  1. 任务分块:将大型任务(如渲染10000条数据)分解为多个小任务(如每次渲染50条)
  2. 空闲时间执行:使用 requestIdleCallback 或 setTimeout 在浏览器空闲期执行任务
  3. 渲染优先级:保证用户交互和动画等高优先级任务优先执行

使用 requestIdleCallback 实现基础时间切片

function processChunk(startIndex, total) {
  const CHUNK_SIZE = 50;
  let endIndex = Math.min(startIndex + CHUNK_SIZE, total);
  // 处理当前分块
  for (let i = startIndex; i < endIndex; i++) {
    // 渲染或处理数据项
  }
  if (endIndex < total) {
    // 请求下一个空闲时间段
    requestIdleCallback(() => {
      processChunk(endIndex, total);
    }, { timeout: 100 });
  }
}
// 开始处理
processChunk(0, 10000);

requestIdleCallback - Web API | MDN

使用 setTimeout 实现基础时间切片

可以是可以,但是setTimeout是加入事件队列(事件循环)的宏任务(会排队),它递归执行匹频率和屏幕刷新率并不一致,如果要渲染到页面上的话可能会产生空白割裂的情况。

使用 requestAnimationFrame 实现基础时间切片

function processChunk(startIndex, total) {
  const CHUNK_SIZE = 50;
  let endIndex = Math.min(startIndex + CHUNK_SIZE, total);

  // 处理当前分块
  for (let i = startIndex; i < endIndex; i++) {
    // 渲染或处理数据项
  }

  if (endIndex < total) {
    // 在下一次重绘前执行
    requestAnimationFrame(() => {
      processChunk(endIndex, total);
    });
  }
}

// 开始处理
processChunk(0, 10000);

requestAnimationFrame因为是在主线程上执行的,当主线程非常繁忙时,requestAnimationFrame的效果也大打折扣

分页

不再赘述。。。。

利用worker进行任务分担

Worker - Web API | MDN

很简单的东西,单独开个线程执行js

但是限制访问DOM/BOM

// 1. 创建Worker实例
const worker = new Worker('/path/to/worker.js')

// 2. 主线程发送消息
worker.postMessage({ task: 'calculate', data: payload })

// 3. 接收处理结果
worker.onmessage = (e) => {
  console.log('Result:', e.data)
}

// 4. 错误处理
worker.onerror = (error) => {
  console.error('Worker error:', error)
}
//5.注销
worker.terminate()
// /path/to/worker.j
self.importScripts('utils.js');// 导入外部脚本
self.onmessage = function(e) {
  self.postMessage(e.data+1);
};

可以利用worker做一些运算,数据处理

优势

  • 解放主线程 - 避免UI卡顿
  • 并行计算 - 充分利用多核CPU
  • 隔离环境 - 不共享作用域更安全
内存管理
  1. 定时器,无用引用,无用闭包,绑定事件,requestAnimationFrame等等 的清除
  2. WekRef,WeakMap,WeakSet的使用
  3. 大数据下合理使用数据结构(享元模式的应用)
代码分割和tree-shaking(重要哦)
  1. 现代方案动态import()
  2. 构建框架内抽离公共代码
  3. tree-shaking(合理使用)

持续更新