UNPKG

react-insight

Version:

A powerful React performance monitoring component that helps you identify and fix performance issues in your React applications

275 lines 12.1 kB
// src/components/PerformanceMonitor.tsx import React, { useEffect, useRef, useState } from "react"; if (!window.__PM_PATCHED__) { window.__PM_PATCHED__ = true; let timeoutCount = 0; let intervalCount = 0; const _setTimeout = window.setTimeout; const _clearTimeout = window.clearTimeout; const _setInterval = window.setInterval; const _clearInterval = window.clearInterval; window.setTimeout = (...args) => { timeoutCount++; return _setTimeout(...args); }; window.clearTimeout = (id) => { if (timeoutCount > 0) timeoutCount--; _clearTimeout(id); }; window.setInterval = (...args) => { intervalCount++; return _setInterval(...args); }; window.clearInterval = (id) => { if (intervalCount > 0) intervalCount--; _clearInterval(id); }; window.__PM_GET_ACTIVE_TIMERS__ = () => timeoutCount + intervalCount; let listenerCount = 0; const _add = EventTarget.prototype.addEventListener; const _remove = EventTarget.prototype.removeEventListener; EventTarget.prototype.addEventListener = function(...args) { listenerCount++; return _add.apply(this, args); }; EventTarget.prototype.removeEventListener = function(...args) { if (listenerCount > 0) listenerCount--; return _remove.apply(this, args); }; window.__PM_GET_EVENT_LISTENERS__ = () => listenerCount; let pendingFetches = 0; const _fetch = window.fetch.bind(window); window.fetch = (...args) => { var _a; pendingFetches++; return (_a = _fetch(...args)) == null ? void 0 : _a.finally(() => { if (pendingFetches > 0) pendingFetches--; }); }; window.__PM_GET_PENDING_REQUESTS__ = () => pendingFetches; } var clsValue = 0; var clsEntries = []; if (!("__PM_CLS_OBSERVER__" in window) && "PerformanceObserver" in window) { const clsObserver = new PerformanceObserver((list) => { list.getEntries().forEach((e) => { const entry = e; if (!entry.hadRecentInput && entry.value > 0) { clsValue += entry.value; clsEntries.push(entry); } }); }); clsObserver.observe({ type: "layout-shift", buffered: true }); window.__PM_CLS_OBSERVER__ = clsObserver; } var checkImagesAlt = () => { const imgs = Array.from(document.querySelectorAll("img")); const without = imgs.filter((img) => { var _a; return !((_a = img.getAttribute("alt")) != null ? _a : "").trim(); }); return { total: imgs.length, withoutAlt: without.length, elements: without }; }; var getMaxDepth = (el, depth = 0) => { if (!el || el.children.length === 0) return depth; return Math.max(...Array.from(el.children).map((c) => getMaxDepth(c, depth + 1))); }; var checkDOMSize = () => { const bodyElements = document.body.getElementsByTagName("*").length; const headElements = document.head.getElementsByTagName("*").length; return { totalElements: bodyElements + headElements, depth: getMaxDepth(document.body), bodyElements, headElements }; }; var checkLayoutShift = () => { const nodes = /* @__PURE__ */ new Set(); clsEntries.forEach((e) => { var _a; return (_a = e.sources) == null ? void 0 : _a.forEach((s) => s.node && nodes.add(s.node)); }); return { value: +clsValue.toFixed(3), elements: Array.from(nodes) }; }; var checkMemoryUsage = () => { if (performance.memory) { const { usedJSHeapSize, totalJSHeapSize, jsHeapSizeLimit } = performance.memory; return { usedMB: +(usedJSHeapSize / 1048576).toFixed(2), totalMB: +(totalJSHeapSize / 1048576).toFixed(2), limitMB: +(jsHeapSizeLimit / 1048576).toFixed(2) }; } return null; }; var measureInteractionDelay = () => new Promise((resolve) => { const start = performance.now(); requestAnimationFrame(() => resolve(performance.now() - start)); }); var checkPerformance = async () => { var _a, _b, _c, _d, _e, _f; const domSize = checkDOMSize(); const imagesAlt = checkImagesAlt(); const layoutShift = checkLayoutShift(); const memoryUsage = checkMemoryUsage(); const activeTimers = (_b = (_a = window.__PM_GET_ACTIVE_TIMERS__) == null ? void 0 : _a.call(window)) != null ? _b : 0; const totalListeners = (_d = (_c = window.__PM_GET_EVENT_LISTENERS__) == null ? void 0 : _c.call(window)) != null ? _d : 0; const pending = (_f = (_e = window.__PM_GET_PENDING_REQUESTS__) == null ? void 0 : _e.call(window)) != null ? _f : 0; const interactionDelay = await measureInteractionDelay(); let score = 100; if (domSize.totalElements > 3e3) score -= 50; else if (domSize.totalElements > 2e3) score -= 35; else if (domSize.totalElements > 1e3) score -= 25; if (domSize.depth > 32) score -= Math.min(10, domSize.depth - 10); if (imagesAlt.withoutAlt) score -= Math.min(15, imagesAlt.withoutAlt * 2); if (layoutShift.value > 0.1) { score -= Math.min(10, layoutShift.value * 100); } if (memoryUsage && memoryUsage.limitMB && memoryUsage.usedMB / memoryUsage.limitMB > 0.3) { score -= Math.min(15, (memoryUsage.usedMB / memoryUsage.limitMB - 0.3) * 50); } if (activeTimers > 10) score -= Math.min(10, activeTimers - 10); if (totalListeners > 500) score -= Math.min(15, (totalListeners - 500) / 50); if (interactionDelay > 100) score -= Math.min(20, (interactionDelay - 100) / 10); if (pending > 5) score -= Math.min(10, pending - 5); if (score < 0) score = 0; return { score, domSize, imagesAlt, layoutShift, memoryUsage, activeTimers, eventListeners: { totalListeners }, interactionDelay, pendingRequests: { total: pending, requests: [] } }; }; var scoreColor = (s) => s >= 90 ? "#4CAF50" : s >= 70 ? "#FFC107" : s >= 50 ? "#FF9800" : "#F44336"; var PerformanceMonitor = ({ children }) => { const [open, setOpen] = useState(false); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const pop = useRef(null); const analyze = async () => { setLoading(true); try { setData(await checkPerformance()); setOpen(true); } finally { setLoading(false); } }; useEffect(() => { const handler = (e) => { if (pop.current && !pop.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, []); return /* @__PURE__ */ React.createElement(React.Fragment, null, children, /* @__PURE__ */ React.createElement("div", { className: "PerformanceMonitor_container" }, /* @__PURE__ */ React.createElement( "button", { className: "PerformanceMonitor_analyzeButton", onClick: analyze, disabled: loading, "aria-label": "Analyze Performance" }, /* @__PURE__ */ React.createElement("span", { className: "PerformanceMonitor_buttonIcon" }, loading ? "\u23F3" : "\u{1F50D}"), /* @__PURE__ */ React.createElement("span", { className: "PerformanceMonitor_buttonText" }, loading ? "Analyzing\u2026" : "Analyze Performance") ), open && data && /* @__PURE__ */ React.createElement("div", { className: "PerformanceMonitor_popover", ref: pop }, /* @__PURE__ */ React.createElement("div", { className: "PerformanceMonitor_popoverHeader" }, /* @__PURE__ */ React.createElement("h2", { className: "PerformanceMonitor_popoverTitle" }, "Performance Analysis"), /* @__PURE__ */ React.createElement( "button", { className: "PerformanceMonitor_closeButton", onClick: () => setOpen(false), "aria-label": "Close" }, "\xD7" )), /* @__PURE__ */ React.createElement("div", { className: "PerformanceMonitor_scoreContainer" }, /* @__PURE__ */ React.createElement( "div", { className: "PerformanceMonitor_scoreCircle", style: { background: `conic-gradient(${scoreColor( data.score )} ${data.score}%, #f0f0f0 ${data.score}% 100%)` } }, /* @__PURE__ */ React.createElement("div", { className: "PerformanceMonitor_scoreInner" }, /* @__PURE__ */ React.createElement("span", { className: "PerformanceMonitor_scoreValue" }, Math.round(data.score)), /* @__PURE__ */ React.createElement("span", { className: "PerformanceMonitor_scoreLabel" }, "Score")) )), /* @__PURE__ */ React.createElement("div", { className: "PerformanceMonitor_resultsContainer" }, data.score >= 99 ? /* @__PURE__ */ React.createElement("div", { className: "PerformanceMonitor_goodResult" }, /* @__PURE__ */ React.createElement("h3", null, "Great job! Your app is performing well."), /* @__PURE__ */ React.createElement("p", null, "Keep monitoring as your codebase grows.")) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("h3", { className: "PerformanceMonitor_resultsTitle" }, "Areas for Improvement:"), data.domSize.totalElements > 1e3 && /* @__PURE__ */ React.createElement( Issue, { title: "Large DOM Size", detail: `DOM has ${data.domSize.totalElements} elements (limit \u2248\xA01000).`, suggestion: "Simplify or virtualise parts of the UI." } ), data.domSize.depth > 32 && /* @__PURE__ */ React.createElement( Issue, { title: "Deep DOM Structure", detail: `DOM depth is ${data.domSize.depth} (recommended \u2264\xA032) for Single Page Applications.`, suggestion: "Flatten nested component trees." } ), data.imagesAlt.withoutAlt > 0 && /* @__PURE__ */ React.createElement( Issue, { title: "Missing Image Alts", detail: `${data.imagesAlt.withoutAlt} image(s) lack alt attributes.`, suggestion: "Add descriptive alt text." } ), data.layoutShift.value > 0 && /* @__PURE__ */ React.createElement( Issue, { title: "Layout Shifts Detected", detail: `CLS\xA0score\xA0${data.layoutShift.value.toFixed(3)} across ${data.layoutShift.elements.length} element(s).`, suggestion: "Reserve space for images/ads and avoid inserting content above existing UI." } ), data.memoryUsage && data.memoryUsage.usedMB > 100 && /* @__PURE__ */ React.createElement( Issue, { title: "High Memory Usage", detail: `Using ${data.memoryUsage.usedMB}\xA0MB (target \u2264\xA0100\xA0MB).`, suggestion: "Release large objects and watch for leaks." } ), data.activeTimers > 10 && /* @__PURE__ */ React.createElement( Issue, { title: "Too Many Active Timers", detail: `${data.activeTimers} active timeouts/intervals (limit \u2248\xA010).`, suggestion: "Clear timers on unmount, replace polling with events." } ), data.eventListeners.totalListeners > 500 && /* @__PURE__ */ React.createElement( Issue, { title: "Excessive Event Listeners", detail: `${data.eventListeners.totalListeners} listeners attached (limit \u2248\xA0500).`, suggestion: "Remove listeners on cleanup, delegate where possible." } ), data.interactionDelay > 100 && /* @__PURE__ */ React.createElement( Issue, { title: "Slow Interaction Response", detail: `Interaction delay ${Math.round( data.interactionDelay )}\xA0ms (target \u2264\xA0100\xA0ms).`, suggestion: "Move heavy work off main thread or use Web\xA0Workers." } ), data.pendingRequests.total > 5 && /* @__PURE__ */ React.createElement( Issue, { title: "Many Pending Requests", detail: `${data.pendingRequests.total} concurrent fetches (limit \u2248\xA05).`, suggestion: "Batch requests or debounce API calls." } )))))); }; var Issue = ({ title, detail, suggestion }) => /* @__PURE__ */ React.createElement("div", { className: "PerformanceMonitor_issueItem" }, /* @__PURE__ */ React.createElement("div", { className: "PerformanceMonitor_issueHeader" }, /* @__PURE__ */ React.createElement("span", { className: "PerformanceMonitor_issueIcon" }, "\u26A0\uFE0F"), /* @__PURE__ */ React.createElement("h4", null, title)), /* @__PURE__ */ React.createElement("p", null, detail), /* @__PURE__ */ React.createElement("p", { className: "PerformanceMonitor_suggestion" }, suggestion)); var PerformanceMonitor_default = PerformanceMonitor; export { PerformanceMonitor_default as PerformanceMonitor }; //# sourceMappingURL=index.mjs.map