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
JavaScript
// 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