nextjs-perfkit
Version:
Frontend performance analyzer for Next.js apps – render time tracking, memory logging, network analysis, DevTools UI.
374 lines (354 loc) • 12.9 kB
JavaScript
import { jsxs, jsx } from 'react/jsx-runtime';
import { useState, useRef, useEffect } from 'react';
import { __awaiter } from 'tslib';
const trackMemoryUsage = () => {
if (performance.memory) {
const mem = performance.memory;
console.log("[PerfKit] JS Heap Size:", {
usedMB: (mem.usedJSHeapSize / 1048576).toFixed(2),
totalMB: (mem.totalJSHeapSize / 1048576).toFixed(2),
limitMB: (mem.jsHeapSizeLimit / 1048576).toFixed(2),
});
if (mem.usedJSHeapSize > 100 * 1048576) {
alert("[PerfKit] High memory usage detected!");
}
}
};
const interceptNetwork = () => {
const originalFetch = window.fetch;
window.fetch = (...args) => __awaiter(void 0, void 0, void 0, function* () {
const start = performance.now();
const response = yield originalFetch(...args);
const duration = performance.now() - start;
console.log("[PerfKit] Fetch:", args[0], "Duration:", duration.toFixed(2), "ms");
return response;
});
};
const logs = [];
/**
* Adds a new performance log entry to the internal log store
*/
const addLog = (type, label, duration, meta) => {
logs.push({
type,
label,
duration,
timestamp: new Date().toISOString(),
meta,
});
};
/**
* Returns all logs
*/
const getLogs = () => {
return [...logs];
};
/**
* Clears all stored logs
*/
const clearLogs = () => {
logs.length = 0;
};
/**
* Downloads logs as a JSON file
*/
const downloadLogs = () => {
const blob = new Blob([JSON.stringify(logs, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `perfkit-logs-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
};
const injectOverlayFallbackStyles = () => {
if (document.querySelector("[data-perfkit-style]"))
return;
console.warn("[PerfKit] overlay.css not imported! Injecting fallback inline styles. For best appearance, import: nextjs-perfkit/styles/overlay.css");
const style = document.createElement("style");
style.setAttribute("data-perfkit-style", "true");
style.innerHTML = `
.perfkit-overlay {
position: fixed;
bottom: 20px;
right: 20px;
background-color: rgba(30, 30, 30, 0.95);
border: 1px solid #444;
border-radius: 10px;
padding: 12px;
font-family: monospace;
font-size: 14px;
color: #fff;
z-index: 9999;
width: 280px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
.perfkit-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.perfkit-header h4 {
margin: 0;
font-size: 16px;
color: #00d8ff;
}
.perfkit-toggle {
background: none;
border: none;
color: #ccc;
font-size: 16px;
cursor: pointer;
}
.perfkit-body {
margin-top: 12px;
}
.perfkit-section {
margin-bottom: 16px;
}
.perfkit-section h5 {
margin: 0 0 6px;
font-size: 14px;
color: #fff;
}
.perfkit-section button {
background: #444;
color: white;
border: none;
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
margin-top: 4px;
}
.perfkit-section button:hover {
background: #666;
}
.perfkit-log-preview {
margin-top: 8px;
max-height: 120px;
overflow-y: auto;
background: #111;
border: 1px solid #555;
border-radius: 6px;
padding: 6px;
font-size: 12px;
color: #0f0;
}
`;
document.head.appendChild(style);
};
const DevToolsOverlay = () => {
const [collapsed, setCollapsed] = useState(false);
const [logs, setLogs] = useState([]);
const overlayRef = useRef(null);
// Drag to move overlay
useRef(null);
useEffect(() => {
trackMemoryUsage();
interceptNetwork();
setLogs(getLogs());
}, []);
// Drag handlers for moving overlay
useEffect(() => {
const overlay = overlayRef.current;
if (!overlay)
return;
let isDragging = false;
let startX = 0, startY = 0, startLeft = 0, startTop = 0;
const header = overlay.querySelector(".perfkit-header");
if (!header)
return;
const onMouseDown = (e) => {
if (e.target.classList.contains("perfkit-toggle"))
return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = overlay.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
document.body.style.userSelect = "none";
};
const onMouseMove = (e) => {
if (!isDragging)
return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
overlay.style.left = `${startLeft + dx}px`;
overlay.style.top = `${startTop + dy}px`;
overlay.style.right = "auto";
overlay.style.bottom = "auto";
};
const onMouseUp = () => {
isDragging = false;
document.body.style.userSelect = "";
};
header.addEventListener("mousedown", onMouseDown);
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp);
return () => {
header.removeEventListener("mousedown", onMouseDown);
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
};
}, []);
return (jsxs("div", { className: "perfkit-overlay", ref: overlayRef, style: { resize: "both", overflow: "auto" }, children: [jsxs("div", { className: "perfkit-header", children: [jsx("h4", { children: "\uD83D\uDE80 PerfKit" }), jsx("button", { className: "perfkit-toggle", onClick: () => setCollapsed(!collapsed), title: collapsed ? "Expand" : "Collapse", children: collapsed ? "▶️" : "🔽" })] }), !collapsed && (jsxs("div", { className: "perfkit-body", children: [jsxs("div", { className: "perfkit-section", children: [jsx("h5", { children: "\uD83E\uDDE0 Memory" }), jsx("button", { onClick: trackMemoryUsage, children: "Check Now" })] }), jsxs("div", { className: "perfkit-section", children: [jsx("h5", { children: "\uD83D\uDD25 Render & Logs" }), jsx("button", { onClick: downloadLogs, children: "Download Logs" }), jsx("div", { className: "perfkit-log-preview", children: jsx("pre", { children: JSON.stringify(logs.slice(-3), null, 2) }) })] })] }))] }));
};
const initPerfKit = () => {
injectOverlayFallbackStyles();
const container = document.createElement("div");
document.body.appendChild(container);
import('react-dom/client').then(({ createRoot }) => {
const root = createRoot(container);
root.render(jsx(DevToolsOverlay, {}));
});
};
const useRenderHeatmap = (label) => {
const startMark = `${label}-start`;
const endMark = `${label}-end`;
performance.mark(startMark);
useEffect(() => {
performance.mark(endMark);
performance.measure(label, startMark, endMark);
const [measure] = performance.getEntriesByName(label);
if (measure.duration > 16) {
console.warn(`[PerfKit] ${label} took ${measure.duration.toFixed(2)}ms to render.`);
const elem = document.querySelector(`[data-perf-label="${label}"]`);
if (elem) {
elem.style.outline = "2px solid red";
elem.title = `Render Time: ${measure.duration.toFixed(2)}ms`;
}
}
return () => {
performance.clearMarks(startMark);
performance.clearMarks(endMark);
performance.clearMeasures(label);
};
}, []);
};
/**
* Custom hook to track JS heap memory usage in the browser.
* Uses `performance.memory` (Chrome only) and logs at a fixed interval.
*/
const useMemoryTracker = (intervalMs = 5000) => {
const intervalRef = useRef(null);
useEffect(() => {
if (!performance.memory) {
console.warn("[PerfKit] Memory tracking is not supported in this browser.");
return;
}
const trackMemory = () => {
const memory = performance.memory;
const used = (memory.usedJSHeapSize / 1048576).toFixed(2); // MB
const total = (memory.totalJSHeapSize / 1048576).toFixed(2);
const limit = (memory.jsHeapSizeLimit / 1048576).toFixed(2);
console.log(`[PerfKit] Memory usage: ${used} MB / ${total} MB (limit: ${limit} MB)`);
addLog("memory", "HeapUsage", parseFloat(used), {
totalMB: parseFloat(total),
limitMB: parseFloat(limit),
});
};
intervalRef.current = setInterval(trackMemory, intervalMs);
trackMemory(); // run immediately once
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [intervalMs]);
};
/**
* usePropDebugger
* Logs prop changes between renders for debugging unnecessary re-renders.
*
* @param props - The props object to watch
* @param label - Optional label for easier identification in logs
*
* Example:
* usePropDebugger(props, "MyComponent");
*/
const usePropDebugger = (props, label = "Component") => {
const prevProps = useRef(null);
useEffect(() => {
if (prevProps.current) {
const changedProps = {};
const allKeys = new Set([
...Object.keys(prevProps.current),
...Object.keys(props),
]);
allKeys.forEach((key) => {
if (prevProps.current[key] !== props[key]) {
changedProps[key] = {
prev: prevProps.current[key],
next: props[key],
};
}
});
if (Object.keys(changedProps).length > 0) {
console.log(`[PerfKit] [${label}] Prop changes:`, changedProps);
addLog("custom", `${label} prop change`, undefined, changedProps);
}
}
else {
// Initial mount
console.log(`[PerfKit] [${label}] Initial props:`, props);
addLog("custom", `${label} initial props`, undefined, props);
}
prevProps.current = props;
}, [props, label]);
};
let memoryThresholdMB = 100; // Default: 100 MB
let repeatedNetworkThreshold = 5; // Same URL fetched X times within Y seconds
let alertHandler = null;
const networkCallTimestamps = {};
/**
* Registers a callback to be called when an alert condition is triggered.
*/
const registerAlertHandler = (callback) => {
alertHandler = callback;
};
/**
* Sets the memory threshold in MB
*/
const setMemoryThreshold = (mb) => {
memoryThresholdMB = mb;
};
/**
* Sets the number of repeated calls allowed for the same URL before triggering a network alert
*/
const setRepeatedNetworkThreshold = (count) => {
repeatedNetworkThreshold = count;
};
/**
* Tracks memory and alerts if threshold is exceeded
*/
const checkMemoryUsage = () => {
if (performance.memory) {
const memory = performance.memory;
const usedMB = memory.usedJSHeapSize / 1048576;
if (usedMB > memoryThresholdMB && alertHandler) {
alertHandler(`[PerfKit] Memory usage exceeded: ${usedMB.toFixed(2)} MB`, "memory", { usedMB });
}
}
};
/**
* Tracks network requests and triggers alert if a URL is hit repeatedly in a short time.
*/
const trackNetworkRequest = (url) => {
const now = Date.now();
const windowMs = 10000; // 10 seconds
if (!networkCallTimestamps[url]) {
networkCallTimestamps[url] = [];
}
// Keep only timestamps within window
networkCallTimestamps[url] = networkCallTimestamps[url].filter((t) => now - t < windowMs);
networkCallTimestamps[url].push(now);
if (networkCallTimestamps[url].length > repeatedNetworkThreshold &&
alertHandler) {
alertHandler(`[PerfKit] Repeated network call: ${url} (${networkCallTimestamps[url].length} times in 10s)`, "network", { url, count: networkCallTimestamps[url].length });
}
};
export { DevToolsOverlay, addLog, checkMemoryUsage, clearLogs, downloadLogs, getLogs, initPerfKit, injectOverlayFallbackStyles, interceptNetwork, registerAlertHandler, setMemoryThreshold, setRepeatedNetworkThreshold, trackMemoryUsage, trackNetworkRequest, useMemoryTracker, usePropDebugger, useRenderHeatmap };
//# sourceMappingURL=index.js.map