13 性能优化
约 4877 字大约 16 分钟
2025-09-07
- DCL(DOM Content Loaded):dom解析完毕,不包括css、图像等资源的加载 监听:
document.addeventListener('DOMContentLoaded', function() {}, false)计算方式:
const dclTime = performance.timing.domContentLoadedEventEnd - performance.timing.domContentLoadedEventStart- load (Onload Event):页面中依赖的所有资源加载完的事件 监听:
window.onload计算方式
const loadTime = performance.timing.loadEventEnd - performance.timing.loadEventStart;- FP (First Paint):渲染出第一个像素点。(一般在html解析完成或者解析一部分的时候触发)
const fp = performance.getEntriesByType('paint').filter(entry => entry.name == 'first-paint')[0].startTime;- FCP(First Contentful Paint):渲染出第一个内容 计算方式
const fcp = performance.getEntriesByType('paint').filter(entry => entry.name == 'first-contentful-paint')[0].startTime;- FMP(First Meaningful Paint):首次渲染出有意义的内容的事件。没有标准的定义 由于没有标准的定义,需要自己使用MutationObserver计算
- LCP(Largest Contentful Paint):最大内容渲染时间 计算方式
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('LCP candidate:', entry.startTime, entry);
}
}).observe({type: 'largest-contentful-paint', buffered: true});- 白屏时间:地址栏输入网址后回车 - 浏览器出现第一个元素 白屏结束时间 = FP事件触发时间
- 首屏时间:地址栏输入网址后回车 - 浏览器第一屏渲染完成 首屏结束时间 = FCP事件触发时间 或 FMP、LCP
可交互时间
- TTI(Time to Interactive):首次可交互时间。计算较为复杂,需要满足以下条件:
- 从 FCP 指标后开始计算;
- 持续 5 秒内无长任务(执行时间超过 50 ms)且无两个以上正在进行中的 GET 请求;
- 往前回溯至 5 秒前的最后一个长任务结束的时间。 计算方式
const timeToInteractive = performance.timing.domInteractive - performance.timing.fetchStart- FID(First Input Delay):首次输入延迟时间,记录在FCP和TTI之间,用户首次与页面交互时响应的延迟 计算方式
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('fid', entry.processingStart - entry.startTime);
}
});
observer.observe({type: 'first-input', buffer: true});稳定性指标
- CLS(Cumulative Layout Shift):在页面的整个生命周期中发生的每一次意外布局变化的最大布局变化得分,得分越小页面越稳定 不稳定元素:一个非用户操作但发生较大偏移的可见元素称为不稳定元素。 布局变化得分:元素从原始位置偏移到当前位置影响的页面比例 * 元素偏移距离比例 计算方式
import {getCLS} from 'web-vitals';
getCLS(console.log);流畅性指标
FPS
- Chrome DevTool 中有一栏 Rendering 中包含 FPS 指标,但目前浏览器标准中暂时没有提供相应 API ,只能手动实现。这里需要借助
requestAnimationFrame方法模拟实现,浏览器会在下一次重绘之前执行 rAF 的回调,因此可以通过计算每秒内 rAF 的执行次数来计算当前页面的 FPS。 - FPS过低会让用户感觉卡顿,因此这个计算可以用来监控页面卡顿情况。
什么情况会造成操作卡顿和渲染慢
- 一次性操作大量dom
- 进行复杂度很高的运算
vue的优化
- 正确使用v-show和v-if
- v-for使用合理的key值
- keep-alive缓存
- 区分请求粒度,减少请求范围
- 不变数据、定期失效可以缓存再cookies或者localstorage中
- 对于要页面要缓存的数据存于内存中(全局对象、pinia),能够保证更新、
大文件上传
- 多线程切片:web-worker,切片完成后发送消息
- 分块读取,增量哈希:如果一次性读取整个文件可能会导致内存溢出,每次只读取一小块,处理完成后再读取后面的块,处理完成后垃圾回收机制也会回收不使用的数据
- 抽样哈希:如果文件及其庞大,webwoker计算也会耗费很久就可以采用抽样哈希,只取一部分分块的数据进行哈希运算
- 断点续传:选择文件后先请求后端是否有相同文件已上传一部分
- 断开重传:
- 资源嗅探:切片完后将blob存储到IndexedDB,下次用户进来后嗅探一下是否有未上传完的切片 上传完成后向后端定义的接口中发送上传完成的请求让后端开始合并
后端 后端合并时为避免文件过大也可采用流式api
行为埋点
埋点
埋点主要用于收集用户行为数据。通过在前端代码中插入代码或脚本的方式来实现埋点功能。 埋点的主要作用就是:捕获特定用户行为(如点击、浏览、提交表单、页面跳转等)以及关键业务数据(如下单金额、商品类别等)
埋点的实现方案大致可以分为以下三大类:
- 手动埋点:在代码中手动加入记录代码来捕获特定事件。
- 自动埋点:利用DOM事件代理等技术来捕获页面上所有事件,从而减少手动配置。
- 可视化埋点:通过工具界面标记需要采集的元素和事件,可以不用手写代码。
手动埋点
手动埋点就是在用户操作的行为添加监听,并将该操作记录,比如:按钮点击埋点就是监听目标按钮的点击事件然后将这些信息记录下来
监控
监控主要关注系统的性能和稳定性。在日常开发中,我们会通过采集页面加载时间、资源请求、错误日志等数据的方式来实现前端监控。 监控的主要作用就是:及时发现并定位页面性能瓶颈或代码异常,目的是为了保障系统不出bug
监控一般需要完成以下三大部分:
- 性能监控:如:首屏加载时间、页面交互耗时、资源加载耗时等。
- 错误监控:捕获JEvaScript错误、网络请求失败、资源加载异常等。
- 用户体验监控:收集白屏、卡顿等影响用户体验的问题等。
区别
| 维度 | 埋点 | 监控 |
|---|---|---|
| 目标 | 捕获用户行为数据 | 监控系统性能、错误、稳定性 |
| 数据类型 | 用户点击、表单提交,页面跳转等 | 页面加载事件、错误日志、卡顿情况等 |
| 实现方式 | 手动埋点、自动埋点、可视化埋点 | 错误捕获、性能指标采集 |
| 核心关注点 | 用户行为、业务数据 | 系统bug、性能优化 |
性能监控
如果要监控某个操作的性能可以该操作开始前与结束后分别记录要监控的性能指标,比如:首屏渲染耗时,就需要在加载前记录一下当前的时间戳,等加载完成后再记录下时间戳,用加载完成后的时间减去开始时的时间就能得到耗时 性能监控一般使用preference全局对象中提供的函数等接口获取数据的
错误监控
监听js-error事件并根据不同的错误类型进行判断来监控
上报
对于用户行为的埋点和错误监控,一般都是需要上报的,上报给服务器服务器接收后进行处理将数据展示出来以便分析并优化程序
上报也分为统一上报和实时上报
ECharts性能优化
1. 分段加载
使用dataZoom配置只显示一部分区域内的数据
优点 可以很好的解决 ECharts 首次进行大数据量渲染造成的卡顿体验问题,不需要额外的数据处理,只需要通过简单的配置 dataZoom 缩放组件就可以实现
缺点
- 无法进行全局概览数据,只能分段查看数据
- 可能需要根据数据量动态的配置属性值,start、end、minSpan 和 maxSpan
2. 降采样
通过sampling属性配置过滤数据点的策略,被过滤的数据将不会被渲染 可选值有以下几种:
- lttb: 采用
Largest-Triangle-Three-Bucket算法,可以最大程度保证采样后线条的趋势,形状和极值。 - average: 取过滤点的平均值
- min: 取过滤点的最小值
- max: 取过滤点的最大值
- minmax: 取过滤点绝对值的最大极值 (从 v5.5.0 开始支持)
- sum: 取过滤点的和
优点
- 使用简单,ECharts 内部降采样算法,效果显著
- 可以完整的将曲线趋势展示出来,和原曲线基本一致
缺点
- 并不是展示的所有点,会删除一些无用的点,保证渲染性能
- 最大程度保证采样后线条的趋势,形状和极值,但是某些情况下,极值有偏差
3. 使用后台线程进行数据处理
使用webworker在后台处理数据,不阻塞主线程
4. 过滤不必要的数据
5. 使用canvas
相比于svg,canvas没有真实dom也没有事件,所以在没有复杂事件需求的图表中使用canvas可以提升性能
6. 使用appendData异步分片加载数据
appendData是v5版本提出的,只有少部分图表支持,它可以分片异步加载数据和增量渲染 允许向指定的系列(series)追加数据,而无需重新设置整个 option。
echartsInstance. appendData
(opts: {
// 要增加数据的系列序号。
seriesIndex?: string,
// 增加的数据。
data?: Array|TypedArray
}) => string示例:
myChart.appendData({
seriesIndex: 0, // 指定要追加数据的系列索引
data: [
// 新的数据块
[timestamp1, value1],
[timestamp2, value2],
// ...
]
});开启大数据模式与渐进式渲染
大数据模式
large属性是ECharts为海量数据渲染设计的专用性能优化开关,通常数据量在数万到数百万级规模时,开启此选项将直接调用底层优化算法,从而获得显著的渲染性能提升, largeThreshold是与large属性配合使用的阈值参数,它定义了启用大规模优化模式的数据量下限。当且仅当数据项数超过此阈值时,优化绘制逻辑才会生效;反之,系统将使用标准渲染流程,避免不必要的开销;大多数情况下无需手动调整。
setOption({
series: [
{
type: "bar",
data,
large: true,
largeThreshold: 1000,
},
],
});[!NOTE] 缺陷
- 不是每种类型的图表都支持large和largeThreshold属性的,目前仅有bar和scatter图表支持。
- 在large模式下,Echarts出于性能优化的考虑,无法为单个数据点设置独立样式,所有数据点将共享同一套样式配置。
分片渲染
progressive本质是开启“分片渲染”模式,用以解决超大规模图形元素(数千至千万级)造成的浏览器瞬时阻塞风险。启用后,ECharts会将庞大的数据集自动分割为多个小块(chunk),在多个动画帧中依次渲染,从而将一次性的沉重负载分散为平缓的增量任务。 progressive属性控制渐进式渲染的 “粒度”,其值为每一帧渲染的图形数量,默认值为400,设为0,则相当于关闭此功能; progressiveThreshold属性则设定了启用此功能的 “门槛”。仅当图形总数超过此阈值时,progressive的配置才会生效,开始分帧渲染。
[!NOTE] Title 开启large默认会开启progressive渐进渲染。
vue中的性能优化
- 数据层级不宜过深,合理设置响应式数据
- 通过Object.freeze()方法冻结属性
- 使用数据时缓存值的结果,不频繁取值。
- 合理设置Key属性
- v-show和v-if的选取
- 控制组件粒度->Vue采用组件级更新
- 采用函数式组件->函数式组件开销低
- 采用异步组件->借助webpack分包的能力
- 使用keep-alive缓存组件v-once
- 分页、虚拟滚动、时间分片等策略..
- 对于只有内容且变化较少的内容手动判断而不使用v-if
<div>{{ isShow ? '显示' : '隐藏' }}</div>
<div v-if='isShow'>显示</div>
<div v-else>隐藏</div>手动控制的话只会更改其中的内容,如果使用v-if整个dom都会重新创建
时间分片
对于大量数据渲染的时候,JS运算往往不是性能的瓶颈,性能的瓶颈主要在于渲染阶段
时间分片的核心思想就是将计算量大的任务分批执行
- settimeout
- promise
- requestAnimationFrame
- DocumentFragment
内存泄漏
程序中一个变量已经没用了,而垃圾回收机制认为这段内存还在使用,从而无法释放。(程序对一段内存失去控制权)
首屏加载优化
一、网络层优化
- 启动http2/3:多路复用请求,资源并行下载
- gzip/brotli压缩:代码文件压缩减小体积
- cdn分发:从最近的节点拿资源
- 合并请求:数据小的请求合并到业务相同的请求中
- dns预解析、预连接
二、资源优化
- 代码分块:第三方库和业务代码剥离可以缓存库代码
- 图片优化:雪碧图、小图使用base64编码,图标使用svg可以合并请求内嵌到代码中减少请求
- 树摇优化:删除未使用的代码、切换新版本的支持树摇优化的库、简单的工具自己实现
- 图片懒加载、预加载
树摇
基于es6的导入导出语法,施行静态分析,es6的导入语法只能在文件的头部使用,即使没有放在头部,解释器也会将它移动到头部,这样在静态分析时就可以明确的知道都导入了哪些其他的文件,而commonjs的导入语法可以写在文件的任何位置,就无法进行静态分析,只能将整个文件全部执行才知道导入了哪些文件。 在进行树摇优化时如果导入的模块没有用到也不会被树摇删除,这是因为出于安全考虑,一些模块可能会有副作用,如果引用了即使没有使用也会有副作用生效(比如模块中导入全局样式,而文件中又导入了模块就会导致全局样式生效),浏览器不知道这些副作用是否是有意为之,因此就会选择保留。如果要将导入而没有使用的模块也被树摇删除,可以在packages.json中的sideEffects字段设置为布尔值或者数组表示项目中的所有文件都有/没有副作用,或者只有数组中的文件有副作用。 此外还可以在文件中使用注释标记某些函数没有副作用
export const test = /*#__PURE__*/ () => {
// ...
}动态导入import()如果放在if语句中会影响树摇优化,如果没有放在条件语句中也会根据它导入路径的不同进行树摇优化。如果路径是项目中的文件则可以进行树摇优化,如果不是项目中的文件,如远程加载则不会进行树摇优化 动态导入返回的是一个promise,当导入的模块加载完毕后会resolve
三、渲染层
- ssr/ssg:服务端生成页面可以让用户马上看到页面
- 骨架屏、占位符、进度条、加载动画
- 关键路径优化:只加载首屏关键资源,其他的延迟加载
四、js执行优化
- 异步或延迟加载脚本
- 拆分首屏逻辑,非首屏逻辑延迟加载
五、缓存策略
- 合适的缓存策略优化非首次加载
大数据量优化
一、渲染层
- 虚拟列表
- 分片渲染
- 避免重复渲染:选择合理的key、使用v-memo
二、计算层
- webworker多线程计算
- 数据懒加载、分页加载
- 缓存
三、绘制层
- 分层
- 降采样
四、传输层
- 压缩数据传输
- 只传输必要字段
- 结构化数据:扁平化大对象、减少深层响应式代理
五、vue优化
- shallowRef、shallowReactive
- 使用watchEffect、computed代理深层代理
图片优化
- 选择合适的格式:webp比jpg和png都要小,且支持透明通道,avif体积更小但兼容性差
- 压缩:有损、无损、根据网络状态动态调整
- 响应式加载:根据设备的不同加载不同尺寸及清晰度的图片
- 懒加载
- cdn分发
will change
will-change是一个CSS属性,它可以告诉浏览器某个元素将要发生的变化。通过明确指定这些变化,浏览器可以事先分配和优化相应的资源,从而提升渲染的性能。
will change能够优化的原理 例如,当我们设置了will-change: transform时,浏览器会为该元素创建一个独立的图层,将这个图层标记为“即将变换”。这样,在进行布局和绘制时,浏览器就可以更高效地处理这个元素,而无需重新计算整个渲染树。 加入will-change后,元素会被提升到单独的复合层,动画(重绘、重排)的操作只会在单独复合层上进行,减少了原来的页面层重绘和重排的行为
但分层亦需要性能开销,因此不能滥用will change
文档碎片
document.fragment创建一个dom,这个dom是在内存中的,期间对这个dom的任何操作都不会触发页面中dom的更新,只有手动让这个dom更新才会让这个dom中的内容应用到页面dom中 核心思想和虚拟dom相同,都是在内存中创建一个dom对其进行修改再统一更新减少重排
断线重连
重连应该有次数上限
- 如果用户断网一直重连意义不大并且还会造成资源浪费和性能损耗
- 会带来冗余日志
- 用户体验差:一直重连没有明确反馈
流式渲染markdown
- 自动补全末尾缺失的markdown语法
- 将未闭合的markdown语法暂时当作字符展示
- 在输出完成前将内容当作纯文本渲染,输出完成后使用markdwon渲染
- 分区块渲染,每个区块收集完毕后再渲染
- 和后端约定每个token必须是完整的markdown语法
市面上已经有成熟的库支持流式渲染markdown
保护js代码
- 禁止打开开发者工具
- 禁用开发者工具快捷键
- 定时通过一定的特征检测开发者工具是否被打开,打开旧清空页面或跳转
- 添加debugger断点
- 压缩在单行并且添加debugger
- 动态添加debugger
- 混淆:使用工具将变量名替换成无意义字符
- 将核心逻辑(如加密算法)进行加密,执行时动态解析
贡献者
版权所有
版权归属:wynnsimon
