前端性能优化
- 哪里可以进行优化
- 性能指标
- 怎么测量性能
- 哪些层面进行优化
哪里可以进行优化
- SPA的首屏加载(老生常谈)
- 资源加载(图片,js,css,字体...)
- 渲染速度
- 用户体验(操作流畅性,ui界面反馈,帧率)
性能指标
当然不能凭感觉啊小老弟(妹)
- 渲染指标(浏览器画的速度)
- 交互指标(用户操作后页面反应速度)
- 视觉稳定性(画面拉扯度)
- 资源加载速度
- 帧率(fps)
- RAIL模型
渲染指标(FMP,LCP,FCP,TTI,FP)已按照重要程度排序
- FMP (First Meaningful Paint) - 首次有意义绘制
- 定义:浏览器首次渲染有意义内容的时间点
- 描述:衡量页面内容是否足够丰富,用户是否能快速理解页面内容
- 测量:利用MutationObserver(等会讲)采集信息,计算FMP
- 良好标准 < 1.8s
- LCP (Largest Meaningful Paint) - 最大内容绘制
- 定义:可视区域内最大内容元素渲染完成的时间
- 描述:Google核心Web指标之一,衡量主要内容加载完成时间
- 测量:Chrome DevTools
- 良好标准 < 2.5s
- FCP (First Meaningful Paint) - 首次内容绘制
- 定义:浏览器首次渲染DOM内容的时刻(文本、图像、非白色canvas等)
- 描述:用户感知页面开始加载的关键时间点
- 测量:Chrome DevTools
- 良好标准 < 1.8s
- TTI (Time to Interactive) - 可交互时间
- 定义:页面完全可交互所需时间(主线程空闲且可响应用户输入)
- 描述:页面开始加载到用户可以操作的时间
- 测量:通过Lighthouse或WebPageTest测量
- 良好标准 < 3.9s
- FP (First Paint) - 首次绘制
- 定义:浏览器首次将像素渲染到屏幕上的时间点
- 描述:同定义
- 测量:Chrome DevTools 使用PerformanceObserver监听'paint'事件
- 良好标准 < 1.8s
交互指标 (FID,INP)
- FID (First Input Delay) -首次输入延迟
- 定义:用户首次与页面交互到浏览器实际响应该交互的时间
- 描述:同定义
- 测量:利用new PerformanceObserver(等会讲)采集信息
- 良好标准 < 100ms
- INP (Interaction to Next Paint) - 下次绘制交互
- 定义:用户交互开始到下一次绘制完成的总时间(取代FID)
- 描述:2024年成为新的核心Web指标,衡量所有用户交互的响应能力
- 测量:web-vitals , Lighthouse,WebPagetest
- 良好标准 < 200ms
视觉稳定性 (CLS)
- CLS (Cumulative Layout Shift) - 累积布局偏移
- 定义:页面生命周期内所有意外布局偏移得分的总和
- 描述:预期外的布局偏移,元素偏移或者扩大缩小
- 测量:布局偏移分数 = 影响范围 * 距离分数
- 良好标准 < 0.1
资源加载速度 (TTFB)
- TTFB (Time to First Byte) - 首字节时间
- 定义:从发起请求到接收到响应第一个字节的时间
- 描述:输入url到浏览器获得第一个字节数据的时间
- 测量:Chrome DevTools, WebPageTest,PerformanceObserver(api),performance(api)
- 良好标准 < 200ms
帧率 (FPS)
- FPS (Frames Per Second) - 帧率
- 定义:页面每秒渲染的帧数,衡量动画和交互的流畅度
- 描述:同定义
- 测量:Chrome DevTools(Rendering面板)
- 良好标准 > 50fps
RAIL模型
- R - Response(响应) 用户操作后,应用应在 50ms 内响应,确保交互流畅无延迟
- A - Animation(动画) 动画和滚动应以 60fps(每帧约16ms)运行,保证视觉流畅
- I - Idle(空闲)利用主线程空闲时间分批处理任务,每次任务不超过 50ms,避免阻塞关键交互
- L - Load(加载)页面内容应在 1000ms 内完成主要渲染,提升首屏体验
怎么测量性能
知道了哪些指标可以优化了,但怎么知道是否优化了呢
测量:分两个层面
- 代码层面
- 工具层面
代码层面测量
performance api
时间测量
- 时间戳 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)} 毫秒`);
- 时间戳 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`);
- performance.measure() 测量两个标记之间的时间间隔
- 使用场景:测量关键业务逻辑执行时间、监控组件渲染性能、分析异步操作耗时、自定义性能指标
// 测量页面初始化时间
performance.measure('init-time', undefined, 'dom-interactive');
// 获取所有测量结果
performance.getEntriesByType('measure').forEach(entry => {
console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
});
资源计时
- performance.getEntriesByType('resource')
- 获取所有资源加载的性能数据
// 获取所有资源加载性能数据
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
`);
});
导航计时
- performance.timing
- 供页面加载全过程的详细时间点
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);
- performance.getEntriesByType('navigation')
- 获取页面导航的性能数据(现代替代方案)
// 获取页面导航性能数据
const [navigation] = performance.getEntriesByType('navigation');
console.log(`
重定向次数: ${navigation.redirectCount}
页面类型: ${navigation.type}
卸载前页面时间: ${navigation.unloadEventEnd}ms
DOM解析时间: ${navigation.domComplete - navigation.domInteractive}ms
`);
内存监控
- 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 方法的局限性,提供了更高效、更灵活的性能监控方式
优点
- 异步监控:避免轮询性能数据,减少性能开销
- 精准监测:只收集你关心的性能指标
- 实时反馈:能事件发生时立即获取数据
// 创建 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可监控的指标
- paint
- 监控内容: 首次绘制(FP)和首次内容绘制(FCP)
- 关键属性: name, startTime
- 使用场景: 测量页面渲染性能
observer.observe({ type: 'paint', buffered: true });
- 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 对比
其实很简单PerformanceObserver基于事件驱动,而Performance是主动调用的
web-vitals
web-vitals是Google发起的,旨在提供各种质量信号的统一指南,其可获取三个关键指标(CLS、FID、LCP)和两个辅助指标(FCP、TTFB)。 指引
怎么用
- 先下载npm i web-vitals
- 使用
import {onLCP, onINP, onCLS } from 'web-vitals';
onCLS(console.log);
onFID(console.log);
onLCP(console.log);
利用PerformanceObserver和Performance和web-vitals做性能监控
- 利用这三个api获得数据并上传到后端
- 进行单台设备数据统计
- 再进行多台设备统计
- 还可以进行跨平台统计
- 利用图表或者可视化工具(canvas库)展示以达到性能监控的方案
- 具体细节这里就不再赘述了
工具层面测量
- 开发者工具(dev tools)
- 利用Performance面板进行监控
- 利用lighthouse(其实也是dev tools的一部分)
哪些层面进行优化(重头戏)
- 代码层面
- 工程配置层面
- 网络层面
代码层面
- js代码优化
- css代码优化
- 加载优化
js代码优化
- 用户操作相关
- 数据(展示)相关
- worker
- 内存管理
- 动画,渲染优化
- 代码分割和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,然后在视口进行滚动复用
- 网上的图
-实现思路
-
容器高度计算:计算整个列表的总高度(项目数量 × 项目高度)
-
可见区域计算:根据滚动位置确定哪些项目在视口中
-
动态渲染:只渲染可见区域内的项目,其他位置用空白占位
-
滚动位置调整:使用transform调整列表位置,保持滚动体验
-
示例代码就不写了,很多,自行搜索,算是作业
注意事项:
- 请求数据,注意滑动速度与预取数据的长度和请求延迟的适配,尽量不出现空白,使滑动更顺畅
- 每项高度如果不确定,可根据内容自动生成高度(或者直接用dom的高度)进行适配
使用场景
- 大型数据表格
- 聊天应用
- 地图标记
- 电商产品列表
- 图片库
时间切片
- 时间切片(Time Slicing)是一种将大型任务分解为多个小任务并在多个帧中执行的技术,避免长时间阻塞主线程。
- 核心思想: 将JavaScript任务分解为小片段,在浏览器空闲时间执行,保证UI流畅响应
- 实现机制
- 任务分块:将大型任务(如渲染10000条数据)分解为多个小任务(如每次渲染50条)
- 空闲时间执行:使用
requestIdleCallback或setTimeout在浏览器空闲期执行任务 - 渲染优先级:保证用户交互和动画等高优先级任务优先执行
使用 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进行任务分担
很简单的东西,单独开个线程执行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
- 隔离环境 - 不共享作用域更安全
内存管理
- 定时器,无用引用,无用闭包,绑定事件,requestAnimationFrame等等 的清除
- WekRef,WeakMap,WeakSet的使用
- 大数据下合理使用数据结构(享元模式的应用)
代码分割和tree-shaking(重要哦)
- 现代方案动态import()
- 构建框架内抽离公共代码
- tree-shaking(合理使用)