content-script.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. /**
  2. * 计算并保存网页加载时间
  3. * @author zhaoxianlie
  4. */
  5. window.pagetimingContentScript = function () {
  6. /**
  7. * Navigation Timing API helpers
  8. * timing.getTimes();
  9. **/
  10. window.timing = window.timing || {
  11. /**
  12. * Outputs extended measurements using Navigation Timing API
  13. * @param Object opts Options (simple (bool) - opts out of full data view)
  14. * @return Object measurements
  15. */
  16. getTimes: function(opts) {
  17. var performance = window.performance || window.webkitPerformance || window.msPerformance || window.mozPerformance;
  18. if (performance === undefined) {
  19. return false;
  20. }
  21. var timing = performance.timing;
  22. var api = {};
  23. opts = opts || {};
  24. if (timing) {
  25. if(opts && !opts.simple) {
  26. for (var k in timing) {
  27. if(isNumeric(timing[k])) {
  28. api[k] = parseFloat(timing[k]);
  29. }
  30. }
  31. }
  32. // Time to first paint
  33. if (api.firstPaint === undefined) {
  34. var firstPaint = 0;
  35. // IE
  36. if (typeof timing.msFirstPaint === 'number') {
  37. firstPaint = timing.msFirstPaint;
  38. api.firstPaintTime = firstPaint - timing.navigationStart;
  39. } else if (performance.getEntriesByName !== undefined) {
  40. var firstPaintPerformanceEntry = performance.getEntriesByName('first-paint');
  41. if (firstPaintPerformanceEntry.length === 1) {
  42. var firstPaintTime = firstPaintPerformanceEntry[0].startTime;
  43. firstPaint = performance.timeOrigin + firstPaintTime;
  44. api.firstPaintTime = firstPaintTime;
  45. }
  46. }
  47. if (opts && !opts.simple) {
  48. api.firstPaint = firstPaint;
  49. }
  50. }
  51. // Total time from start to load
  52. api.loadTime = timing.loadEventEnd - timing.fetchStart;
  53. // Time spent constructing the DOM tree
  54. api.domReadyTime = timing.domComplete - timing.domInteractive;
  55. // Time consumed preparing the new page
  56. api.readyStart = timing.fetchStart - timing.navigationStart;
  57. // Time spent during redirection
  58. api.redirectTime = timing.redirectEnd - timing.redirectStart;
  59. // AppCache
  60. api.appcacheTime = timing.domainLookupStart - timing.fetchStart;
  61. // Time spent unloading documents
  62. api.unloadEventTime = timing.unloadEventEnd - timing.unloadEventStart;
  63. // DNS query time
  64. api.lookupDomainTime = timing.domainLookupEnd - timing.domainLookupStart;
  65. // TCP connection time
  66. api.connectTime = timing.connectEnd - timing.connectStart;
  67. // Time spent during the request
  68. api.requestTime = timing.responseEnd - timing.requestStart;
  69. // Request to completion of the DOM loading
  70. api.initDomTreeTime = timing.domInteractive - timing.responseEnd;
  71. // Load event time
  72. api.loadEventTime = timing.loadEventEnd - timing.loadEventStart;
  73. }
  74. return api;
  75. },
  76. /**
  77. * Uses console.table() to print a complete table of timing information
  78. * @param Object opts Options (simple (bool) - opts out of full data view)
  79. */
  80. printTable: function(opts) {
  81. var table = {};
  82. var data = this.getTimes(opts) || {};
  83. Object.keys(data).sort().forEach(function(k) {
  84. table[k] = {
  85. ms: data[k],
  86. s: +((data[k] / 1000).toFixed(2))
  87. };
  88. });
  89. console.table(table);
  90. },
  91. /**
  92. * Uses console.table() to print a summary table of timing information
  93. */
  94. printSimpleTable: function() {
  95. this.printTable({simple: true});
  96. }
  97. };
  98. function isNumeric(n) {
  99. return !isNaN(parseFloat(n)) && isFinite(n);
  100. }
  101. // 获取资源加载性能数据
  102. function getResourceTiming() {
  103. const resources = performance.getEntriesByType('resource');
  104. return resources.map(resource => ({
  105. name: resource.name,
  106. entryType: resource.entryType,
  107. startTime: resource.startTime,
  108. duration: resource.duration,
  109. transferSize: resource.transferSize,
  110. decodedBodySize: resource.decodedBodySize,
  111. encodedBodySize: resource.encodedBodySize,
  112. dnsTime: resource.domainLookupEnd - resource.domainLookupStart,
  113. tcpTime: resource.connectEnd - resource.connectStart,
  114. ttfb: resource.responseStart - resource.requestStart,
  115. downloadTime: resource.responseEnd - resource.responseStart
  116. }));
  117. }
  118. // 获取核心Web指标
  119. function getCoreWebVitals() {
  120. return new Promise(resolve => {
  121. let webVitals = {};
  122. // LCP (Largest Contentful Paint)
  123. new PerformanceObserver((entryList) => {
  124. const entries = entryList.getEntries();
  125. const lastEntry = entries[entries.length - 1];
  126. webVitals.lcp = lastEntry.renderTime || lastEntry.loadTime;
  127. }).observe({entryTypes: ['largest-contentful-paint']});
  128. // FID (First Input Delay)
  129. new PerformanceObserver((entryList) => {
  130. const firstInput = entryList.getEntries()[0];
  131. if (firstInput) {
  132. webVitals.fid = firstInput.processingTime;
  133. webVitals.firstInputTime = firstInput.startTime;
  134. }
  135. }).observe({entryTypes: ['first-input']});
  136. // CLS (Cumulative Layout Shift)
  137. let clsValue = 0;
  138. new PerformanceObserver((entryList) => {
  139. for (const entry of entryList.getEntries()) {
  140. if (!entry.hadRecentInput) {
  141. clsValue += entry.value;
  142. }
  143. }
  144. webVitals.cls = clsValue;
  145. }).observe({entryTypes: ['layout-shift']});
  146. setTimeout(() => resolve(webVitals), 3000);
  147. });
  148. }
  149. // 获取性能指标
  150. function getPerformanceMetrics() {
  151. if (window.performance.memory) {
  152. return {
  153. jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
  154. totalJSHeapSize: performance.memory.totalJSHeapSize,
  155. usedJSHeapSize: performance.memory.usedJSHeapSize
  156. };
  157. }
  158. return null;
  159. }
  160. // 监控长任务
  161. function observeLongTasks() {
  162. const longTasks = [];
  163. new PerformanceObserver((list) => {
  164. for (const entry of list.getEntries()) {
  165. longTasks.push({
  166. duration: entry.duration,
  167. startTime: entry.startTime,
  168. name: entry.name
  169. });
  170. }
  171. }).observe({entryTypes: ['longtask']});
  172. return longTasks;
  173. }
  174. // 获取网络信息
  175. function getNetworkInfo() {
  176. if ('connection' in navigator) {
  177. const connection = navigator.connection;
  178. return {
  179. effectiveType: connection.effectiveType,
  180. downlink: connection.downlink,
  181. rtt: connection.rtt,
  182. saveData: connection.saveData
  183. };
  184. }
  185. return null;
  186. }
  187. // 创建进度提示框
  188. function createProgressTip() {
  189. const tipContainer = document.createElement('div');
  190. tipContainer.id = 'fe-helper-timing-tip';
  191. tipContainer.style.cssText = `
  192. position: fixed;
  193. top: 20px;
  194. right: 20px;
  195. background: rgba(0, 0, 0, 0.8);
  196. color: white;
  197. padding: 15px 20px;
  198. border-radius: 8px;
  199. font-size: 14px;
  200. z-index: 999999;
  201. box-shadow: 0 2px 10px rgba(0,0,0,0.2);
  202. transition: opacity 0.3s;
  203. `;
  204. document.body.appendChild(tipContainer);
  205. return tipContainer;
  206. }
  207. // 更新提示框内容
  208. function updateProgressTip(message, progress) {
  209. const tipContainer = document.getElementById('fe-helper-timing-tip') || createProgressTip();
  210. tipContainer.innerHTML = `
  211. <div>${message}</div>
  212. ${progress ? `<div style="margin-top:8px;background:rgba(255,255,255,0.2);height:2px;border-radius:1px">
  213. <div style="width:${progress}%;height:100%;background:#4CAF50;border-radius:1px"></div>
  214. </div>` : ''}
  215. `;
  216. }
  217. // 移除提示框
  218. function removeProgressTip() {
  219. const tipContainer = document.getElementById('fe-helper-timing-tip');
  220. if (tipContainer) {
  221. tipContainer.style.opacity = '0';
  222. setTimeout(() => tipContainer.remove(), 300);
  223. }
  224. }
  225. window.pagetimingNoPage = function() {
  226. updateProgressTip('正在收集页面基础信息...', 20);
  227. let wpoInfo = {
  228. pageInfo: {
  229. title: document.title,
  230. url: location.href
  231. },
  232. time: window.timing.getTimes({simple: true}),
  233. resources: getResourceTiming(),
  234. networkInfo: getNetworkInfo(),
  235. performanceMetrics: getPerformanceMetrics(),
  236. longTasks: []
  237. };
  238. updateProgressTip('正在监控页面性能...', 40);
  239. // 初始化长任务监控
  240. const longTasksMonitor = observeLongTasks();
  241. let sendWpoInfo = function () {
  242. updateProgressTip('正在处理性能数据...', 60);
  243. // 合并长任务数据
  244. wpoInfo.longTasks = longTasksMonitor;
  245. // 获取核心Web指标
  246. getCoreWebVitals().then(webVitals => {
  247. updateProgressTip('正在完成数据采集...', 80);
  248. wpoInfo.webVitals = webVitals;
  249. chrome.runtime.sendMessage({
  250. type: 'fh-dynamic-any-thing',
  251. thing: 'set-page-timing-data',
  252. wpoInfo: wpoInfo
  253. }, () => {
  254. updateProgressTip('数据采集完成!', 100);
  255. setTimeout(removeProgressTip, 1000);
  256. });
  257. });
  258. };
  259. let getHttpHeaders = function () {
  260. if (wpoInfo.header && wpoInfo.time && wpoInfo.pageInfo) {
  261. sendWpoInfo();
  262. } else {
  263. updateProgressTip('正在获取页面请求头信息...', 50);
  264. fetch(location.href).then(resp => {
  265. let header = {};
  266. for (let pair of resp.headers.entries()) {
  267. header[pair[0]] = pair[1];
  268. }
  269. return header;
  270. }).then(header => {
  271. wpoInfo.header = header;
  272. sendWpoInfo();
  273. }).catch(error => {
  274. console.log(error);
  275. updateProgressTip('获取请求头信息失败,继续其他数据采集...', 50);
  276. sendWpoInfo();
  277. });
  278. }
  279. };
  280. let detect = function () {
  281. // 如果是网络地址,才去获取header
  282. if (/^((http)|(https)):\/\//.test(location.href)) {
  283. getHttpHeaders();
  284. } else {
  285. sendWpoInfo();
  286. }
  287. };
  288. detect();
  289. };
  290. };