UNPKG

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