performance-helper 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/render.d.ts +127 -0
- package/dist/core/render.js +670 -0
- package/dist/core/reporter.d.ts +1 -1
- package/dist/core/reporter.js +14 -3
- package/dist/index.d.ts +59 -2
- package/dist/index.js +105 -3
- package/dist/types/index.d.ts +84 -4
- package/package.json +5 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { RenderMetrics } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* 渲染性能监控器
|
|
4
|
+
* 用于检测重排(Reflow)和重绘(Repaint)
|
|
5
|
+
*/
|
|
6
|
+
export declare class RenderMonitor {
|
|
7
|
+
private reflows;
|
|
8
|
+
private repaints;
|
|
9
|
+
private gpuAcceleratedElements;
|
|
10
|
+
private isMonitoring;
|
|
11
|
+
private frameObserver?;
|
|
12
|
+
private mutationObserver?;
|
|
13
|
+
private styleObserver?;
|
|
14
|
+
private reflowDetector;
|
|
15
|
+
/**
|
|
16
|
+
* 开始监控渲染性能
|
|
17
|
+
*/
|
|
18
|
+
start(): void;
|
|
19
|
+
/**
|
|
20
|
+
* 停止监控
|
|
21
|
+
*/
|
|
22
|
+
stop(): void;
|
|
23
|
+
/**
|
|
24
|
+
* 观察帧性能(用于检测重排重绘)
|
|
25
|
+
*/
|
|
26
|
+
private observeFrameTiming;
|
|
27
|
+
/**
|
|
28
|
+
* 监控帧率变化(用于检测性能问题)
|
|
29
|
+
*/
|
|
30
|
+
private monitorFrameRate;
|
|
31
|
+
/**
|
|
32
|
+
* 记录潜在的重排
|
|
33
|
+
* 优化:限制记录数量,避免内存溢出
|
|
34
|
+
*/
|
|
35
|
+
private recordPotentialReflow;
|
|
36
|
+
/**
|
|
37
|
+
* 观察DOM变化(用于检测可能导致重排的操作)
|
|
38
|
+
*/
|
|
39
|
+
private observeDOMChanges;
|
|
40
|
+
/**
|
|
41
|
+
* 观察样式变化(用于检测可能导致重绘的操作)
|
|
42
|
+
*/
|
|
43
|
+
private observeStyleChanges;
|
|
44
|
+
/**
|
|
45
|
+
* 拦截样式读取(用于检测重绘)
|
|
46
|
+
* 注意:这个方法会修改全局的 getComputedStyle,需要谨慎使用
|
|
47
|
+
*/
|
|
48
|
+
private interceptStyleReads;
|
|
49
|
+
/**
|
|
50
|
+
* 检测GPU加速
|
|
51
|
+
* 优化:使用 requestIdleCallback 和分批处理,避免阻塞主线程
|
|
52
|
+
*/
|
|
53
|
+
private detectGPUAcceleration;
|
|
54
|
+
/**
|
|
55
|
+
* 检测GPU加速(检测所有元素)
|
|
56
|
+
* 优化:使用分批处理,避免阻塞主线程
|
|
57
|
+
*/
|
|
58
|
+
private detectGPUAccelerationSelectively;
|
|
59
|
+
/**
|
|
60
|
+
* 分批处理GPU加速检测
|
|
61
|
+
*/
|
|
62
|
+
private processGPUAccelerationBatch;
|
|
63
|
+
/**
|
|
64
|
+
* 检查元素是否使用GPU加速
|
|
65
|
+
*/
|
|
66
|
+
private checkGPUAcceleration;
|
|
67
|
+
/**
|
|
68
|
+
* 判断属性是否可能导致重排
|
|
69
|
+
*/
|
|
70
|
+
private isReflowTriggeringAttribute;
|
|
71
|
+
/**
|
|
72
|
+
* 从Mutation记录重排
|
|
73
|
+
*/
|
|
74
|
+
private recordReflowFromMutation;
|
|
75
|
+
/**
|
|
76
|
+
* 记录重排
|
|
77
|
+
*/
|
|
78
|
+
private recordReflow;
|
|
79
|
+
/**
|
|
80
|
+
* 记录重绘
|
|
81
|
+
*/
|
|
82
|
+
private recordRepaint;
|
|
83
|
+
/**
|
|
84
|
+
* 获取堆栈跟踪
|
|
85
|
+
* 优化:只在需要时获取,避免频繁调用影响性能
|
|
86
|
+
*/
|
|
87
|
+
private getStackTrace;
|
|
88
|
+
/**
|
|
89
|
+
* 手动标记重排(供外部调用)
|
|
90
|
+
*/
|
|
91
|
+
markReflow(element?: Element, reason?: string): void;
|
|
92
|
+
/**
|
|
93
|
+
* 手动标记重绘(供外部调用)
|
|
94
|
+
*/
|
|
95
|
+
markRepaint(element?: Element, reason?: string): void;
|
|
96
|
+
/**
|
|
97
|
+
* 获取渲染性能指标
|
|
98
|
+
*/
|
|
99
|
+
getMetrics(): RenderMetrics;
|
|
100
|
+
/**
|
|
101
|
+
* 获取频繁重排的元素
|
|
102
|
+
* 优化:使用 Map 和单次遍历,时间复杂度 O(n)
|
|
103
|
+
*/
|
|
104
|
+
getFrequentReflowElements(threshold?: number): Array<{
|
|
105
|
+
element: Element;
|
|
106
|
+
path: string;
|
|
107
|
+
count: number;
|
|
108
|
+
}>;
|
|
109
|
+
/**
|
|
110
|
+
* 获取未使用GPU加速但应该使用的元素
|
|
111
|
+
* 这些元素通常是动画元素或频繁更新的元素
|
|
112
|
+
* 直接检测所有元素
|
|
113
|
+
*/
|
|
114
|
+
getElementsNeedingGPUAcceleration(): Array<{
|
|
115
|
+
element: HTMLElement;
|
|
116
|
+
path: string;
|
|
117
|
+
reason: string;
|
|
118
|
+
}>;
|
|
119
|
+
/**
|
|
120
|
+
* 检查元素是否有真正的动画(duration > 0)
|
|
121
|
+
*/
|
|
122
|
+
private hasRealAnimation;
|
|
123
|
+
/**
|
|
124
|
+
* 清理数据
|
|
125
|
+
*/
|
|
126
|
+
clear(): void;
|
|
127
|
+
}
|
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import { getElementPath } from '../utils/index';
|
|
2
|
+
/**
|
|
3
|
+
* 渲染性能监控器
|
|
4
|
+
* 用于检测重排(Reflow)和重绘(Repaint)
|
|
5
|
+
*/
|
|
6
|
+
export class RenderMonitor {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.reflows = [];
|
|
9
|
+
this.repaints = [];
|
|
10
|
+
this.gpuAcceleratedElements = [];
|
|
11
|
+
this.isMonitoring = false;
|
|
12
|
+
// 用于检测重排的标记
|
|
13
|
+
this.reflowDetector = {
|
|
14
|
+
lastFrameTime: 0,
|
|
15
|
+
frameCount: 0,
|
|
16
|
+
suspiciousElements: new Map(),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 开始监控渲染性能
|
|
21
|
+
*/
|
|
22
|
+
start() {
|
|
23
|
+
if (this.isMonitoring) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
this.isMonitoring = true;
|
|
27
|
+
this.observeFrameTiming();
|
|
28
|
+
this.observeDOMChanges();
|
|
29
|
+
this.observeStyleChanges();
|
|
30
|
+
this.detectGPUAcceleration();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 停止监控
|
|
34
|
+
*/
|
|
35
|
+
stop() {
|
|
36
|
+
this.isMonitoring = false;
|
|
37
|
+
if (this.frameObserver) {
|
|
38
|
+
this.frameObserver.disconnect();
|
|
39
|
+
this.frameObserver = undefined;
|
|
40
|
+
}
|
|
41
|
+
if (this.mutationObserver) {
|
|
42
|
+
this.mutationObserver.disconnect();
|
|
43
|
+
this.mutationObserver = undefined;
|
|
44
|
+
}
|
|
45
|
+
if (this.styleObserver) {
|
|
46
|
+
this.styleObserver.disconnect();
|
|
47
|
+
this.styleObserver = undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 观察帧性能(用于检测重排重绘)
|
|
52
|
+
*/
|
|
53
|
+
observeFrameTiming() {
|
|
54
|
+
if (!('PerformanceObserver' in window)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
this.frameObserver = new PerformanceObserver((list) => {
|
|
59
|
+
for (const entry of list.getEntries()) {
|
|
60
|
+
if (entry.entryType === 'measure' && entry.name.includes('reflow')) {
|
|
61
|
+
// 自定义的 reflow 测量
|
|
62
|
+
this.recordReflow(entry);
|
|
63
|
+
}
|
|
64
|
+
else if (entry.entryType === 'measure' && entry.name.includes('repaint')) {
|
|
65
|
+
// 自定义的 repaint 测量
|
|
66
|
+
this.recordRepaint(entry);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// 观察 measure 类型(用于检测自定义标记的重排重绘)
|
|
71
|
+
try {
|
|
72
|
+
this.frameObserver.observe({
|
|
73
|
+
entryTypes: ['measure'],
|
|
74
|
+
buffered: true
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
// 某些浏览器可能不支持 entryTypes,尝试使用 type
|
|
79
|
+
try {
|
|
80
|
+
this.frameObserver.observe({
|
|
81
|
+
type: 'measure',
|
|
82
|
+
buffered: true
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (e2) {
|
|
86
|
+
// 如果都不支持,忽略
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
// 浏览器不支持或已触发
|
|
92
|
+
console.warn('Frame observer not supported:', e);
|
|
93
|
+
}
|
|
94
|
+
// 使用 requestAnimationFrame 检测帧率变化(间接检测重排重绘)
|
|
95
|
+
this.monitorFrameRate();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 监控帧率变化(用于检测性能问题)
|
|
99
|
+
*/
|
|
100
|
+
monitorFrameRate() {
|
|
101
|
+
let lastTime = performance.now();
|
|
102
|
+
let frameCount = 0;
|
|
103
|
+
let droppedFrames = 0;
|
|
104
|
+
const checkFrame = (currentTime) => {
|
|
105
|
+
if (!this.isMonitoring) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
frameCount++;
|
|
109
|
+
const deltaTime = currentTime - lastTime;
|
|
110
|
+
// 如果帧间隔超过 20ms(正常应该是 16.67ms),可能是重排重绘导致的
|
|
111
|
+
if (deltaTime > 20) {
|
|
112
|
+
droppedFrames += Math.floor((deltaTime - 16.67) / 16.67);
|
|
113
|
+
// 如果连续掉帧,记录为潜在的重排重绘问题
|
|
114
|
+
if (droppedFrames > 2) {
|
|
115
|
+
this.recordPotentialReflow(deltaTime, droppedFrames);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
droppedFrames = 0;
|
|
120
|
+
}
|
|
121
|
+
lastTime = currentTime;
|
|
122
|
+
requestAnimationFrame(checkFrame);
|
|
123
|
+
};
|
|
124
|
+
requestAnimationFrame(checkFrame);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 记录潜在的重排
|
|
128
|
+
* 优化:限制记录数量,避免内存溢出
|
|
129
|
+
*/
|
|
130
|
+
recordPotentialReflow(deltaTime, droppedFrames) {
|
|
131
|
+
// 限制重排记录数量,避免内存溢出
|
|
132
|
+
const MAX_REFLOW_RECORDS = 1000;
|
|
133
|
+
if (this.reflows.length >= MAX_REFLOW_RECORDS) {
|
|
134
|
+
return; // 已达到最大记录数,不再记录
|
|
135
|
+
}
|
|
136
|
+
// 获取当前可能导致重排的元素(通过检查最近修改的DOM)
|
|
137
|
+
const suspiciousElements = Array.from(this.reflowDetector.suspiciousElements.entries())
|
|
138
|
+
.sort((a, b) => b[1] - a[1])
|
|
139
|
+
.slice(0, 5); // 只取前5个最可疑的元素
|
|
140
|
+
if (suspiciousElements.length > 0) {
|
|
141
|
+
suspiciousElements.forEach(([element, count]) => {
|
|
142
|
+
const reflowInfo = {
|
|
143
|
+
timestamp: performance.now(),
|
|
144
|
+
duration: deltaTime,
|
|
145
|
+
element: element,
|
|
146
|
+
elementPath: getElementPath(element),
|
|
147
|
+
reason: 'frame_drop',
|
|
148
|
+
droppedFrames: droppedFrames,
|
|
149
|
+
stackTrace: this.getStackTrace(),
|
|
150
|
+
};
|
|
151
|
+
this.reflows.push(reflowInfo);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 观察DOM变化(用于检测可能导致重排的操作)
|
|
157
|
+
*/
|
|
158
|
+
observeDOMChanges() {
|
|
159
|
+
if (!('MutationObserver' in window)) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.mutationObserver = new MutationObserver((mutations) => {
|
|
163
|
+
mutations.forEach((mutation) => {
|
|
164
|
+
// 检测可能导致重排的DOM操作
|
|
165
|
+
if (mutation.type === 'childList' || mutation.type === 'attributes') {
|
|
166
|
+
const target = mutation.target;
|
|
167
|
+
// 记录可疑元素
|
|
168
|
+
const count = this.reflowDetector.suspiciousElements.get(target) || 0;
|
|
169
|
+
this.reflowDetector.suspiciousElements.set(target, count + 1);
|
|
170
|
+
// 清理旧记录(只保留最近5秒的)
|
|
171
|
+
setTimeout(() => {
|
|
172
|
+
this.reflowDetector.suspiciousElements.delete(target);
|
|
173
|
+
}, 5000);
|
|
174
|
+
// 如果修改了可能导致重排的属性
|
|
175
|
+
if (mutation.type === 'attributes') {
|
|
176
|
+
const attrName = mutation.attributeName;
|
|
177
|
+
if (attrName && this.isReflowTriggeringAttribute(attrName)) {
|
|
178
|
+
this.recordReflowFromMutation(target, attrName);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
this.mutationObserver.observe(document.body, {
|
|
185
|
+
childList: true,
|
|
186
|
+
subtree: true,
|
|
187
|
+
attributes: true,
|
|
188
|
+
attributeFilter: ['style', 'class', 'width', 'height', 'display', 'position'],
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* 观察样式变化(用于检测可能导致重绘的操作)
|
|
193
|
+
*/
|
|
194
|
+
observeStyleChanges() {
|
|
195
|
+
if (!('MutationObserver' in window)) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// 使用 Performance API 的 mark/measure 来检测样式计算
|
|
199
|
+
// 通过拦截样式读取来检测重绘
|
|
200
|
+
this.interceptStyleReads();
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* 拦截样式读取(用于检测重绘)
|
|
204
|
+
* 注意:这个方法会修改全局的 getComputedStyle,需要谨慎使用
|
|
205
|
+
*/
|
|
206
|
+
interceptStyleReads() {
|
|
207
|
+
// 通过代理 getComputedStyle 来检测频繁的样式读取
|
|
208
|
+
// 注意:这个方法可能会影响性能,建议在开发环境使用
|
|
209
|
+
// 检查是否在生产环境(通过检查是否有 source map 或其他方式)
|
|
210
|
+
const self = this;
|
|
211
|
+
const originalGetComputedStyle = window.getComputedStyle;
|
|
212
|
+
let readCount = 0;
|
|
213
|
+
let lastReadTime = 0;
|
|
214
|
+
let readElements = new Map();
|
|
215
|
+
window.getComputedStyle = function (element, ...args) {
|
|
216
|
+
const now = performance.now();
|
|
217
|
+
readCount++;
|
|
218
|
+
// 记录元素读取次数
|
|
219
|
+
const elementReadCount = readElements.get(element) || 0;
|
|
220
|
+
readElements.set(element, elementReadCount + 1);
|
|
221
|
+
// 如果频繁读取样式(可能是重绘导致的)
|
|
222
|
+
if (now - lastReadTime < 10 && readCount > 5) {
|
|
223
|
+
// 记录潜在的重绘
|
|
224
|
+
const frequentElement = Array.from(readElements.entries())
|
|
225
|
+
.find(([_, count]) => count > 3);
|
|
226
|
+
if (frequentElement) {
|
|
227
|
+
self.markRepaint(frequentElement[0], 'frequent_style_read');
|
|
228
|
+
}
|
|
229
|
+
// 重置计数器
|
|
230
|
+
readCount = 0;
|
|
231
|
+
readElements.clear();
|
|
232
|
+
}
|
|
233
|
+
// 清理旧记录
|
|
234
|
+
if (now - lastReadTime > 100) {
|
|
235
|
+
readElements.clear();
|
|
236
|
+
}
|
|
237
|
+
lastReadTime = now;
|
|
238
|
+
return originalGetComputedStyle.call(window, element, ...args);
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* 检测GPU加速
|
|
243
|
+
* 优化:使用 requestIdleCallback 和分批处理,避免阻塞主线程
|
|
244
|
+
*/
|
|
245
|
+
detectGPUAcceleration() {
|
|
246
|
+
// 延迟执行,等待页面加载完成
|
|
247
|
+
const detectInIdle = () => {
|
|
248
|
+
if ('requestIdleCallback' in window) {
|
|
249
|
+
requestIdleCallback(() => {
|
|
250
|
+
this.detectGPUAccelerationSelectively();
|
|
251
|
+
}, { timeout: 2000 });
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// 降级:延迟执行
|
|
255
|
+
setTimeout(() => {
|
|
256
|
+
this.detectGPUAccelerationSelectively();
|
|
257
|
+
}, 1000);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
// 如果页面已加载完成,立即检测;否则等待加载完成
|
|
261
|
+
if (document.readyState === 'complete') {
|
|
262
|
+
detectInIdle();
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
window.addEventListener('load', detectInIdle, { once: true });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* 检测GPU加速(检测所有元素)
|
|
270
|
+
* 优化:使用分批处理,避免阻塞主线程
|
|
271
|
+
*/
|
|
272
|
+
detectGPUAccelerationSelectively() {
|
|
273
|
+
// 直接获取所有元素
|
|
274
|
+
const allElements = document.querySelectorAll('*');
|
|
275
|
+
const candidates = [];
|
|
276
|
+
// 不应该检测的元素标签(这些元素通常不需要GPU加速)
|
|
277
|
+
const excludeTags = new Set([
|
|
278
|
+
'META', 'LINK', 'SCRIPT', 'STYLE', 'TITLE', 'HEAD', 'HTML', 'BODY',
|
|
279
|
+
'NOSCRIPT', 'TEMPLATE', 'SVG', 'MATH'
|
|
280
|
+
]);
|
|
281
|
+
// 过滤出 HTMLElement 类型的元素,并排除不需要检测的元素
|
|
282
|
+
allElements.forEach((element) => {
|
|
283
|
+
if (element instanceof HTMLElement) {
|
|
284
|
+
// 排除不应该检测的元素
|
|
285
|
+
if (!excludeTags.has(element.tagName)) {
|
|
286
|
+
candidates.push(element);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
// 分批处理,避免阻塞主线程
|
|
291
|
+
this.processGPUAccelerationBatch(candidates, 0);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* 分批处理GPU加速检测
|
|
295
|
+
*/
|
|
296
|
+
processGPUAccelerationBatch(candidates, index) {
|
|
297
|
+
const batchSize = 50; // 每批处理50个元素
|
|
298
|
+
const endIndex = Math.min(index + batchSize, candidates.length);
|
|
299
|
+
for (let i = index; i < endIndex; i++) {
|
|
300
|
+
const element = candidates[i];
|
|
301
|
+
try {
|
|
302
|
+
const computedStyle = window.getComputedStyle(element);
|
|
303
|
+
const gpuInfo = this.checkGPUAcceleration(element, computedStyle);
|
|
304
|
+
if (gpuInfo.isAccelerated) {
|
|
305
|
+
this.gpuAcceleratedElements.push(gpuInfo);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch (e) {
|
|
309
|
+
// 忽略错误,继续处理下一个
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// 如果还有剩余元素,继续处理
|
|
313
|
+
if (endIndex < candidates.length) {
|
|
314
|
+
// 使用 requestIdleCallback 或 setTimeout 继续处理
|
|
315
|
+
if ('requestIdleCallback' in window) {
|
|
316
|
+
requestIdleCallback(() => {
|
|
317
|
+
this.processGPUAccelerationBatch(candidates, endIndex);
|
|
318
|
+
}, { timeout: 1000 });
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
setTimeout(() => {
|
|
322
|
+
this.processGPUAccelerationBatch(candidates, endIndex);
|
|
323
|
+
}, 0);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* 检查元素是否使用GPU加速
|
|
329
|
+
*/
|
|
330
|
+
checkGPUAcceleration(element, computedStyle) {
|
|
331
|
+
const info = {
|
|
332
|
+
element: element,
|
|
333
|
+
elementPath: getElementPath(element),
|
|
334
|
+
isAccelerated: false,
|
|
335
|
+
accelerationMethods: [],
|
|
336
|
+
willChange: false,
|
|
337
|
+
transform: false,
|
|
338
|
+
opacity: false,
|
|
339
|
+
filter: false,
|
|
340
|
+
backdropFilter: false,
|
|
341
|
+
};
|
|
342
|
+
// 1. 检查 will-change(明确声明需要GPU加速)
|
|
343
|
+
const willChange = computedStyle.willChange;
|
|
344
|
+
if (willChange && willChange !== 'auto' && willChange.trim() !== '') {
|
|
345
|
+
info.willChange = true;
|
|
346
|
+
info.isAccelerated = true;
|
|
347
|
+
info.accelerationMethods.push('will-change');
|
|
348
|
+
}
|
|
349
|
+
// 2. 检查 transform(只有非 'none' 时才使用GPU加速)
|
|
350
|
+
const transform = computedStyle.transform;
|
|
351
|
+
const transitionProperty = computedStyle.transitionProperty;
|
|
352
|
+
const transitionDuration = computedStyle.transitionDuration;
|
|
353
|
+
// 检查是否有 transform 过渡动画(即使当前 transform 是默认值,transition: transform 也会使用GPU加速)
|
|
354
|
+
const hasTransformTransition = transitionProperty &&
|
|
355
|
+
transitionProperty !== 'none' &&
|
|
356
|
+
parseFloat(transitionDuration) > 0 &&
|
|
357
|
+
(transitionProperty.includes('transform') || transitionProperty === 'all');
|
|
358
|
+
if (transform && transform !== 'none' && transform !== 'matrix(1, 0, 0, 1, 0, 0)') {
|
|
359
|
+
info.transform = true;
|
|
360
|
+
info.isAccelerated = true;
|
|
361
|
+
info.accelerationMethods.push('transform');
|
|
362
|
+
// 检查3D变换(强制GPU加速)
|
|
363
|
+
if (transform.includes('translate3d') ||
|
|
364
|
+
transform.includes('translateZ') ||
|
|
365
|
+
transform.includes('perspective') ||
|
|
366
|
+
transform.includes('matrix3d')) {
|
|
367
|
+
if (!info.accelerationMethods.includes('transform-3d')) {
|
|
368
|
+
info.accelerationMethods.push('transform-3d');
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else if (hasTransformTransition) {
|
|
373
|
+
// 即使当前 transform 是默认值,如果有 transition: transform,也认为使用了GPU加速
|
|
374
|
+
info.transform = true;
|
|
375
|
+
info.isAccelerated = true;
|
|
376
|
+
info.accelerationMethods.push('transform-transition');
|
|
377
|
+
}
|
|
378
|
+
// 3. 检查 opacity(只有小于 1 时才真正使用GPU加速)
|
|
379
|
+
const opacityValue = parseFloat(computedStyle.opacity);
|
|
380
|
+
if (!isNaN(opacityValue) && opacityValue < 1 && opacityValue >= 0) {
|
|
381
|
+
info.opacity = true;
|
|
382
|
+
info.isAccelerated = true;
|
|
383
|
+
info.accelerationMethods.push('opacity');
|
|
384
|
+
}
|
|
385
|
+
// 4. 检查 filter(只有非 'none' 时才使用GPU加速)
|
|
386
|
+
const filter = computedStyle.filter;
|
|
387
|
+
if (filter && filter !== 'none' && filter.trim() !== '') {
|
|
388
|
+
info.filter = true;
|
|
389
|
+
info.isAccelerated = true;
|
|
390
|
+
info.accelerationMethods.push('filter');
|
|
391
|
+
}
|
|
392
|
+
// 5. 检查 backdrop-filter(只有非 'none' 时才使用GPU加速)
|
|
393
|
+
const backdropFilter = computedStyle.backdropFilter;
|
|
394
|
+
if (backdropFilter && backdropFilter !== 'none' && backdropFilter.trim() !== '') {
|
|
395
|
+
info.backdropFilter = true;
|
|
396
|
+
info.isAccelerated = true;
|
|
397
|
+
info.accelerationMethods.push('backdrop-filter');
|
|
398
|
+
}
|
|
399
|
+
return info;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* 判断属性是否可能导致重排
|
|
403
|
+
*/
|
|
404
|
+
isReflowTriggeringAttribute(attrName) {
|
|
405
|
+
if (!attrName) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
// 这些属性的改变会导致重排
|
|
409
|
+
const reflowAttributes = [
|
|
410
|
+
'width', 'height', 'padding', 'margin', 'border',
|
|
411
|
+
'display', 'position', 'top', 'left', 'right', 'bottom',
|
|
412
|
+
'font-size', 'font-weight', 'line-height',
|
|
413
|
+
'overflow', 'float', 'clear',
|
|
414
|
+
];
|
|
415
|
+
return reflowAttributes.some(attr => attrName.includes(attr));
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* 从Mutation记录重排
|
|
419
|
+
*/
|
|
420
|
+
recordReflowFromMutation(element, attrName) {
|
|
421
|
+
const reflowInfo = {
|
|
422
|
+
timestamp: performance.now(),
|
|
423
|
+
duration: 0, // 无法准确测量,设为0
|
|
424
|
+
element: element,
|
|
425
|
+
elementPath: getElementPath(element),
|
|
426
|
+
reason: `attribute_change:${attrName}`,
|
|
427
|
+
stackTrace: this.getStackTrace(),
|
|
428
|
+
};
|
|
429
|
+
this.reflows.push(reflowInfo);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* 记录重排
|
|
433
|
+
*/
|
|
434
|
+
recordReflow(entry) {
|
|
435
|
+
const reflowInfo = {
|
|
436
|
+
timestamp: entry.startTime,
|
|
437
|
+
duration: entry.duration,
|
|
438
|
+
reason: 'measured',
|
|
439
|
+
stackTrace: this.getStackTrace(),
|
|
440
|
+
};
|
|
441
|
+
this.reflows.push(reflowInfo);
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* 记录重绘
|
|
445
|
+
*/
|
|
446
|
+
recordRepaint(entry) {
|
|
447
|
+
const repaintInfo = {
|
|
448
|
+
timestamp: entry.startTime,
|
|
449
|
+
duration: entry.duration,
|
|
450
|
+
reason: 'measured',
|
|
451
|
+
stackTrace: this.getStackTrace(),
|
|
452
|
+
};
|
|
453
|
+
this.repaints.push(repaintInfo);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* 获取堆栈跟踪
|
|
457
|
+
* 优化:只在需要时获取,避免频繁调用影响性能
|
|
458
|
+
*/
|
|
459
|
+
getStackTrace() {
|
|
460
|
+
// 在生产环境或大量数据时,可以跳过堆栈跟踪以提升性能
|
|
461
|
+
// 可以通过环境变量或配置控制
|
|
462
|
+
if (this.reflows.length > 1000 || this.repaints.length > 1000) {
|
|
463
|
+
return ''; // 数据量太大时跳过堆栈跟踪
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
throw new Error();
|
|
467
|
+
}
|
|
468
|
+
catch (e) {
|
|
469
|
+
return e.stack || '';
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* 手动标记重排(供外部调用)
|
|
474
|
+
*/
|
|
475
|
+
markReflow(element, reason) {
|
|
476
|
+
performance.mark('reflow-start');
|
|
477
|
+
// 使用 setTimeout 0 来测量重排时间
|
|
478
|
+
setTimeout(() => {
|
|
479
|
+
performance.mark('reflow-end');
|
|
480
|
+
performance.measure('reflow', 'reflow-start', 'reflow-end');
|
|
481
|
+
const measure = performance.getEntriesByName('reflow', 'measure')[0];
|
|
482
|
+
if (measure) {
|
|
483
|
+
const reflowInfo = {
|
|
484
|
+
timestamp: measure.startTime,
|
|
485
|
+
duration: measure.duration,
|
|
486
|
+
element: element,
|
|
487
|
+
elementPath: element ? getElementPath(element) : undefined,
|
|
488
|
+
reason: reason || 'manual',
|
|
489
|
+
stackTrace: this.getStackTrace(),
|
|
490
|
+
};
|
|
491
|
+
this.reflows.push(reflowInfo);
|
|
492
|
+
}
|
|
493
|
+
}, 0);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* 手动标记重绘(供外部调用)
|
|
497
|
+
*/
|
|
498
|
+
markRepaint(element, reason) {
|
|
499
|
+
performance.mark('repaint-start');
|
|
500
|
+
setTimeout(() => {
|
|
501
|
+
performance.mark('repaint-end');
|
|
502
|
+
performance.measure('repaint', 'repaint-start', 'repaint-end');
|
|
503
|
+
const measure = performance.getEntriesByName('repaint', 'measure')[0];
|
|
504
|
+
if (measure) {
|
|
505
|
+
const repaintInfo = {
|
|
506
|
+
timestamp: measure.startTime,
|
|
507
|
+
duration: measure.duration,
|
|
508
|
+
element: element,
|
|
509
|
+
elementPath: element ? getElementPath(element) : undefined,
|
|
510
|
+
reason: reason || 'manual',
|
|
511
|
+
stackTrace: this.getStackTrace(),
|
|
512
|
+
};
|
|
513
|
+
this.repaints.push(repaintInfo);
|
|
514
|
+
}
|
|
515
|
+
}, 0);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* 获取渲染性能指标
|
|
519
|
+
*/
|
|
520
|
+
getMetrics() {
|
|
521
|
+
return {
|
|
522
|
+
reflowCount: this.reflows.length,
|
|
523
|
+
repaintCount: this.repaints.length,
|
|
524
|
+
reflows: this.reflows,
|
|
525
|
+
repaints: this.repaints,
|
|
526
|
+
gpuAcceleratedCount: this.gpuAcceleratedElements.length,
|
|
527
|
+
gpuAcceleratedElements: this.gpuAcceleratedElements,
|
|
528
|
+
totalReflowTime: this.reflows.reduce((sum, r) => sum + r.duration, 0),
|
|
529
|
+
totalRepaintTime: this.repaints.reduce((sum, r) => sum + r.duration, 0),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* 获取频繁重排的元素
|
|
534
|
+
* 优化:使用 Map 和单次遍历,时间复杂度 O(n)
|
|
535
|
+
*/
|
|
536
|
+
getFrequentReflowElements(threshold = 3) {
|
|
537
|
+
const elementCounts = new Map();
|
|
538
|
+
// 单次遍历,统计每个元素的重排次数
|
|
539
|
+
for (let i = 0; i < this.reflows.length; i++) {
|
|
540
|
+
const reflow = this.reflows[i];
|
|
541
|
+
if (reflow.element && reflow.elementPath) {
|
|
542
|
+
const existing = elementCounts.get(reflow.elementPath);
|
|
543
|
+
if (existing) {
|
|
544
|
+
existing.count++;
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
elementCounts.set(reflow.elementPath, {
|
|
548
|
+
element: reflow.element,
|
|
549
|
+
count: 1,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// 过滤、映射、排序
|
|
555
|
+
return Array.from(elementCounts.entries())
|
|
556
|
+
.filter(([_, data]) => data.count >= threshold)
|
|
557
|
+
.map(([path, data]) => ({
|
|
558
|
+
element: data.element,
|
|
559
|
+
path,
|
|
560
|
+
count: data.count,
|
|
561
|
+
}))
|
|
562
|
+
.sort((a, b) => b.count - a.count);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* 获取未使用GPU加速但应该使用的元素
|
|
566
|
+
* 这些元素通常是动画元素或频繁更新的元素
|
|
567
|
+
* 直接检测所有元素
|
|
568
|
+
*/
|
|
569
|
+
getElementsNeedingGPUAcceleration() {
|
|
570
|
+
const candidates = [];
|
|
571
|
+
// 不应该检测的元素标签(这些元素通常不需要GPU加速)
|
|
572
|
+
const excludeTags = new Set([
|
|
573
|
+
'META', 'LINK', 'SCRIPT', 'STYLE', 'TITLE', 'HEAD', 'HTML', 'BODY',
|
|
574
|
+
'NOSCRIPT', 'TEMPLATE', 'SVG', 'MATH'
|
|
575
|
+
]);
|
|
576
|
+
// 直接检测所有元素
|
|
577
|
+
const allElements = document.querySelectorAll('*');
|
|
578
|
+
allElements.forEach((element) => {
|
|
579
|
+
if (element instanceof HTMLElement) {
|
|
580
|
+
// 排除不应该检测的元素
|
|
581
|
+
if (excludeTags.has(element.tagName)) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
const computedStyle = window.getComputedStyle(element);
|
|
586
|
+
// 检查是否有真正的动画(duration > 0)
|
|
587
|
+
const hasRealAnimation = this.hasRealAnimation(computedStyle);
|
|
588
|
+
if (hasRealAnimation) {
|
|
589
|
+
const gpuInfo = this.checkGPUAcceleration(element, computedStyle);
|
|
590
|
+
if (!gpuInfo.isAccelerated) {
|
|
591
|
+
candidates.push({
|
|
592
|
+
element,
|
|
593
|
+
path: getElementPath(element),
|
|
594
|
+
reason: 'has_animation',
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
catch (e) {
|
|
600
|
+
// 忽略错误,继续处理下一个
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
// 查找频繁重排的元素
|
|
605
|
+
const frequentReflowElements = this.getFrequentReflowElements(5);
|
|
606
|
+
frequentReflowElements.forEach(({ element, path }) => {
|
|
607
|
+
if (element instanceof HTMLElement) {
|
|
608
|
+
// 排除不应该检测的元素
|
|
609
|
+
if (excludeTags.has(element.tagName)) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
const computedStyle = window.getComputedStyle(element);
|
|
614
|
+
const gpuInfo = this.checkGPUAcceleration(element, computedStyle);
|
|
615
|
+
if (!gpuInfo.isAccelerated) {
|
|
616
|
+
// 避免重复添加
|
|
617
|
+
if (!candidates.some(c => c.element === element)) {
|
|
618
|
+
candidates.push({
|
|
619
|
+
element,
|
|
620
|
+
path,
|
|
621
|
+
reason: 'frequent_reflow',
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch (e) {
|
|
627
|
+
// 忽略错误
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
return candidates;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* 检查元素是否有真正的动画(duration > 0)
|
|
635
|
+
*/
|
|
636
|
+
hasRealAnimation(computedStyle) {
|
|
637
|
+
// 检查 animation
|
|
638
|
+
const animationName = computedStyle.animationName;
|
|
639
|
+
const animationDuration = computedStyle.animationDuration;
|
|
640
|
+
if (animationName && animationName !== 'none') {
|
|
641
|
+
// 解析 animationDuration(可能是多个值,用逗号分隔)
|
|
642
|
+
const durations = animationDuration.split(',').map(d => parseFloat(d.trim()));
|
|
643
|
+
// 只要有一个 duration > 0,就认为有动画
|
|
644
|
+
if (durations.some(d => !isNaN(d) && d > 0)) {
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
// 检查 transition
|
|
649
|
+
const transitionProperty = computedStyle.transitionProperty;
|
|
650
|
+
const transitionDuration = computedStyle.transitionDuration;
|
|
651
|
+
if (transitionProperty && transitionProperty !== 'none') {
|
|
652
|
+
// 解析 transitionDuration(可能是多个值,用逗号分隔)
|
|
653
|
+
const durations = transitionDuration.split(',').map(d => parseFloat(d.trim()));
|
|
654
|
+
// 只要有一个 duration > 0,就认为有过渡动画
|
|
655
|
+
if (durations.some(d => !isNaN(d) && d > 0)) {
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* 清理数据
|
|
663
|
+
*/
|
|
664
|
+
clear() {
|
|
665
|
+
this.reflows = [];
|
|
666
|
+
this.repaints = [];
|
|
667
|
+
this.gpuAcceleratedElements = [];
|
|
668
|
+
this.reflowDetector.suspiciousElements.clear();
|
|
669
|
+
}
|
|
670
|
+
}
|
package/dist/core/reporter.d.ts
CHANGED
package/dist/core/reporter.js
CHANGED
|
@@ -49,6 +49,11 @@ export class Reporter {
|
|
|
49
49
|
* 发送数据
|
|
50
50
|
*/
|
|
51
51
|
send(data) {
|
|
52
|
+
// 如果没有 reportUrl,将数据打印到控制台
|
|
53
|
+
if (!this.reportUrl) {
|
|
54
|
+
console.log('📊 Performance Helper Report:', data);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
52
57
|
// 使用 sendBeacon 优先,fallback 到 fetch
|
|
53
58
|
if (navigator.sendBeacon) {
|
|
54
59
|
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
|
|
@@ -86,9 +91,15 @@ export class Reporter {
|
|
|
86
91
|
setupBeforeUnload() {
|
|
87
92
|
window.addEventListener('beforeunload', () => {
|
|
88
93
|
if (this.queue.length > 0) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
if (this.reportUrl) {
|
|
95
|
+
// 使用 sendBeacon 确保数据能发送
|
|
96
|
+
const blob = new Blob([JSON.stringify(this.queue)], { type: 'application/json' });
|
|
97
|
+
navigator.sendBeacon(this.reportUrl, blob);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// 如果没有 reportUrl,打印到控制台
|
|
101
|
+
console.log('📊 Performance Helper Report (beforeunload):', this.queue);
|
|
102
|
+
}
|
|
92
103
|
}
|
|
93
104
|
});
|
|
94
105
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PerformanceHelperOptions, PerformanceMetrics, ResourceInfo, ErrorInfo, ReportData, LongTaskInfo, LongTaskAttribution } from './types';
|
|
1
|
+
import { PerformanceHelperOptions, PerformanceMetrics, ResourceInfo, ErrorInfo, ReportData, LongTaskInfo, LongTaskAttribution, RenderMetrics, ReflowInfo, RepaintInfo, GPUAccelerationInfo } from './types';
|
|
2
2
|
import { HighlightOptions } from './utils/index';
|
|
3
3
|
/**
|
|
4
4
|
* 性能监控 SDK
|
|
@@ -9,6 +9,7 @@ export declare class PerformanceHelper {
|
|
|
9
9
|
private performanceCollector;
|
|
10
10
|
private resourceMonitor;
|
|
11
11
|
private errorMonitor;
|
|
12
|
+
private renderMonitor;
|
|
12
13
|
private initialized;
|
|
13
14
|
constructor(options: PerformanceHelperOptions);
|
|
14
15
|
/**
|
|
@@ -58,10 +59,66 @@ export declare class PerformanceHelper {
|
|
|
58
59
|
* 上报自定义数据
|
|
59
60
|
*/
|
|
60
61
|
reportCustom(type: string, data: any): void;
|
|
62
|
+
/**
|
|
63
|
+
* 获取渲染性能指标
|
|
64
|
+
*/
|
|
65
|
+
getRenderMetrics(): RenderMetrics;
|
|
66
|
+
/**
|
|
67
|
+
* 获取频繁重排的元素
|
|
68
|
+
* @param threshold 阈值,默认3次
|
|
69
|
+
*/
|
|
70
|
+
getFrequentReflowElements(threshold?: number): Array<{
|
|
71
|
+
element: Element;
|
|
72
|
+
path: string;
|
|
73
|
+
count: number;
|
|
74
|
+
}>;
|
|
75
|
+
/**
|
|
76
|
+
* 获取需要GPU加速但未使用的元素
|
|
77
|
+
*/
|
|
78
|
+
getElementsNeedingGPUAcceleration(): Array<{
|
|
79
|
+
element: HTMLElement;
|
|
80
|
+
path: string;
|
|
81
|
+
reason: string;
|
|
82
|
+
}>;
|
|
83
|
+
/**
|
|
84
|
+
* 手动标记重排
|
|
85
|
+
* @param element 相关元素
|
|
86
|
+
* @param reason 原因
|
|
87
|
+
*/
|
|
88
|
+
markReflow(element?: Element, reason?: string): void;
|
|
89
|
+
/**
|
|
90
|
+
* 手动标记重绘
|
|
91
|
+
* @param element 相关元素
|
|
92
|
+
* @param reason 原因
|
|
93
|
+
*/
|
|
94
|
+
markRepaint(element?: Element, reason?: string): void;
|
|
95
|
+
/**
|
|
96
|
+
* 高亮频繁重排的元素(用于调试)
|
|
97
|
+
* @param threshold 阈值,默认3次
|
|
98
|
+
* @param options 高亮选项
|
|
99
|
+
*/
|
|
100
|
+
highlightFrequentReflowElements(threshold?: number, options?: HighlightOptions): number;
|
|
101
|
+
/**
|
|
102
|
+
* 高亮需要GPU加速的元素(用于调试)
|
|
103
|
+
* @param options 高亮选项
|
|
104
|
+
*/
|
|
105
|
+
highlightElementsNeedingGPUAcceleration(options?: HighlightOptions): number;
|
|
106
|
+
/**
|
|
107
|
+
* 上报渲染性能数据
|
|
108
|
+
*/
|
|
109
|
+
reportRenderMetrics(): void;
|
|
110
|
+
/**
|
|
111
|
+
* 停止渲染性能监控
|
|
112
|
+
*/
|
|
113
|
+
stopRenderMonitoring(): void;
|
|
114
|
+
/**
|
|
115
|
+
* 清理渲染性能数据
|
|
116
|
+
*/
|
|
117
|
+
clearRenderMetrics(): void;
|
|
61
118
|
/**
|
|
62
119
|
* 销毁 SDK
|
|
63
120
|
*/
|
|
64
121
|
destroy(): void;
|
|
65
122
|
}
|
|
66
|
-
export type { PerformanceHelperOptions, PerformanceMetrics, ResourceInfo, ErrorInfo, ReportData, LongTaskInfo, LongTaskAttribution };
|
|
123
|
+
export type { PerformanceHelperOptions, PerformanceMetrics, ResourceInfo, ErrorInfo, ReportData, LongTaskInfo, LongTaskAttribution, RenderMetrics, ReflowInfo, RepaintInfo, GPUAccelerationInfo };
|
|
67
124
|
export default PerformanceHelper;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PerformanceCollector } from './core/performance';
|
|
2
2
|
import { ResourceMonitor } from './core/resource';
|
|
3
3
|
import { ErrorMonitor } from './core/error';
|
|
4
|
+
import { RenderMonitor } from './core/render';
|
|
4
5
|
import { Reporter } from './core/reporter';
|
|
5
6
|
import { querySelectorByPath, highlightElement, removeElementHighlight } from './utils/index';
|
|
6
7
|
/**
|
|
@@ -9,9 +10,6 @@ import { querySelectorByPath, highlightElement, removeElementHighlight } from '.
|
|
|
9
10
|
export class PerformanceHelper {
|
|
10
11
|
constructor(options) {
|
|
11
12
|
this.initialized = false;
|
|
12
|
-
if (!options.reportUrl) {
|
|
13
|
-
throw new Error('reportUrl is required');
|
|
14
|
-
}
|
|
15
13
|
this.options = {
|
|
16
14
|
immediate: false,
|
|
17
15
|
monitorResources: true,
|
|
@@ -24,6 +22,7 @@ export class PerformanceHelper {
|
|
|
24
22
|
this.performanceCollector = new PerformanceCollector();
|
|
25
23
|
this.resourceMonitor = new ResourceMonitor();
|
|
26
24
|
this.errorMonitor = new ErrorMonitor();
|
|
25
|
+
this.renderMonitor = new RenderMonitor();
|
|
27
26
|
// 立即初始化性能观察器(需要在页面加载前设置)
|
|
28
27
|
if (this.options.monitorPerformance) {
|
|
29
28
|
this.performanceCollector.init();
|
|
@@ -48,6 +47,8 @@ export class PerformanceHelper {
|
|
|
48
47
|
if (this.options.monitorErrors) {
|
|
49
48
|
this.errorMonitor.start();
|
|
50
49
|
}
|
|
50
|
+
// 启动渲染性能监控
|
|
51
|
+
this.renderMonitor.start();
|
|
51
52
|
if (this.options.monitorPerformance) {
|
|
52
53
|
// 等待页面加载完成后采集性能指标
|
|
53
54
|
if (document.readyState === 'complete') {
|
|
@@ -179,12 +180,113 @@ export class PerformanceHelper {
|
|
|
179
180
|
data: data,
|
|
180
181
|
});
|
|
181
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* 获取渲染性能指标
|
|
185
|
+
*/
|
|
186
|
+
getRenderMetrics() {
|
|
187
|
+
return this.renderMonitor.getMetrics();
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 获取频繁重排的元素
|
|
191
|
+
* @param threshold 阈值,默认3次
|
|
192
|
+
*/
|
|
193
|
+
getFrequentReflowElements(threshold) {
|
|
194
|
+
return this.renderMonitor.getFrequentReflowElements(threshold);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* 获取需要GPU加速但未使用的元素
|
|
198
|
+
*/
|
|
199
|
+
getElementsNeedingGPUAcceleration() {
|
|
200
|
+
return this.renderMonitor.getElementsNeedingGPUAcceleration();
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* 手动标记重排
|
|
204
|
+
* @param element 相关元素
|
|
205
|
+
* @param reason 原因
|
|
206
|
+
*/
|
|
207
|
+
markReflow(element, reason) {
|
|
208
|
+
this.renderMonitor.markReflow(element, reason);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* 手动标记重绘
|
|
212
|
+
* @param element 相关元素
|
|
213
|
+
* @param reason 原因
|
|
214
|
+
*/
|
|
215
|
+
markRepaint(element, reason) {
|
|
216
|
+
this.renderMonitor.markRepaint(element, reason);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* 高亮频繁重排的元素(用于调试)
|
|
220
|
+
* @param threshold 阈值,默认3次
|
|
221
|
+
* @param options 高亮选项
|
|
222
|
+
*/
|
|
223
|
+
highlightFrequentReflowElements(threshold, options) {
|
|
224
|
+
const frequentElements = this.getFrequentReflowElements(threshold);
|
|
225
|
+
let highlightedCount = 0;
|
|
226
|
+
frequentElements.forEach(({ element }) => {
|
|
227
|
+
if (element instanceof HTMLElement) {
|
|
228
|
+
const removeHighlight = highlightElement(element, {
|
|
229
|
+
borderColor: '#ff0000',
|
|
230
|
+
backgroundColor: 'rgba(255, 0, 0, 0.1)',
|
|
231
|
+
...options,
|
|
232
|
+
});
|
|
233
|
+
if (removeHighlight) {
|
|
234
|
+
highlightedCount++;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
console.log(`Highlighted ${highlightedCount} frequent reflow elements`);
|
|
239
|
+
return highlightedCount;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* 高亮需要GPU加速的元素(用于调试)
|
|
243
|
+
* @param options 高亮选项
|
|
244
|
+
*/
|
|
245
|
+
highlightElementsNeedingGPUAcceleration(options) {
|
|
246
|
+
const elements = this.getElementsNeedingGPUAcceleration();
|
|
247
|
+
let highlightedCount = 0;
|
|
248
|
+
elements.forEach(({ element }) => {
|
|
249
|
+
const removeHighlight = highlightElement(element, {
|
|
250
|
+
borderColor: '#ffa500',
|
|
251
|
+
backgroundColor: 'rgba(255, 165, 0, 0.1)',
|
|
252
|
+
...options,
|
|
253
|
+
});
|
|
254
|
+
if (removeHighlight) {
|
|
255
|
+
highlightedCount++;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
console.log(`Highlighted ${highlightedCount} elements needing GPU acceleration`);
|
|
259
|
+
return highlightedCount;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* 上报渲染性能数据
|
|
263
|
+
*/
|
|
264
|
+
reportRenderMetrics() {
|
|
265
|
+
const metrics = this.renderMonitor.getMetrics();
|
|
266
|
+
this.reporter.report({
|
|
267
|
+
type: 'render',
|
|
268
|
+
data: metrics,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* 停止渲染性能监控
|
|
273
|
+
*/
|
|
274
|
+
stopRenderMonitoring() {
|
|
275
|
+
this.renderMonitor.stop();
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 清理渲染性能数据
|
|
279
|
+
*/
|
|
280
|
+
clearRenderMetrics() {
|
|
281
|
+
this.renderMonitor.clear();
|
|
282
|
+
}
|
|
182
283
|
/**
|
|
183
284
|
* 销毁 SDK
|
|
184
285
|
*/
|
|
185
286
|
destroy() {
|
|
186
287
|
this.reporter.destroy();
|
|
187
288
|
this.performanceCollector.destroy();
|
|
289
|
+
this.renderMonitor.stop();
|
|
188
290
|
this.initialized = false;
|
|
189
291
|
}
|
|
190
292
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* SDK 配置选项
|
|
3
3
|
*/
|
|
4
4
|
export interface PerformanceHelperOptions {
|
|
5
|
-
/**
|
|
6
|
-
reportUrl
|
|
5
|
+
/** 上报地址,如果为空则将数据打印到控制台 */
|
|
6
|
+
reportUrl?: string;
|
|
7
7
|
/** 应用ID */
|
|
8
8
|
appId?: string;
|
|
9
9
|
/** 用户ID */
|
|
@@ -124,11 +124,91 @@ export interface ErrorInfo {
|
|
|
124
124
|
* 上报数据
|
|
125
125
|
*/
|
|
126
126
|
export interface ReportData {
|
|
127
|
-
type: 'performance' | 'resource' | 'error';
|
|
128
|
-
data: PerformanceMetrics | ResourceInfo | ErrorInfo;
|
|
127
|
+
type: 'performance' | 'resource' | 'error' | 'render';
|
|
128
|
+
data: PerformanceMetrics | ResourceInfo | ErrorInfo | RenderMetrics;
|
|
129
129
|
timestamp: number;
|
|
130
130
|
url: string;
|
|
131
131
|
userAgent: string;
|
|
132
132
|
appId?: string;
|
|
133
133
|
userId?: string;
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* 重排(Reflow)信息
|
|
137
|
+
*/
|
|
138
|
+
export interface ReflowInfo {
|
|
139
|
+
/** 时间戳(相对于页面加载) */
|
|
140
|
+
timestamp: number;
|
|
141
|
+
/** 持续时间(毫秒) */
|
|
142
|
+
duration: number;
|
|
143
|
+
/** 相关DOM元素 */
|
|
144
|
+
element?: Element;
|
|
145
|
+
/** 元素路径 */
|
|
146
|
+
elementPath?: string;
|
|
147
|
+
/** 重排原因 */
|
|
148
|
+
reason: string;
|
|
149
|
+
/** 掉帧数(如果是帧率下降导致) */
|
|
150
|
+
droppedFrames?: number;
|
|
151
|
+
/** 堆栈跟踪 */
|
|
152
|
+
stackTrace?: string;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* 重绘(Repaint)信息
|
|
156
|
+
*/
|
|
157
|
+
export interface RepaintInfo {
|
|
158
|
+
/** 时间戳(相对于页面加载) */
|
|
159
|
+
timestamp: number;
|
|
160
|
+
/** 持续时间(毫秒) */
|
|
161
|
+
duration: number;
|
|
162
|
+
/** 相关DOM元素 */
|
|
163
|
+
element?: Element;
|
|
164
|
+
/** 元素路径 */
|
|
165
|
+
elementPath?: string;
|
|
166
|
+
/** 重绘原因 */
|
|
167
|
+
reason: string;
|
|
168
|
+
/** 堆栈跟踪 */
|
|
169
|
+
stackTrace?: string;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* GPU加速信息
|
|
173
|
+
*/
|
|
174
|
+
export interface GPUAccelerationInfo {
|
|
175
|
+
/** DOM元素 */
|
|
176
|
+
element: HTMLElement;
|
|
177
|
+
/** 元素路径 */
|
|
178
|
+
elementPath: string;
|
|
179
|
+
/** 是否启用GPU加速 */
|
|
180
|
+
isAccelerated: boolean;
|
|
181
|
+
/** 加速方法列表 */
|
|
182
|
+
accelerationMethods: string[];
|
|
183
|
+
/** 是否使用 will-change */
|
|
184
|
+
willChange: boolean;
|
|
185
|
+
/** 是否使用 transform */
|
|
186
|
+
transform: boolean;
|
|
187
|
+
/** 是否使用 opacity */
|
|
188
|
+
opacity: boolean;
|
|
189
|
+
/** 是否使用 filter */
|
|
190
|
+
filter: boolean;
|
|
191
|
+
/** 是否使用 backdrop-filter */
|
|
192
|
+
backdropFilter: boolean;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* 渲染性能指标
|
|
196
|
+
*/
|
|
197
|
+
export interface RenderMetrics {
|
|
198
|
+
/** 重排次数 */
|
|
199
|
+
reflowCount: number;
|
|
200
|
+
/** 重绘次数 */
|
|
201
|
+
repaintCount: number;
|
|
202
|
+
/** 重排详情列表 */
|
|
203
|
+
reflows: ReflowInfo[];
|
|
204
|
+
/** 重绘详情列表 */
|
|
205
|
+
repaints: RepaintInfo[];
|
|
206
|
+
/** GPU加速元素数量 */
|
|
207
|
+
gpuAcceleratedCount: number;
|
|
208
|
+
/** GPU加速元素详情 */
|
|
209
|
+
gpuAcceleratedElements: GPUAccelerationInfo[];
|
|
210
|
+
/** 总重排时间(毫秒) */
|
|
211
|
+
totalReflowTime: number;
|
|
212
|
+
/** 总重绘时间(毫秒) */
|
|
213
|
+
totalRepaintTime: number;
|
|
214
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "performance-helper",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "前端性能监控 SDK",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
],
|
|
20
20
|
"author": "",
|
|
21
21
|
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/FangHaoming/performance-helper.git"
|
|
25
|
+
},
|
|
22
26
|
"devDependencies": {
|
|
23
27
|
"@types/node": "^20.10.0",
|
|
24
28
|
"typescript": "^5.3.3"
|