vite-plugin-ai-perf-analyzer 1.0.3 → 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/index.js +305 -266
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +254 -239
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import pc from 'picocolors';
|
|
2
|
-
import fs2 from 'fs';
|
|
3
|
-
import path3 from 'path';
|
|
4
|
-
import { gzipSync } from 'zlib';
|
|
5
|
-
import { ChatOpenAI } from '@langchain/openai';
|
|
6
|
-
import { SystemMessage, HumanMessage } from '@langchain/core/messages';
|
|
7
|
-
|
|
8
1
|
// src/index.ts
|
|
2
|
+
import pc2 from "picocolors";
|
|
3
|
+
|
|
4
|
+
// src/analyzer.ts
|
|
5
|
+
import fs3 from "fs";
|
|
6
|
+
import path3 from "path";
|
|
7
|
+
import { gzipSync } from "zlib";
|
|
8
|
+
import { ChatOpenAI } from "@langchain/openai";
|
|
9
|
+
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
|
10
|
+
|
|
11
|
+
// src/dependency-analyzer.ts
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import path from "path";
|
|
9
14
|
var DependencyAnalyzer = class {
|
|
10
15
|
/**
|
|
11
16
|
* 分析依赖
|
|
@@ -50,12 +55,12 @@ var DependencyAnalyzer = class {
|
|
|
50
55
|
parseImports(file) {
|
|
51
56
|
const imports = [];
|
|
52
57
|
try {
|
|
53
|
-
const distDir =
|
|
54
|
-
const filePath =
|
|
55
|
-
if (!
|
|
58
|
+
const distDir = path.resolve(process.cwd(), "dist");
|
|
59
|
+
const filePath = path.join(distDir, file.path);
|
|
60
|
+
if (!fs.existsSync(filePath)) {
|
|
56
61
|
return imports;
|
|
57
62
|
}
|
|
58
|
-
const content =
|
|
63
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
59
64
|
const patterns = [
|
|
60
65
|
// node_modules 依赖
|
|
61
66
|
/from\s+["']([^"']+)["']/g,
|
|
@@ -89,10 +94,14 @@ var DependencyAnalyzer = class {
|
|
|
89
94
|
})).sort((a, b) => b.usedBy.length - a.usedBy.length).slice(0, 10);
|
|
90
95
|
}
|
|
91
96
|
};
|
|
97
|
+
|
|
98
|
+
// src/history-analyzer.ts
|
|
99
|
+
import fs2 from "fs";
|
|
100
|
+
import path2 from "path";
|
|
92
101
|
var HistoryAnalyzer = class {
|
|
93
102
|
constructor() {
|
|
94
|
-
const reportsDir =
|
|
95
|
-
this.historyFile =
|
|
103
|
+
const reportsDir = path2.resolve(process.cwd(), "ai-reports");
|
|
104
|
+
this.historyFile = path2.join(reportsDir, ".perf-history.json");
|
|
96
105
|
}
|
|
97
106
|
/**
|
|
98
107
|
* 对比当前构建与历史记录
|
|
@@ -179,7 +188,7 @@ var HistoryAnalyzer = class {
|
|
|
179
188
|
const history = JSON.parse(content);
|
|
180
189
|
return history.slice(-10);
|
|
181
190
|
} catch (error) {
|
|
182
|
-
console.warn("
|
|
191
|
+
console.warn("⚠️ 无法读取历史记录");
|
|
183
192
|
return [];
|
|
184
193
|
}
|
|
185
194
|
}
|
|
@@ -198,7 +207,7 @@ var HistoryAnalyzer = class {
|
|
|
198
207
|
}))
|
|
199
208
|
};
|
|
200
209
|
history.push(record);
|
|
201
|
-
const dir =
|
|
210
|
+
const dir = path2.dirname(this.historyFile);
|
|
202
211
|
if (!fs2.existsSync(dir)) {
|
|
203
212
|
fs2.mkdirSync(dir, { recursive: true });
|
|
204
213
|
}
|
|
@@ -235,7 +244,7 @@ var OptimizationExamples = class {
|
|
|
235
244
|
if (issues.some((i) => i.type === "optimization")) {
|
|
236
245
|
examples.push(this.getCompressionExample());
|
|
237
246
|
}
|
|
238
|
-
if (issues.some((i) => i.description.includes("
|
|
247
|
+
if (issues.some((i) => i.description.includes("图片"))) {
|
|
239
248
|
examples.push(this.getImageOptimizationExample());
|
|
240
249
|
}
|
|
241
250
|
if (issues.some((i) => i.type === "size" && i.severity === "high")) {
|
|
@@ -249,11 +258,11 @@ var OptimizationExamples = class {
|
|
|
249
258
|
*/
|
|
250
259
|
getCodeSplittingExample() {
|
|
251
260
|
return {
|
|
252
|
-
title: "
|
|
253
|
-
description: "
|
|
261
|
+
title: "配置代码分割",
|
|
262
|
+
description: "将大文件拆分成多个小文件,提高加载性能",
|
|
254
263
|
priority: "high",
|
|
255
|
-
impact: "
|
|
256
|
-
difficulty: "
|
|
264
|
+
impact: "可减少 30-50% 的初始加载大小",
|
|
265
|
+
difficulty: "简单",
|
|
257
266
|
code: {
|
|
258
267
|
language: "typescript",
|
|
259
268
|
content: `// vite.config.ts
|
|
@@ -262,20 +271,20 @@ export default defineConfig({
|
|
|
262
271
|
rollupOptions: {
|
|
263
272
|
output: {
|
|
264
273
|
manualChunks: {
|
|
265
|
-
//
|
|
274
|
+
// 提取 Vue 核心库
|
|
266
275
|
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
|
267
276
|
|
|
268
|
-
//
|
|
277
|
+
// 提取 UI 组件库
|
|
269
278
|
'ui-vendor': ['element-plus', '@element-plus/icons-vue'],
|
|
270
279
|
|
|
271
|
-
//
|
|
280
|
+
// 提取工具库
|
|
272
281
|
'utils': ['lodash-es', 'dayjs', 'axios'],
|
|
273
282
|
|
|
274
|
-
//
|
|
283
|
+
// 提取图表库(如果使用)
|
|
275
284
|
'charts': ['echarts', 'chart.js'],
|
|
276
285
|
},
|
|
277
286
|
|
|
278
|
-
//
|
|
287
|
+
// 自动分割大于 500KB 的文件
|
|
279
288
|
chunkFileNames: (chunkInfo) => {
|
|
280
289
|
const facadeModuleId = chunkInfo.facadeModuleId
|
|
281
290
|
? chunkInfo.facadeModuleId.split('/').pop()
|
|
@@ -285,7 +294,7 @@ export default defineConfig({
|
|
|
285
294
|
},
|
|
286
295
|
},
|
|
287
296
|
|
|
288
|
-
//
|
|
297
|
+
// 设置 chunk 大小警告阈值
|
|
289
298
|
chunkSizeWarningLimit: 500,
|
|
290
299
|
},
|
|
291
300
|
});`
|
|
@@ -298,11 +307,11 @@ export default defineConfig({
|
|
|
298
307
|
getDependencyOptimizationExample(duplicates) {
|
|
299
308
|
const topDuplicates = duplicates.slice(0, 3).map((d) => d.name);
|
|
300
309
|
return {
|
|
301
|
-
title: "
|
|
302
|
-
description:
|
|
310
|
+
title: "优化重复依赖",
|
|
311
|
+
description: `检测到 ${duplicates.length} 个重复打包的依赖,建议提取到公共 chunk`,
|
|
303
312
|
priority: "high",
|
|
304
|
-
impact:
|
|
305
|
-
difficulty: "
|
|
313
|
+
impact: `可减少 ${(duplicates.reduce((sum, d) => sum + d.size, 0) / 1024 / 1024).toFixed(2)}MB`,
|
|
314
|
+
difficulty: "简单",
|
|
306
315
|
code: {
|
|
307
316
|
language: "typescript",
|
|
308
317
|
content: `// vite.config.ts
|
|
@@ -311,7 +320,7 @@ export default defineConfig({
|
|
|
311
320
|
rollupOptions: {
|
|
312
321
|
output: {
|
|
313
322
|
manualChunks: {
|
|
314
|
-
//
|
|
323
|
+
// 提取重复依赖到公共 chunk
|
|
315
324
|
'common-vendor': [
|
|
316
325
|
${topDuplicates.map((name) => `'${name}'`).join(",\n ")}
|
|
317
326
|
],
|
|
@@ -320,7 +329,7 @@ export default defineConfig({
|
|
|
320
329
|
},
|
|
321
330
|
},
|
|
322
331
|
|
|
323
|
-
//
|
|
332
|
+
// 或者使用自动分割策略
|
|
324
333
|
optimizeDeps: {
|
|
325
334
|
include: [
|
|
326
335
|
${topDuplicates.map((name) => `'${name}'`).join(",\n ")}
|
|
@@ -336,31 +345,31 @@ export default defineConfig({
|
|
|
336
345
|
*/
|
|
337
346
|
getCompressionExample() {
|
|
338
347
|
return {
|
|
339
|
-
title: "
|
|
340
|
-
description: "
|
|
348
|
+
title: "启用 Gzip/Brotli 压缩",
|
|
349
|
+
description: "使用压缩插件减少传输大小",
|
|
341
350
|
priority: "medium",
|
|
342
|
-
impact: "
|
|
343
|
-
difficulty: "
|
|
351
|
+
impact: "可减少 60-70% 的传输大小",
|
|
352
|
+
difficulty: "简单",
|
|
344
353
|
code: {
|
|
345
354
|
language: "bash",
|
|
346
|
-
content: `# 1.
|
|
355
|
+
content: `# 1. 安装压缩插件
|
|
347
356
|
npm install vite-plugin-compression -D
|
|
348
357
|
|
|
349
|
-
# 2.
|
|
358
|
+
# 2. 配置插件
|
|
350
359
|
# vite.config.ts
|
|
351
360
|
import viteCompression from 'vite-plugin-compression';
|
|
352
361
|
|
|
353
362
|
export default defineConfig({
|
|
354
363
|
plugins: [
|
|
355
|
-
// Gzip
|
|
364
|
+
// Gzip 压缩
|
|
356
365
|
viteCompression({
|
|
357
366
|
algorithm: 'gzip',
|
|
358
367
|
ext: '.gz',
|
|
359
|
-
threshold: 10240, //
|
|
368
|
+
threshold: 10240, // 大于 10KB 才压缩
|
|
360
369
|
deleteOriginFile: false,
|
|
361
370
|
}),
|
|
362
371
|
|
|
363
|
-
// Brotli
|
|
372
|
+
// Brotli 压缩(可选,压缩率更高)
|
|
364
373
|
viteCompression({
|
|
365
374
|
algorithm: 'brotliCompress',
|
|
366
375
|
ext: '.br',
|
|
@@ -370,7 +379,7 @@ export default defineConfig({
|
|
|
370
379
|
],
|
|
371
380
|
});
|
|
372
381
|
|
|
373
|
-
# 3. Nginx
|
|
382
|
+
# 3. Nginx 配置(服务器端)
|
|
374
383
|
# nginx.conf
|
|
375
384
|
gzip on;
|
|
376
385
|
gzip_types text/plain text/css application/json application/javascript;
|
|
@@ -383,40 +392,40 @@ gzip_min_length 1024;`
|
|
|
383
392
|
*/
|
|
384
393
|
getImageOptimizationExample() {
|
|
385
394
|
return {
|
|
386
|
-
title: "
|
|
387
|
-
description: "
|
|
395
|
+
title: "优化图片资源",
|
|
396
|
+
description: "压缩图片并转换为现代格式",
|
|
388
397
|
priority: "medium",
|
|
389
|
-
impact: "
|
|
390
|
-
difficulty: "
|
|
398
|
+
impact: "可减少 50-80% 的图片大小",
|
|
399
|
+
difficulty: "简单",
|
|
391
400
|
code: {
|
|
392
401
|
language: "bash",
|
|
393
|
-
content: `# 1.
|
|
402
|
+
content: `# 1. 安装图片优化插件
|
|
394
403
|
npm install vite-plugin-imagemin -D
|
|
395
404
|
|
|
396
|
-
# 2.
|
|
405
|
+
# 2. 配置插件
|
|
397
406
|
# vite.config.ts
|
|
398
407
|
import viteImagemin from 'vite-plugin-imagemin';
|
|
399
408
|
|
|
400
409
|
export default defineConfig({
|
|
401
410
|
plugins: [
|
|
402
411
|
viteImagemin({
|
|
403
|
-
// GIF
|
|
412
|
+
// GIF 优化
|
|
404
413
|
gifsicle: {
|
|
405
414
|
optimizationLevel: 7,
|
|
406
415
|
interlaced: false,
|
|
407
416
|
},
|
|
408
417
|
|
|
409
|
-
// PNG
|
|
418
|
+
// PNG 优化
|
|
410
419
|
optipng: {
|
|
411
420
|
optimizationLevel: 7,
|
|
412
421
|
},
|
|
413
422
|
|
|
414
|
-
// JPEG
|
|
423
|
+
// JPEG 优化
|
|
415
424
|
mozjpeg: {
|
|
416
425
|
quality: 80,
|
|
417
426
|
},
|
|
418
427
|
|
|
419
|
-
// SVG
|
|
428
|
+
// SVG 优化
|
|
420
429
|
svgo: {
|
|
421
430
|
plugins: [
|
|
422
431
|
{ name: 'removeViewBox', active: false },
|
|
@@ -424,7 +433,7 @@ export default defineConfig({
|
|
|
424
433
|
],
|
|
425
434
|
},
|
|
426
435
|
|
|
427
|
-
// WebP
|
|
436
|
+
// WebP 转换
|
|
428
437
|
webp: {
|
|
429
438
|
quality: 80,
|
|
430
439
|
},
|
|
@@ -432,7 +441,7 @@ export default defineConfig({
|
|
|
432
441
|
],
|
|
433
442
|
});
|
|
434
443
|
|
|
435
|
-
# 3.
|
|
444
|
+
# 3. 使用 WebP 格式(HTML)
|
|
436
445
|
<picture>
|
|
437
446
|
<source srcset="image.webp" type="image/webp">
|
|
438
447
|
<img src="image.jpg" alt="description">
|
|
@@ -445,20 +454,20 @@ export default defineConfig({
|
|
|
445
454
|
*/
|
|
446
455
|
getDynamicImportExample() {
|
|
447
456
|
return {
|
|
448
|
-
title: "
|
|
449
|
-
description: "
|
|
457
|
+
title: "使用动态导入(懒加载)",
|
|
458
|
+
description: "按需加载组件和路由,减少初始加载大小",
|
|
450
459
|
priority: "high",
|
|
451
|
-
impact: "
|
|
452
|
-
difficulty: "
|
|
460
|
+
impact: "可减少 40-60% 的初始加载大小",
|
|
461
|
+
difficulty: "中等",
|
|
453
462
|
code: {
|
|
454
463
|
language: "typescript",
|
|
455
|
-
content: `// 1.
|
|
464
|
+
content: `// 1. 路由懒加载
|
|
456
465
|
// router/index.ts
|
|
457
466
|
const routes = [
|
|
458
467
|
{
|
|
459
468
|
path: '/dashboard',
|
|
460
469
|
name: 'Dashboard',
|
|
461
|
-
//
|
|
470
|
+
// 使用动态导入
|
|
462
471
|
component: () => import('@/views/Dashboard.vue'),
|
|
463
472
|
},
|
|
464
473
|
{
|
|
@@ -468,17 +477,17 @@ const routes = [
|
|
|
468
477
|
},
|
|
469
478
|
];
|
|
470
479
|
|
|
471
|
-
// 2.
|
|
480
|
+
// 2. 组件懒加载
|
|
472
481
|
// App.vue
|
|
473
482
|
<script setup>
|
|
474
483
|
import { defineAsyncComponent } from 'vue';
|
|
475
484
|
|
|
476
|
-
//
|
|
485
|
+
// 异步组件
|
|
477
486
|
const HeavyComponent = defineAsyncComponent(() =>
|
|
478
487
|
import('./components/HeavyComponent.vue')
|
|
479
488
|
);
|
|
480
489
|
|
|
481
|
-
//
|
|
490
|
+
// 带加载状态的异步组件
|
|
482
491
|
const AsyncComponent = defineAsyncComponent({
|
|
483
492
|
loader: () => import('./components/AsyncComponent.vue'),
|
|
484
493
|
loadingComponent: LoadingSpinner,
|
|
@@ -488,12 +497,12 @@ const AsyncComponent = defineAsyncComponent({
|
|
|
488
497
|
});
|
|
489
498
|
</script>
|
|
490
499
|
|
|
491
|
-
// 3.
|
|
500
|
+
// 3. 条件加载
|
|
492
501
|
<script setup>
|
|
493
502
|
const loadCharts = async () => {
|
|
494
503
|
if (needCharts) {
|
|
495
504
|
const { default: ECharts } = await import('echarts');
|
|
496
|
-
//
|
|
505
|
+
// 使用 ECharts
|
|
497
506
|
}
|
|
498
507
|
};
|
|
499
508
|
</script>`
|
|
@@ -505,50 +514,50 @@ const loadCharts = async () => {
|
|
|
505
514
|
*/
|
|
506
515
|
getTreeShakingExample() {
|
|
507
516
|
return {
|
|
508
|
-
title: "
|
|
509
|
-
description: "
|
|
517
|
+
title: "优化 Tree-shaking",
|
|
518
|
+
description: "确保未使用的代码被正确移除",
|
|
510
519
|
priority: "medium",
|
|
511
|
-
impact: "
|
|
512
|
-
difficulty: "
|
|
520
|
+
impact: "可减少 10-30% 的代码大小",
|
|
521
|
+
difficulty: "中等",
|
|
513
522
|
code: {
|
|
514
523
|
language: "typescript",
|
|
515
|
-
content: `// 1.
|
|
516
|
-
//
|
|
524
|
+
content: `// 1. 使用 ES6 模块导入(支持 tree-shaking)
|
|
525
|
+
// ❌ 不推荐
|
|
517
526
|
import _ from 'lodash';
|
|
518
527
|
const result = _.debounce(fn, 300);
|
|
519
528
|
|
|
520
|
-
//
|
|
529
|
+
// ✅ 推荐
|
|
521
530
|
import { debounce } from 'lodash-es';
|
|
522
531
|
const result = debounce(fn, 300);
|
|
523
532
|
|
|
524
|
-
// 2.
|
|
533
|
+
// 2. 配置 package.json
|
|
525
534
|
{
|
|
526
|
-
"sideEffects": false, //
|
|
527
|
-
//
|
|
535
|
+
"sideEffects": false, // 标记为无副作用
|
|
536
|
+
// 或指定有副作用的文件
|
|
528
537
|
"sideEffects": ["*.css", "*.scss"]
|
|
529
538
|
}
|
|
530
539
|
|
|
531
|
-
// 3. Vite
|
|
540
|
+
// 3. Vite 配置
|
|
532
541
|
// vite.config.ts
|
|
533
542
|
export default defineConfig({
|
|
534
543
|
build: {
|
|
535
|
-
//
|
|
544
|
+
// 启用 tree-shaking
|
|
536
545
|
minify: 'terser',
|
|
537
546
|
terserOptions: {
|
|
538
547
|
compress: {
|
|
539
|
-
drop_console: true, //
|
|
540
|
-
drop_debugger: true, //
|
|
541
|
-
pure_funcs: ['console.log'], //
|
|
548
|
+
drop_console: true, // 移除 console
|
|
549
|
+
drop_debugger: true, // 移除 debugger
|
|
550
|
+
pure_funcs: ['console.log'], // 移除特定函数
|
|
542
551
|
},
|
|
543
552
|
},
|
|
544
553
|
},
|
|
545
554
|
});
|
|
546
555
|
|
|
547
|
-
// 4.
|
|
548
|
-
//
|
|
556
|
+
// 4. 避免副作用导入
|
|
557
|
+
// ❌ 不推荐(会导入整个模块)
|
|
549
558
|
import 'some-library';
|
|
550
559
|
|
|
551
|
-
//
|
|
560
|
+
// ✅ 推荐(明确导入需要的部分)
|
|
552
561
|
import { specificFunction } from 'some-library';`
|
|
553
562
|
}
|
|
554
563
|
};
|
|
@@ -578,14 +587,14 @@ var PerfAnalyzer = class {
|
|
|
578
587
|
*/
|
|
579
588
|
async analyze() {
|
|
580
589
|
const distDir = path3.resolve(process.cwd(), "dist");
|
|
581
|
-
if (!
|
|
582
|
-
throw new Error("
|
|
590
|
+
if (!fs3.existsSync(distDir)) {
|
|
591
|
+
throw new Error("构建目录不存在,请先执行构建");
|
|
583
592
|
}
|
|
584
593
|
const bundles = this.collectBundles(distDir);
|
|
585
594
|
const summary = this.calculateSummary(bundles);
|
|
586
|
-
console.log("
|
|
595
|
+
console.log("📦 正在分析依赖...");
|
|
587
596
|
const dependencies = this.depAnalyzer.analyzeDependencies(bundles);
|
|
588
|
-
console.log("
|
|
597
|
+
console.log("📊 正在对比历史记录...");
|
|
589
598
|
const comparison = this.historyAnalyzer.compare(
|
|
590
599
|
bundles,
|
|
591
600
|
summary.totalSize,
|
|
@@ -593,14 +602,14 @@ var PerfAnalyzer = class {
|
|
|
593
602
|
);
|
|
594
603
|
const issues = this.detectIssues(bundles, summary, dependencies);
|
|
595
604
|
const suggestions = this.generateSuggestions(issues);
|
|
596
|
-
console.log("
|
|
605
|
+
console.log("💡 正在生成优化示例...");
|
|
597
606
|
const optimizationExamples = this.examplesGenerator.generate(
|
|
598
607
|
issues,
|
|
599
608
|
dependencies.duplicates
|
|
600
609
|
);
|
|
601
610
|
let aiAnalysis;
|
|
602
611
|
if (this.llm) {
|
|
603
|
-
console.log("
|
|
612
|
+
console.log("🤖 正在使用 AI 分析性能...\n");
|
|
604
613
|
aiAnalysis = await this.performAIAnalysis(
|
|
605
614
|
bundles,
|
|
606
615
|
summary,
|
|
@@ -625,14 +634,14 @@ var PerfAnalyzer = class {
|
|
|
625
634
|
*/
|
|
626
635
|
collectBundles(dir, baseDir = dir) {
|
|
627
636
|
const bundles = [];
|
|
628
|
-
const files =
|
|
637
|
+
const files = fs3.readdirSync(dir);
|
|
629
638
|
for (const file of files) {
|
|
630
639
|
const filePath = path3.join(dir, file);
|
|
631
|
-
const stat =
|
|
640
|
+
const stat = fs3.statSync(filePath);
|
|
632
641
|
if (stat.isDirectory()) {
|
|
633
642
|
bundles.push(...this.collectBundles(filePath, baseDir));
|
|
634
643
|
} else {
|
|
635
|
-
const content =
|
|
644
|
+
const content = fs3.readFileSync(filePath);
|
|
636
645
|
const gzipSize = gzipSync(content).length;
|
|
637
646
|
const relativePath = path3.relative(baseDir, filePath);
|
|
638
647
|
bundles.push({
|
|
@@ -708,9 +717,9 @@ var PerfAnalyzer = class {
|
|
|
708
717
|
issues.push({
|
|
709
718
|
type: "size",
|
|
710
719
|
severity: "high",
|
|
711
|
-
title: "
|
|
712
|
-
description:
|
|
713
|
-
suggestion: "
|
|
720
|
+
title: "构建产物总大小过大",
|
|
721
|
+
description: `总大小 ${totalSizeMB.toFixed(2)}MB 超过阈值 ${threshold.totalSize}MB`,
|
|
722
|
+
suggestion: "考虑代码分割、tree-shaking、压缩等优化手段"
|
|
714
723
|
});
|
|
715
724
|
}
|
|
716
725
|
const largeBundles = bundles.filter(
|
|
@@ -720,12 +729,12 @@ var PerfAnalyzer = class {
|
|
|
720
729
|
issues.push({
|
|
721
730
|
type: "size",
|
|
722
731
|
severity: "medium",
|
|
723
|
-
title: "
|
|
724
|
-
description:
|
|
732
|
+
title: "存在过大的单个文件",
|
|
733
|
+
description: `发现 ${largeBundles.length} 个文件超过 ${threshold.bundleSize}KB`,
|
|
725
734
|
files: largeBundles.map(
|
|
726
735
|
(b) => `${b.name} (${(b.size / 1024).toFixed(2)}KB)`
|
|
727
736
|
),
|
|
728
|
-
suggestion: "
|
|
737
|
+
suggestion: "考虑拆分大文件,使用动态导入或代码分割"
|
|
729
738
|
});
|
|
730
739
|
}
|
|
731
740
|
const jsFiles = bundles.filter((b) => b.type === "javascript");
|
|
@@ -733,9 +742,9 @@ var PerfAnalyzer = class {
|
|
|
733
742
|
issues.push({
|
|
734
743
|
type: "count",
|
|
735
744
|
severity: "low",
|
|
736
|
-
title: "JavaScript
|
|
737
|
-
description:
|
|
738
|
-
suggestion: "
|
|
745
|
+
title: "JavaScript 文件数量过多",
|
|
746
|
+
description: `共有 ${jsFiles.length} 个 JS 文件,超过阈值 ${threshold.chunkCount}`,
|
|
747
|
+
suggestion: "过多的文件会增加 HTTP 请求数,考虑合并小文件"
|
|
739
748
|
});
|
|
740
749
|
}
|
|
741
750
|
const images = bundles.filter((b) => b.type === "image");
|
|
@@ -744,12 +753,12 @@ var PerfAnalyzer = class {
|
|
|
744
753
|
issues.push({
|
|
745
754
|
type: "optimization",
|
|
746
755
|
severity: "medium",
|
|
747
|
-
title: "
|
|
748
|
-
description:
|
|
756
|
+
title: "存在未优化的图片",
|
|
757
|
+
description: `发现 ${largeImages.length} 个大于 100KB 的图片`,
|
|
749
758
|
files: largeImages.map(
|
|
750
759
|
(img) => `${img.name} (${(img.size / 1024).toFixed(2)}KB)`
|
|
751
760
|
),
|
|
752
|
-
suggestion: "
|
|
761
|
+
suggestion: "使用图片压缩工具,或转换为 WebP 格式"
|
|
753
762
|
});
|
|
754
763
|
}
|
|
755
764
|
const sourcemaps = bundles.filter((b) => b.type === "sourcemap");
|
|
@@ -758,11 +767,11 @@ var PerfAnalyzer = class {
|
|
|
758
767
|
issues.push({
|
|
759
768
|
type: "optimization",
|
|
760
769
|
severity: "low",
|
|
761
|
-
title: "
|
|
762
|
-
description: `Sourcemap
|
|
770
|
+
title: "生产环境包含 sourcemap",
|
|
771
|
+
description: `Sourcemap 文件占用 ${(totalMapSize / 1024 / 1024).toFixed(
|
|
763
772
|
2
|
|
764
773
|
)}MB`,
|
|
765
|
-
suggestion: "
|
|
774
|
+
suggestion: "生产环境建议禁用 sourcemap 或使用外部 sourcemap"
|
|
766
775
|
});
|
|
767
776
|
}
|
|
768
777
|
if (dependencies && dependencies.duplicates.length > 0) {
|
|
@@ -770,12 +779,12 @@ var PerfAnalyzer = class {
|
|
|
770
779
|
issues.push({
|
|
771
780
|
type: "dependency",
|
|
772
781
|
severity: "medium",
|
|
773
|
-
title: "
|
|
774
|
-
description:
|
|
782
|
+
title: "检测到重复打包的依赖",
|
|
783
|
+
description: `发现 ${dependencies.duplicates.length} 个依赖被多次打包`,
|
|
775
784
|
files: topDuplicates.map(
|
|
776
|
-
(d) => `${d.name} (
|
|
785
|
+
(d) => `${d.name} (被 ${d.usedBy.length} 个文件使用, ${(d.size / 1024).toFixed(2)}KB)`
|
|
777
786
|
),
|
|
778
|
-
suggestion: "
|
|
787
|
+
suggestion: "将重复依赖提取到公共 chunk 中"
|
|
779
788
|
});
|
|
780
789
|
}
|
|
781
790
|
return issues;
|
|
@@ -786,16 +795,16 @@ var PerfAnalyzer = class {
|
|
|
786
795
|
generateSuggestions(issues) {
|
|
787
796
|
const suggestions = [];
|
|
788
797
|
if (issues.some((i) => i.type === "size")) {
|
|
789
|
-
suggestions.push("
|
|
790
|
-
suggestions.push("
|
|
791
|
-
suggestions.push("
|
|
798
|
+
suggestions.push("启用 gzip/brotli 压缩");
|
|
799
|
+
suggestions.push("配置 Vite 的 build.rollupOptions 进行代码分割");
|
|
800
|
+
suggestions.push("使用 vite-plugin-compression 插件");
|
|
792
801
|
}
|
|
793
802
|
if (issues.some((i) => i.type === "optimization")) {
|
|
794
|
-
suggestions.push("
|
|
795
|
-
suggestions.push("
|
|
803
|
+
suggestions.push("使用 vite-plugin-imagemin 优化图片");
|
|
804
|
+
suggestions.push("配置 CSS 压缩和 tree-shaking");
|
|
796
805
|
}
|
|
797
806
|
if (issues.length === 0) {
|
|
798
|
-
suggestions.push("
|
|
807
|
+
suggestions.push("构建产物已经很优秀,继续保持!");
|
|
799
808
|
}
|
|
800
809
|
return suggestions;
|
|
801
810
|
}
|
|
@@ -804,11 +813,11 @@ var PerfAnalyzer = class {
|
|
|
804
813
|
*/
|
|
805
814
|
async performAIAnalysis(bundles, summary, issues, dependencies, comparison) {
|
|
806
815
|
const systemPrompt = new SystemMessage(
|
|
807
|
-
"
|
|
816
|
+
"你是一个专业的前端性能优化专家,精通 Vite、Webpack 等构建工具。请分析构建产物并提供专业的优化建议。"
|
|
808
817
|
);
|
|
809
818
|
const bundlesSummary = summary.largestFiles.slice(0, 5).map((b) => `- ${b.name}: ${(b.size / 1024).toFixed(2)}KB`).join("\n");
|
|
810
819
|
const typesSummary = Object.entries(summary.byType).map(
|
|
811
|
-
([type, info]) => `- ${type}: ${info.count}
|
|
820
|
+
([type, info]) => `- ${type}: ${info.count} 个文件, ${(info.size / 1024).toFixed(2)}KB`
|
|
812
821
|
).join("\n");
|
|
813
822
|
const issuesSummary = issues.map(
|
|
814
823
|
(issue) => `- [${issue.severity}] ${issue.title}: ${issue.description}`
|
|
@@ -816,51 +825,56 @@ var PerfAnalyzer = class {
|
|
|
816
825
|
let dependencySummary = "";
|
|
817
826
|
if (dependencies) {
|
|
818
827
|
dependencySummary = `
|
|
819
|
-
##
|
|
820
|
-
-
|
|
821
|
-
-
|
|
822
|
-
-
|
|
828
|
+
## 依赖分析
|
|
829
|
+
- 总依赖数: ${dependencies.total}
|
|
830
|
+
- 重复依赖: ${dependencies.duplicates.length} 个
|
|
831
|
+
- 最大依赖: ${dependencies.largest.slice(0, 3).map((d) => `${d.name} (${(d.size / 1024).toFixed(2)}KB)`).join(", ")}`;
|
|
823
832
|
}
|
|
824
833
|
let comparisonSummary = "";
|
|
825
834
|
if (comparison) {
|
|
826
|
-
const sizeChange = comparison.totalSize.trend === "increased" ? "
|
|
835
|
+
const sizeChange = comparison.totalSize.trend === "increased" ? "增加" : "减少";
|
|
827
836
|
comparisonSummary = `
|
|
828
|
-
##
|
|
829
|
-
-
|
|
830
|
-
-
|
|
831
|
-
-
|
|
832
|
-
-
|
|
837
|
+
## 历史对比
|
|
838
|
+
- 总大小${sizeChange}: ${Math.abs(comparison.totalSize.diffPercent).toFixed(2)}%
|
|
839
|
+
- 文件数量变化: ${comparison.fileCount.diff > 0 ? "+" : ""}${comparison.fileCount.diff}
|
|
840
|
+
- 新增文件: ${comparison.newFiles.length} 个
|
|
841
|
+
- 删除文件: ${comparison.removedFiles.length} 个`;
|
|
833
842
|
}
|
|
834
843
|
const userPrompt = new HumanMessage(`
|
|
835
|
-
|
|
844
|
+
请分析以下构建产物信息:
|
|
836
845
|
|
|
837
|
-
##
|
|
838
|
-
-
|
|
839
|
-
- Gzip
|
|
840
|
-
-
|
|
846
|
+
## 总体统计
|
|
847
|
+
- 总大小: ${(summary.totalSize / 1024 / 1024).toFixed(2)}MB
|
|
848
|
+
- Gzip 后: ${(summary.totalGzipSize / 1024 / 1024).toFixed(2)}MB
|
|
849
|
+
- 文件数量: ${summary.fileCount}
|
|
841
850
|
|
|
842
|
-
##
|
|
851
|
+
## 最大的文件
|
|
843
852
|
${bundlesSummary}
|
|
844
853
|
|
|
845
|
-
##
|
|
854
|
+
## 按类型分组
|
|
846
855
|
${typesSummary}
|
|
847
856
|
${dependencySummary}
|
|
848
857
|
${comparisonSummary}
|
|
849
858
|
|
|
850
|
-
##
|
|
851
|
-
${issuesSummary || "
|
|
859
|
+
## 检测到的问题
|
|
860
|
+
${issuesSummary || "无明显问题"}
|
|
852
861
|
|
|
853
|
-
|
|
854
|
-
1.
|
|
855
|
-
2.
|
|
856
|
-
3.
|
|
862
|
+
请提供:
|
|
863
|
+
1. 性能评估(3-5 句话)
|
|
864
|
+
2. 具体优化建议(3-5 条,每条简洁明了)
|
|
865
|
+
3. 优先级排序
|
|
857
866
|
|
|
858
|
-
|
|
867
|
+
请用简洁专业的语言回答,不要过于冗长。
|
|
859
868
|
`);
|
|
860
869
|
const response = await this.llm.invoke([systemPrompt, userPrompt]);
|
|
861
870
|
return response.content.toString();
|
|
862
871
|
}
|
|
863
872
|
};
|
|
873
|
+
|
|
874
|
+
// src/reporter.ts
|
|
875
|
+
import fs4 from "fs";
|
|
876
|
+
import path4 from "path";
|
|
877
|
+
import pc from "picocolors";
|
|
864
878
|
var PerfReporter = class {
|
|
865
879
|
/**
|
|
866
880
|
* 生成报告
|
|
@@ -877,88 +891,88 @@ var PerfReporter = class {
|
|
|
877
891
|
* 控制台输出
|
|
878
892
|
*/
|
|
879
893
|
printConsole(result) {
|
|
880
|
-
console.log("\n
|
|
881
|
-
console.log("
|
|
882
|
-
console.log("
|
|
883
|
-
console.log("
|
|
894
|
+
console.log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
895
|
+
console.log("⚡ 性能分析报告");
|
|
896
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
|
|
897
|
+
console.log("📊 总体统计:");
|
|
884
898
|
console.log(
|
|
885
|
-
`
|
|
899
|
+
` 总大小: ${(result.summary.totalSize / 1024 / 1024).toFixed(2)}MB`
|
|
886
900
|
);
|
|
887
901
|
console.log(
|
|
888
|
-
` Gzip
|
|
902
|
+
` Gzip后: ${(result.summary.totalGzipSize / 1024 / 1024).toFixed(2)}MB`
|
|
889
903
|
);
|
|
890
|
-
console.log(`
|
|
904
|
+
console.log(` 文件数: ${result.summary.fileCount}
|
|
891
905
|
`);
|
|
892
906
|
if (result.comparison) {
|
|
893
|
-
console.log("
|
|
907
|
+
console.log("📈 历史对比:");
|
|
894
908
|
const { totalSize, fileCount, newFiles, removedFiles } = result.comparison;
|
|
895
|
-
const sizeIcon = totalSize.trend === "increased" ? "
|
|
909
|
+
const sizeIcon = totalSize.trend === "increased" ? "📈" : "📉";
|
|
896
910
|
console.log(
|
|
897
|
-
` ${sizeIcon}
|
|
911
|
+
` ${sizeIcon} 总大小: ${totalSize.trend === "increased" ? "+" : ""}${(totalSize.diff / 1024 / 1024).toFixed(2)}MB (${totalSize.diffPercent > 0 ? "+" : ""}${totalSize.diffPercent.toFixed(2)}%)`
|
|
898
912
|
);
|
|
899
913
|
console.log(
|
|
900
|
-
`
|
|
914
|
+
` 📊 文件数: ${fileCount.diff > 0 ? "+" : ""}${fileCount.diff}`
|
|
901
915
|
);
|
|
902
916
|
if (newFiles.length > 0) {
|
|
903
|
-
console.log(`
|
|
917
|
+
console.log(` ✨ 新增: ${newFiles.slice(0, 3).join(", ")}`);
|
|
904
918
|
}
|
|
905
919
|
if (removedFiles.length > 0) {
|
|
906
|
-
console.log(`
|
|
920
|
+
console.log(` 🗑️ 删除: ${removedFiles.slice(0, 3).join(", ")}`);
|
|
907
921
|
}
|
|
908
922
|
console.log();
|
|
909
923
|
}
|
|
910
924
|
if (result.dependencies) {
|
|
911
|
-
console.log("
|
|
912
|
-
console.log(`
|
|
925
|
+
console.log("📦 依赖分析:");
|
|
926
|
+
console.log(` 总依赖数: ${result.dependencies.total}`);
|
|
913
927
|
if (result.dependencies.duplicates.length > 0) {
|
|
914
928
|
console.log(
|
|
915
|
-
`
|
|
929
|
+
` ⚠️ 重复依赖: ${result.dependencies.duplicates.length} 个`
|
|
916
930
|
);
|
|
917
931
|
result.dependencies.duplicates.slice(0, 3).forEach((dep) => {
|
|
918
932
|
console.log(
|
|
919
|
-
` - ${dep.name}:
|
|
933
|
+
` - ${dep.name}: 被 ${dep.usedBy.length} 个文件使用`
|
|
920
934
|
);
|
|
921
935
|
});
|
|
922
936
|
}
|
|
923
937
|
console.log();
|
|
924
938
|
}
|
|
925
|
-
console.log("
|
|
939
|
+
console.log("📦 最大的文件:");
|
|
926
940
|
result.summary.largestFiles.slice(0, 5).forEach((file, index) => {
|
|
927
941
|
console.log(
|
|
928
942
|
` ${index + 1}. ${file.name}: ${(file.size / 1024).toFixed(2)}KB`
|
|
929
943
|
);
|
|
930
944
|
});
|
|
931
945
|
console.log();
|
|
932
|
-
console.log("
|
|
946
|
+
console.log("📋 按类型统计:");
|
|
933
947
|
Object.entries(result.summary.byType).forEach(([type, info]) => {
|
|
934
948
|
console.log(
|
|
935
|
-
` ${type}: ${info.count}
|
|
949
|
+
` ${type}: ${info.count} 个, ${(info.size / 1024).toFixed(2)}KB`
|
|
936
950
|
);
|
|
937
951
|
});
|
|
938
952
|
console.log();
|
|
939
953
|
if (result.issues.length > 0) {
|
|
940
|
-
console.log("
|
|
954
|
+
console.log("⚠️ 性能问题:");
|
|
941
955
|
result.issues.forEach((issue, index) => {
|
|
942
956
|
const icon = this.getSeverityIcon(issue.severity);
|
|
943
957
|
console.log(` ${index + 1}. ${icon} ${issue.title}`);
|
|
944
958
|
console.log(` ${issue.description}`);
|
|
945
959
|
if (issue.suggestion) {
|
|
946
|
-
console.log(`
|
|
960
|
+
console.log(` 💡 ${issue.suggestion}`);
|
|
947
961
|
}
|
|
948
962
|
});
|
|
949
963
|
console.log();
|
|
950
964
|
} else {
|
|
951
|
-
console.log("
|
|
965
|
+
console.log("✅ 未发现明显的性能问题\n");
|
|
952
966
|
}
|
|
953
967
|
if (result.suggestions.length > 0) {
|
|
954
|
-
console.log("
|
|
968
|
+
console.log("💡 优化建议:");
|
|
955
969
|
result.suggestions.forEach((suggestion, index) => {
|
|
956
970
|
console.log(` ${index + 1}. ${suggestion}`);
|
|
957
971
|
});
|
|
958
972
|
console.log();
|
|
959
973
|
}
|
|
960
974
|
if (result.optimizationExamples && result.optimizationExamples.length > 0) {
|
|
961
|
-
console.log("
|
|
975
|
+
console.log("🔧 优化示例:");
|
|
962
976
|
result.optimizationExamples.slice(0, 3).forEach((example, index) => {
|
|
963
977
|
console.log(` ${index + 1}. ${example.title} (${example.priority})`);
|
|
964
978
|
console.log(` ${example.description}`);
|
|
@@ -966,13 +980,13 @@ var PerfReporter = class {
|
|
|
966
980
|
console.log();
|
|
967
981
|
}
|
|
968
982
|
if (result.aiAnalysis) {
|
|
969
|
-
console.log("
|
|
983
|
+
console.log("🤖 AI 分析:");
|
|
970
984
|
console.log(
|
|
971
985
|
result.aiAnalysis.split("\n").map((line) => ` ${line}`).join("\n")
|
|
972
986
|
);
|
|
973
987
|
console.log();
|
|
974
988
|
}
|
|
975
|
-
console.log("
|
|
989
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
|
|
976
990
|
}
|
|
977
991
|
/**
|
|
978
992
|
* 生成 HTML 报告
|
|
@@ -984,7 +998,7 @@ var PerfReporter = class {
|
|
|
984
998
|
<head>
|
|
985
999
|
<meta charset="UTF-8">
|
|
986
1000
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
987
|
-
<title
|
|
1001
|
+
<title>性能分析报告</title>
|
|
988
1002
|
<style>
|
|
989
1003
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
990
1004
|
body {
|
|
@@ -1224,33 +1238,33 @@ var PerfReporter = class {
|
|
|
1224
1238
|
<body>
|
|
1225
1239
|
<div class="container">
|
|
1226
1240
|
<div class="header">
|
|
1227
|
-
<h1
|
|
1228
|
-
<div class="time"
|
|
1241
|
+
<h1>⚡ 性能分析报告</h1>
|
|
1242
|
+
<div class="time">生成时间: ${result.timestamp}</div>
|
|
1229
1243
|
</div>
|
|
1230
1244
|
|
|
1231
1245
|
<div class="content">
|
|
1232
|
-
<!--
|
|
1246
|
+
<!-- 总体统计 -->
|
|
1233
1247
|
<div class="section">
|
|
1234
|
-
<div class="section-title"
|
|
1248
|
+
<div class="section-title">📊 总体统计</div>
|
|
1235
1249
|
<div class="stats-grid">
|
|
1236
1250
|
<div class="stat-card">
|
|
1237
|
-
<div class="stat-label"
|
|
1251
|
+
<div class="stat-label">总大小</div>
|
|
1238
1252
|
<div class="stat-value">${(result.summary.totalSize / 1024 / 1024).toFixed(2)}MB</div>
|
|
1239
1253
|
</div>
|
|
1240
1254
|
<div class="stat-card">
|
|
1241
|
-
<div class="stat-label">Gzip
|
|
1255
|
+
<div class="stat-label">Gzip 后</div>
|
|
1242
1256
|
<div class="stat-value">${(result.summary.totalGzipSize / 1024 / 1024).toFixed(2)}MB</div>
|
|
1243
1257
|
</div>
|
|
1244
1258
|
<div class="stat-card">
|
|
1245
|
-
<div class="stat-label"
|
|
1259
|
+
<div class="stat-label">文件数量</div>
|
|
1246
1260
|
<div class="stat-value">${result.summary.fileCount}</div>
|
|
1247
1261
|
</div>
|
|
1248
1262
|
</div>
|
|
1249
1263
|
</div>
|
|
1250
1264
|
|
|
1251
|
-
<!--
|
|
1265
|
+
<!-- 最大文件 -->
|
|
1252
1266
|
<div class="section">
|
|
1253
|
-
<div class="section-title"
|
|
1267
|
+
<div class="section-title">📦 最大的文件</div>
|
|
1254
1268
|
<div class="file-list">
|
|
1255
1269
|
${result.summary.largestFiles.slice(0, 10).map(
|
|
1256
1270
|
(file) => `
|
|
@@ -1263,49 +1277,49 @@ var PerfReporter = class {
|
|
|
1263
1277
|
</div>
|
|
1264
1278
|
</div>
|
|
1265
1279
|
|
|
1266
|
-
<!--
|
|
1280
|
+
<!-- 按类型统计 -->
|
|
1267
1281
|
<div class="section">
|
|
1268
|
-
<div class="section-title"
|
|
1282
|
+
<div class="section-title">📋 按类型统计</div>
|
|
1269
1283
|
<div class="file-list">
|
|
1270
1284
|
${Object.entries(result.summary.byType).map(
|
|
1271
1285
|
([type, info]) => `
|
|
1272
1286
|
<div class="file-item">
|
|
1273
1287
|
<span class="file-name">${type}</span>
|
|
1274
|
-
<span class="file-size">${info.count}
|
|
1288
|
+
<span class="file-size">${info.count} 个, ${(info.size / 1024).toFixed(2)}KB</span>
|
|
1275
1289
|
</div>
|
|
1276
1290
|
`
|
|
1277
1291
|
).join("")}
|
|
1278
1292
|
</div>
|
|
1279
1293
|
</div>
|
|
1280
1294
|
|
|
1281
|
-
<!--
|
|
1295
|
+
<!-- 历史对比 -->
|
|
1282
1296
|
${result.comparison ? `
|
|
1283
1297
|
<div class="section">
|
|
1284
|
-
<div class="section-title"
|
|
1298
|
+
<div class="section-title">📈 历史对比</div>
|
|
1285
1299
|
<div class="stats-grid">
|
|
1286
1300
|
<div class="stat-card ${result.comparison.totalSize.trend === "increased" ? "trend-up" : "trend-down"}">
|
|
1287
|
-
<div class="stat-label"
|
|
1301
|
+
<div class="stat-label">总大小变化</div>
|
|
1288
1302
|
<div class="stat-value">${result.comparison.totalSize.diffPercent > 0 ? "+" : ""}${result.comparison.totalSize.diffPercent.toFixed(2)}%</div>
|
|
1289
1303
|
<div class="stat-label">${result.comparison.totalSize.trend === "increased" ? "+" : ""}${(result.comparison.totalSize.diff / 1024 / 1024).toFixed(
|
|
1290
1304
|
2
|
|
1291
1305
|
)}MB</div>
|
|
1292
1306
|
</div>
|
|
1293
1307
|
<div class="stat-card">
|
|
1294
|
-
<div class="stat-label"
|
|
1308
|
+
<div class="stat-label">文件数变化</div>
|
|
1295
1309
|
<div class="stat-value">${result.comparison.fileCount.diff > 0 ? "+" : ""}${result.comparison.fileCount.diff}</div>
|
|
1296
1310
|
</div>
|
|
1297
1311
|
<div class="stat-card">
|
|
1298
|
-
<div class="stat-label"
|
|
1312
|
+
<div class="stat-label">新增文件</div>
|
|
1299
1313
|
<div class="stat-value">${result.comparison.newFiles.length}</div>
|
|
1300
1314
|
</div>
|
|
1301
1315
|
<div class="stat-card">
|
|
1302
|
-
<div class="stat-label"
|
|
1316
|
+
<div class="stat-label">删除文件</div>
|
|
1303
1317
|
<div class="stat-value">${result.comparison.removedFiles.length}</div>
|
|
1304
1318
|
</div>
|
|
1305
1319
|
</div>
|
|
1306
1320
|
${result.comparison.largestFileChanges.length > 0 ? `
|
|
1307
1321
|
<div class="file-list" style="margin-top: 15px;">
|
|
1308
|
-
<div style="font-weight: bold; padding: 10px; color: #333;"
|
|
1322
|
+
<div style="font-weight: bold; padding: 10px; color: #333;">文件大小变化 Top 5:</div>
|
|
1309
1323
|
${result.comparison.largestFileChanges.slice(0, 5).map(
|
|
1310
1324
|
(change) => `
|
|
1311
1325
|
<div class="file-item">
|
|
@@ -1323,29 +1337,29 @@ var PerfReporter = class {
|
|
|
1323
1337
|
</div>
|
|
1324
1338
|
` : ""}
|
|
1325
1339
|
|
|
1326
|
-
<!--
|
|
1340
|
+
<!-- 依赖分析 -->
|
|
1327
1341
|
${result.dependencies ? `
|
|
1328
1342
|
<div class="section">
|
|
1329
|
-
<div class="section-title"
|
|
1343
|
+
<div class="section-title">📦 依赖分析</div>
|
|
1330
1344
|
<div class="stats-grid">
|
|
1331
1345
|
<div class="stat-card">
|
|
1332
|
-
<div class="stat-label"
|
|
1346
|
+
<div class="stat-label">总依赖数</div>
|
|
1333
1347
|
<div class="stat-value">${result.dependencies.total}</div>
|
|
1334
1348
|
</div>
|
|
1335
1349
|
<div class="stat-card ${result.dependencies.duplicates.length > 0 ? "trend-up" : ""}">
|
|
1336
|
-
<div class="stat-label"
|
|
1350
|
+
<div class="stat-label">重复依赖</div>
|
|
1337
1351
|
<div class="stat-value">${result.dependencies.duplicates.length}</div>
|
|
1338
1352
|
</div>
|
|
1339
1353
|
</div>
|
|
1340
1354
|
${result.dependencies.duplicates.length > 0 ? `
|
|
1341
1355
|
<div class="file-list" style="margin-top: 15px;">
|
|
1342
|
-
<div style="font-weight: bold; padding: 10px; color: #333;"
|
|
1356
|
+
<div style="font-weight: bold; padding: 10px; color: #333;">重复打包的依赖:</div>
|
|
1343
1357
|
${result.dependencies.duplicates.slice(0, 10).map(
|
|
1344
1358
|
(dep) => `
|
|
1345
1359
|
<div class="file-item">
|
|
1346
1360
|
<span class="file-name">${dep.name}</span>
|
|
1347
1361
|
<span class="file-size">
|
|
1348
|
-
${(dep.size / 1024).toFixed(2)}KB
|
|
1362
|
+
${(dep.size / 1024).toFixed(2)}KB · 被 ${dep.usedBy.length} 个文件使用
|
|
1349
1363
|
</span>
|
|
1350
1364
|
</div>
|
|
1351
1365
|
`
|
|
@@ -1354,7 +1368,7 @@ var PerfReporter = class {
|
|
|
1354
1368
|
` : ""}
|
|
1355
1369
|
${result.dependencies.largest.length > 0 ? `
|
|
1356
1370
|
<div class="file-list" style="margin-top: 15px;">
|
|
1357
|
-
<div style="font-weight: bold; padding: 10px; color: #333;"
|
|
1371
|
+
<div style="font-weight: bold; padding: 10px; color: #333;">最大的依赖:</div>
|
|
1358
1372
|
${result.dependencies.largest.slice(0, 10).map(
|
|
1359
1373
|
(dep) => `
|
|
1360
1374
|
<div class="file-item">
|
|
@@ -1370,10 +1384,10 @@ var PerfReporter = class {
|
|
|
1370
1384
|
</div>
|
|
1371
1385
|
` : ""}
|
|
1372
1386
|
|
|
1373
|
-
<!--
|
|
1387
|
+
<!-- 性能问题 -->
|
|
1374
1388
|
${result.issues.length > 0 ? `
|
|
1375
1389
|
<div class="section">
|
|
1376
|
-
<div class="section-title"
|
|
1390
|
+
<div class="section-title">⚠️ 性能问题</div>
|
|
1377
1391
|
${result.issues.map(
|
|
1378
1392
|
(issue) => `
|
|
1379
1393
|
<div class="issue ${issue.severity}">
|
|
@@ -1381,20 +1395,20 @@ var PerfReporter = class {
|
|
|
1381
1395
|
issue.severity
|
|
1382
1396
|
)} ${issue.title}</div>
|
|
1383
1397
|
<div class="issue-desc">${issue.description}</div>
|
|
1384
|
-
${issue.files ? `<div class="issue-desc"
|
|
1398
|
+
${issue.files ? `<div class="issue-desc">相关文件: ${issue.files.join(
|
|
1385
1399
|
", "
|
|
1386
1400
|
)}</div>` : ""}
|
|
1387
|
-
${issue.suggestion ? `<div class="issue-suggestion"
|
|
1401
|
+
${issue.suggestion ? `<div class="issue-suggestion">💡 ${issue.suggestion}</div>` : ""}
|
|
1388
1402
|
</div>
|
|
1389
1403
|
`
|
|
1390
1404
|
).join("")}
|
|
1391
1405
|
</div>
|
|
1392
|
-
` : '<div class="section"><div class="section-title"
|
|
1406
|
+
` : '<div class="section"><div class="section-title">✅ 未发现明显的性能问题</div></div>'}
|
|
1393
1407
|
|
|
1394
|
-
<!--
|
|
1408
|
+
<!-- 优化建议 -->
|
|
1395
1409
|
${result.suggestions.length > 0 ? `
|
|
1396
1410
|
<div class="section">
|
|
1397
|
-
<div class="section-title"
|
|
1411
|
+
<div class="section-title">💡 优化建议</div>
|
|
1398
1412
|
<div class="suggestion-list">
|
|
1399
1413
|
${result.suggestions.map(
|
|
1400
1414
|
(suggestion, index) => `
|
|
@@ -1405,21 +1419,21 @@ var PerfReporter = class {
|
|
|
1405
1419
|
</div>
|
|
1406
1420
|
` : ""}
|
|
1407
1421
|
|
|
1408
|
-
<!--
|
|
1422
|
+
<!-- 优化示例 -->
|
|
1409
1423
|
${result.optimizationExamples && result.optimizationExamples.length > 0 ? `
|
|
1410
1424
|
<div class="section">
|
|
1411
|
-
<div class="section-title"
|
|
1425
|
+
<div class="section-title">🔧 优化示例</div>
|
|
1412
1426
|
${result.optimizationExamples.map(
|
|
1413
1427
|
(example) => `
|
|
1414
1428
|
<div class="optimization-example">
|
|
1415
1429
|
<div class="example-header">
|
|
1416
1430
|
<div class="example-title">
|
|
1417
1431
|
${example.title}
|
|
1418
|
-
<span class="priority-badge priority-${example.priority}">${example.priority === "high" ? "
|
|
1432
|
+
<span class="priority-badge priority-${example.priority}">${example.priority === "high" ? "高优先级" : example.priority === "medium" ? "中优先级" : "低优先级"}</span>
|
|
1419
1433
|
</div>
|
|
1420
1434
|
<div class="example-meta">
|
|
1421
|
-
<span
|
|
1422
|
-
<span
|
|
1435
|
+
<span>💪 ${example.difficulty}</span>
|
|
1436
|
+
<span>📈 ${example.impact}</span>
|
|
1423
1437
|
</div>
|
|
1424
1438
|
</div>
|
|
1425
1439
|
<div class="example-desc">${example.description}</div>
|
|
@@ -1429,7 +1443,7 @@ var PerfReporter = class {
|
|
|
1429
1443
|
</div>
|
|
1430
1444
|
${example.relatedFiles && example.relatedFiles.length > 0 ? `
|
|
1431
1445
|
<div class="related-files">
|
|
1432
|
-
<strong
|
|
1446
|
+
<strong>相关文件:</strong> ${example.relatedFiles.join(", ")}
|
|
1433
1447
|
</div>
|
|
1434
1448
|
` : ""}
|
|
1435
1449
|
</div>
|
|
@@ -1438,10 +1452,10 @@ var PerfReporter = class {
|
|
|
1438
1452
|
</div>
|
|
1439
1453
|
` : ""}
|
|
1440
1454
|
|
|
1441
|
-
<!-- AI
|
|
1455
|
+
<!-- AI 分析 -->
|
|
1442
1456
|
${result.aiAnalysis ? `
|
|
1443
1457
|
<div class="section">
|
|
1444
|
-
<div class="section-title"
|
|
1458
|
+
<div class="section-title">🤖 AI 分析</div>
|
|
1445
1459
|
<div class="ai-analysis">${this.escapeHtml(result.aiAnalysis)}</div>
|
|
1446
1460
|
</div>
|
|
1447
1461
|
` : ""}
|
|
@@ -1455,38 +1469,38 @@ var PerfReporter = class {
|
|
|
1455
1469
|
</body>
|
|
1456
1470
|
</html>
|
|
1457
1471
|
`.trim();
|
|
1458
|
-
const reportsDir =
|
|
1459
|
-
if (!
|
|
1460
|
-
|
|
1472
|
+
const reportsDir = path4.resolve(process.cwd(), "ai-reports");
|
|
1473
|
+
if (!fs4.existsSync(reportsDir)) {
|
|
1474
|
+
fs4.mkdirSync(reportsDir, { recursive: true });
|
|
1461
1475
|
}
|
|
1462
|
-
const reportPath =
|
|
1463
|
-
|
|
1476
|
+
const reportPath = path4.resolve(reportsDir, "performance-report.html");
|
|
1477
|
+
fs4.writeFileSync(reportPath, html, "utf-8");
|
|
1464
1478
|
console.log(pc.green(`
|
|
1465
|
-
|
|
1479
|
+
📄 性能报告已生成: ${pc.cyan(reportPath)}
|
|
1466
1480
|
`));
|
|
1467
1481
|
}
|
|
1468
1482
|
/**
|
|
1469
1483
|
* 生成 JSON 报告
|
|
1470
1484
|
*/
|
|
1471
1485
|
async generateJSONReport(result) {
|
|
1472
|
-
const reportsDir =
|
|
1473
|
-
if (!
|
|
1474
|
-
|
|
1486
|
+
const reportsDir = path4.resolve(process.cwd(), "ai-reports");
|
|
1487
|
+
if (!fs4.existsSync(reportsDir)) {
|
|
1488
|
+
fs4.mkdirSync(reportsDir, { recursive: true });
|
|
1475
1489
|
}
|
|
1476
|
-
const reportPath =
|
|
1477
|
-
|
|
1478
|
-
console.log(pc.green(
|
|
1490
|
+
const reportPath = path4.resolve(reportsDir, "performance-report.json");
|
|
1491
|
+
fs4.writeFileSync(reportPath, JSON.stringify(result, null, 2), "utf-8");
|
|
1492
|
+
console.log(pc.green(`📄 JSON 报告已生成: ${pc.cyan(reportPath)}`));
|
|
1479
1493
|
}
|
|
1480
1494
|
/**
|
|
1481
1495
|
* 获取严重程度图标
|
|
1482
1496
|
*/
|
|
1483
1497
|
getSeverityIcon(severity) {
|
|
1484
1498
|
const icons = {
|
|
1485
|
-
high: "
|
|
1486
|
-
medium: "
|
|
1487
|
-
low: "
|
|
1499
|
+
high: "🔴",
|
|
1500
|
+
medium: "🟡",
|
|
1501
|
+
low: "🔵"
|
|
1488
1502
|
};
|
|
1489
|
-
return icons[severity] || "
|
|
1503
|
+
return icons[severity] || "⚪";
|
|
1490
1504
|
}
|
|
1491
1505
|
/**
|
|
1492
1506
|
* 转义 HTML
|
|
@@ -1531,19 +1545,19 @@ function vitePluginAIPerfAnalyzer(options = {}) {
|
|
|
1531
1545
|
enforce: "post",
|
|
1532
1546
|
configResolved(config) {
|
|
1533
1547
|
if (!enabled) return;
|
|
1534
|
-
console.log(
|
|
1535
|
-
console.log(
|
|
1536
|
-
console.log(`
|
|
1537
|
-
console.log(`
|
|
1548
|
+
console.log(pc2.cyan("\n⚡ AI 性能分析插件已启动..."));
|
|
1549
|
+
console.log(`📊 分析阈值:`);
|
|
1550
|
+
console.log(` 单文件: ${pc2.yellow(threshold.bundleSize + "KB")}`);
|
|
1551
|
+
console.log(` 总大小: ${pc2.yellow(threshold.totalSize + "MB")}`);
|
|
1538
1552
|
console.log(
|
|
1539
|
-
` Chunk
|
|
1553
|
+
` Chunk数: ${pc2.yellow((threshold.chunkCount ?? 10).toString())}`
|
|
1540
1554
|
);
|
|
1541
|
-
console.log(
|
|
1555
|
+
console.log(`🔑 API Key: ${apiKey ? "已配置" : "未配置"}
|
|
1542
1556
|
`);
|
|
1543
1557
|
},
|
|
1544
1558
|
async closeBundle() {
|
|
1545
1559
|
if (!enabled) return;
|
|
1546
|
-
console.log("\n
|
|
1560
|
+
console.log("\n⚡ 正在分析构建产物...\n");
|
|
1547
1561
|
try {
|
|
1548
1562
|
analysisResult = await analyzer.analyze();
|
|
1549
1563
|
await reporter.generate(analysisResult, output);
|
|
@@ -1551,13 +1565,14 @@ function vitePluginAIPerfAnalyzer(options = {}) {
|
|
|
1551
1565
|
reporter.printConsole(analysisResult);
|
|
1552
1566
|
}
|
|
1553
1567
|
} catch (error) {
|
|
1554
|
-
console.error("
|
|
1568
|
+
console.error("❌ 性能分析失败:", error.message);
|
|
1555
1569
|
}
|
|
1556
1570
|
}
|
|
1557
1571
|
};
|
|
1558
1572
|
}
|
|
1559
1573
|
var index_default = vitePluginAIPerfAnalyzer;
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1574
|
+
export {
|
|
1575
|
+
index_default as default,
|
|
1576
|
+
vitePluginAIPerfAnalyzer
|
|
1577
|
+
};
|
|
1563
1578
|
//# sourceMappingURL=index.mjs.map
|