index.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. /**
  2. * FeHelper Wpo Tools
  3. */
  4. new Vue({
  5. el: '#pageContainer',
  6. data: {
  7. pageTitle: '无',
  8. pageUrl: '无',
  9. timing: null,
  10. headerInfo: null,
  11. webVitals: null,
  12. resources: null,
  13. performanceMetrics: null,
  14. longTasks: null,
  15. networkInfo: null,
  16. // 控制各个部分的显示状态
  17. sectionsVisible: {
  18. resourceTiming: true,
  19. longTasks: true,
  20. headerInfo: true,
  21. optimization: true
  22. },
  23. tmDefination: {
  24. lookupDomainTime: 'DNS查询耗时',
  25. connectTime: 'TCP连接耗时',
  26. requestTime: '网络请求耗时',
  27. firstPaintTime: '白屏时间',
  28. readyStart: '构建文档流耗时',
  29. domReadyTime: 'DOM树构建耗时',
  30. redirectTime: '重定向耗时',
  31. appcacheTime: '数据缓存耗时',
  32. unloadEventTime: '卸载文档耗时',
  33. initDomTreeTime: '请求完成到可交互',
  34. loadEventTime: '加载事件耗时',
  35. loadTime: '加载总耗时'
  36. }
  37. },
  38. computed: {
  39. // 获取优化建议
  40. optimizationSuggestions() {
  41. const suggestions = [];
  42. // 性能指标建议
  43. if (this.webVitals) {
  44. // LCP建议
  45. if (this.webVitals.lcp > 2500) {
  46. suggestions.push({
  47. category: '页面加载性能',
  48. type: 'warning',
  49. title: '最大内容绘制(LCP)需要优化',
  50. description: '当前LCP时间为 ' + Math.ceil(this.webVitals.lcp) + 'ms,超过了推荐的2500ms',
  51. suggestions: [
  52. '优化服务器响应时间,考虑使用CDN',
  53. '优化关键渲染路径,减少阻塞资源',
  54. '优化和压缩图片,考虑使用WebP格式',
  55. '实施懒加载策略',
  56. '优化CSS和JavaScript的加载顺序'
  57. ]
  58. });
  59. }
  60. // FID建议
  61. if (this.webVitals.fid > 100) {
  62. suggestions.push({
  63. category: '交互响应性能',
  64. type: 'warning',
  65. title: '首次输入延迟(FID)需要改进',
  66. description: '当前FID时间为 ' + Math.ceil(this.webVitals.fid) + 'ms,超过了推荐的100ms',
  67. suggestions: [
  68. '减少主线程工作量,拆分长任务',
  69. '优化JavaScript执行时间',
  70. '延迟加载非关键JavaScript',
  71. '移除未使用的JavaScript代码',
  72. '使用Web Workers处理复杂计算'
  73. ]
  74. });
  75. }
  76. // CLS建议
  77. if (this.webVitals.cls > 0.1) {
  78. suggestions.push({
  79. category: '视觉稳定性',
  80. type: 'warning',
  81. title: '累积布局偏移(CLS)需要改进',
  82. description: '当前CLS值为 ' + this.webVitals.cls.toFixed(3) + ',超过了推荐的0.1',
  83. suggestions: [
  84. '为图片和视频元素设置明确的宽高比',
  85. '避免在已存在的内容上方插入内容',
  86. '使用transform动画代替改变位置的动画',
  87. '预留足够的空间给动态加载的内容',
  88. '优化字体加载策略'
  89. ]
  90. });
  91. }
  92. }
  93. // 资源优化建议
  94. if (this.resources && this.resources.length) {
  95. let totalSize = 0;
  96. let largeResources = [];
  97. let uncompressedResources = [];
  98. let longLoadingResources = [];
  99. this.resources.forEach(resource => {
  100. totalSize += resource.transferSize || 0;
  101. // 检查大文件
  102. if (resource.transferSize > 500 * 1024) { // 大于500KB
  103. largeResources.push(resource);
  104. }
  105. // 检查加载时间长的资源
  106. if (resource.duration > 1000) { // 超过1秒
  107. longLoadingResources.push(resource);
  108. }
  109. });
  110. if (totalSize > 2 * 1024 * 1024) { // 总大小超过2MB
  111. suggestions.push({
  112. category: '资源优化',
  113. type: 'warning',
  114. title: '总资源大小过大',
  115. description: '页面总资源大小为 ' + this.formatSize(totalSize),
  116. suggestions: [
  117. '使用代码分割和懒加载',
  118. '优化和压缩图片资源',
  119. '启用Gzip/Brotli压缩',
  120. '使用合适的缓存策略',
  121. '移除未使用的CSS和JavaScript代码'
  122. ]
  123. });
  124. }
  125. if (largeResources.length > 0) {
  126. suggestions.push({
  127. category: '资源优化',
  128. type: 'info',
  129. title: '发现大体积资源文件',
  130. description: '有 ' + largeResources.length + ' 个资源文件大于500KB',
  131. suggestions: largeResources.map(r =>
  132. `优化 ${this.getFileName(r.name)} (${this.formatSize(r.transferSize)})`
  133. )
  134. });
  135. }
  136. if (longLoadingResources.length > 0) {
  137. suggestions.push({
  138. category: '资源加载优化',
  139. type: 'warning',
  140. title: '资源加载时间过长',
  141. description: '有 ' + longLoadingResources.length + ' 个资源加载时间超过1秒',
  142. suggestions: longLoadingResources.map(r =>
  143. `优化 ${this.getFileName(r.name)} (${Math.ceil(r.duration)}ms)`
  144. )
  145. });
  146. }
  147. }
  148. // JavaScript性能建议
  149. if (this.performanceMetrics) {
  150. const usedHeapRatio = this.performanceMetrics.usedJSHeapSize / this.performanceMetrics.jsHeapSizeLimit;
  151. if (usedHeapRatio > 0.7) {
  152. suggestions.push({
  153. category: '内存使用优化',
  154. type: 'warning',
  155. title: 'JavaScript内存使用率过高',
  156. description: '当前内存使用率达到 ' + (usedHeapRatio * 100).toFixed(1) + '%',
  157. suggestions: [
  158. '检查内存泄漏问题',
  159. '优化大对象的创建和销毁',
  160. '使用防抖和节流控制频繁操作',
  161. '及时清理不再使用的事件监听器',
  162. '优化闭包使用,避免过度引用'
  163. ]
  164. });
  165. }
  166. }
  167. // 长任务优化建议
  168. if (this.longTasks && this.longTasks.length > 3) {
  169. suggestions.push({
  170. category: '性能优化',
  171. type: 'warning',
  172. title: '检测到多个长任务',
  173. description: '发现 ' + this.longTasks.length + ' 个执行时间超过50ms的任务',
  174. suggestions: [
  175. '将长任务拆分为更小的任务',
  176. '使用Web Workers处理复杂计算',
  177. '优化事件处理函数',
  178. '使用requestAnimationFrame进行视觉更新',
  179. '使用requestIdleCallback处理非关键任务'
  180. ]
  181. });
  182. }
  183. // 网络优化建议
  184. if (this.networkInfo) {
  185. if (this.networkInfo.effectiveType !== '4g') {
  186. suggestions.push({
  187. category: '网络优化',
  188. type: 'info',
  189. title: '检测到非4G网络环境',
  190. description: '当前网络类型: ' + this.networkInfo.effectiveType + ', RTT: ' + this.networkInfo.rtt + 'ms',
  191. suggestions: [
  192. '实施渐进式加载策略',
  193. '优先加载关键资源',
  194. '使用自适应加载',
  195. '考虑使用Service Worker缓存',
  196. '优化资源大小和加载顺序'
  197. ]
  198. });
  199. }
  200. }
  201. // HTTP Header优化建议
  202. if (this.headerInfo) {
  203. const headerSuggestions = [];
  204. if (!this.headerInfo['cache-control']) {
  205. headerSuggestions.push('添加Cache-Control头以优化缓存策略');
  206. }
  207. if (!this.headerInfo['content-encoding']) {
  208. headerSuggestions.push('启用Gzip/Brotli压缩以减少传输大小');
  209. }
  210. if (!this.headerInfo['x-content-type-options']) {
  211. headerSuggestions.push('添加X-Content-Type-Options头以提高安全性');
  212. }
  213. if (!this.headerInfo['x-frame-options']) {
  214. headerSuggestions.push('添加X-Frame-Options头以防止点击劫持');
  215. }
  216. if (headerSuggestions.length > 0) {
  217. suggestions.push({
  218. category: 'HTTP优化',
  219. type: 'info',
  220. title: 'HTTP响应头优化建议',
  221. description: '发现 ' + headerSuggestions.length + ' 个HTTP头部优化建议',
  222. suggestions: headerSuggestions
  223. });
  224. }
  225. }
  226. return suggestions;
  227. }
  228. },
  229. mounted: function () {
  230. // 清理过期数据(7天前的数据)
  231. this.cleanExpiredData();
  232. // 在tab创建或者更新时候,监听事件,看看是否有参数传递过来
  233. if (location.protocol === 'chrome-extension:') {
  234. chrome.tabs.query({currentWindow: true, active: true}, (tabs) => {
  235. if (!tabs || !tabs.length) {
  236. console.warn('未找到活动标签页');
  237. return;
  238. }
  239. let activeTab = tabs[0];
  240. chrome.runtime.sendMessage({
  241. type: 'fh-dynamic-any-thing',
  242. thing: 'request-page-content',
  243. tabId: activeTab.id
  244. }).then(resp => {
  245. if (!resp) {
  246. console.warn('未收到响应数据');
  247. return;
  248. }
  249. if (!resp.content) {
  250. console.warn('响应数据中没有content字段');
  251. return;
  252. }
  253. try {
  254. // 保存数据到localStorage,带上时间戳
  255. const storageData = {
  256. timestamp: Date.now(),
  257. data: resp.content
  258. };
  259. localStorage.setItem('wpo-data', JSON.stringify(storageData));
  260. this.showTiming(resp.content);
  261. } catch (e) {
  262. console.error('处理性能数据时出错:', e);
  263. }
  264. }).catch(err => {
  265. console.error('获取页面性能数据失败:', err);
  266. });
  267. });
  268. } else {
  269. try {
  270. // 从localStorage读取数据
  271. let wpoStorageData = localStorage.getItem('wpo-data');
  272. if (wpoStorageData) {
  273. let storage = JSON.parse(wpoStorageData);
  274. this.showTiming(storage.data);
  275. }
  276. } catch (e) {
  277. console.error('读取缓存的性能数据失败:', e);
  278. }
  279. }
  280. },
  281. methods: {
  282. // 切换部分的显示/隐藏
  283. toggleSection(section) {
  284. this.sectionsVisible[section] = !this.sectionsVisible[section];
  285. // 更新aria-expanded属性
  286. this.$nextTick(() => {
  287. const header = document.querySelector(`#${section} .card-header`);
  288. if (header) {
  289. header.setAttribute('aria-expanded', this.sectionsVisible[section]);
  290. }
  291. });
  292. },
  293. // 清理过期数据(7天前的数据)
  294. cleanExpiredData() {
  295. try {
  296. const wpoStorageData = localStorage.getItem('wpo-data');
  297. if (wpoStorageData) {
  298. const storage = JSON.parse(wpoStorageData);
  299. const expirationTime = 7 * 24 * 60 * 60 * 1000; // 7天
  300. if (Date.now() - storage.timestamp > expirationTime) {
  301. localStorage.removeItem('wpo-data');
  302. }
  303. }
  304. } catch (e) {
  305. console.error('清理过期数据时出错:', e);
  306. }
  307. },
  308. showTiming(wpo) {
  309. if (!wpo || typeof wpo !== 'object') {
  310. console.warn('性能数据格式不正确');
  311. return;
  312. }
  313. try {
  314. this.pageTitle = wpo.pageInfo?.title || "无";
  315. this.pageUrl = wpo.pageInfo?.url || "无";
  316. this.timing = wpo.time || null;
  317. this.headerInfo = wpo.header || null;
  318. this.webVitals = wpo.webVitals || null;
  319. this.resources = wpo.resources || null;
  320. this.performanceMetrics = wpo.performanceMetrics || null;
  321. this.longTasks = wpo.longTasks || null;
  322. this.networkInfo = wpo.networkInfo || null;
  323. } catch (e) {
  324. console.error('显示性能数据时出错:', e);
  325. }
  326. },
  327. // Core Web Vitals 状态判断
  328. getLCPStatus(lcp) {
  329. if (!lcp) return '';
  330. return lcp <= 2500 ? 'text-success' : lcp <= 4000 ? 'text-warning' : 'text-danger';
  331. },
  332. getLCPStatusText(lcp) {
  333. if (!lcp) return '未知';
  334. return lcp <= 2500 ? '良好' : lcp <= 4000 ? '需要改进' : '较差';
  335. },
  336. getFIDStatus(fid) {
  337. if (!fid) return '';
  338. return fid <= 100 ? 'text-success' : fid <= 300 ? 'text-warning' : 'text-danger';
  339. },
  340. getFIDStatusText(fid) {
  341. if (!fid) return '未触发';
  342. return fid <= 100 ? '良好' : fid <= 300 ? '需要改进' : '较差';
  343. },
  344. getCLSStatus(cls) {
  345. if (!cls) return '';
  346. return cls <= 0.1 ? 'text-success' : cls <= 0.25 ? 'text-warning' : 'text-danger';
  347. },
  348. getCLSStatusText(cls) {
  349. if (!cls) return '未知';
  350. return cls <= 0.1 ? '良好' : cls <= 0.25 ? '需要改进' : '较差';
  351. },
  352. // 工具函数
  353. formatSize(bytes) {
  354. if (!bytes) return '0 B';
  355. const units = ['B', 'KB', 'MB', 'GB'];
  356. let i = 0;
  357. while (bytes >= 1024 && i < units.length - 1) {
  358. bytes /= 1024;
  359. i++;
  360. }
  361. return bytes.toFixed(2) + ' ' + units[i];
  362. },
  363. formatTime(timestamp) {
  364. return new Date(timestamp).toLocaleTimeString('zh-CN', {
  365. hour12: false,
  366. hour: '2-digit',
  367. minute: '2-digit',
  368. second: '2-digit',
  369. fractionalSecondDigits: 3
  370. });
  371. },
  372. getFileName(url) {
  373. try {
  374. return new URL(url).pathname.split('/').pop() || url;
  375. } catch (e) {
  376. return url;
  377. }
  378. },
  379. openDonateModal: function(event) {
  380. event.preventDefault();
  381. event.stopPropagation();
  382. chrome.runtime.sendMessage({
  383. type: 'fh-dynamic-any-thing',
  384. thing: 'open-donate-modal',
  385. params: { toolName: 'page-timing' }
  386. });
  387. },
  388. openOptionsPage: function(event) {
  389. event.preventDefault();
  390. event.stopPropagation();
  391. chrome.runtime.openOptionsPage();
  392. }
  393. }
  394. });