Webpage Size Summary with Dual Calculation Overlay (Dynamic, URL Change Refresh)

Displays page performance details grouped by resource type. The very first (initial) calculation is stored and subsequent updates are stored separately. The overlay shows a side‑by‑side comparison (Initial / Updated) for Count, Encoded Size, and Transfer Size. On URL changes the initial data is reset.

目前为 2025-02-09 提交的版本。查看 最新版本

// ==UserScript==
// @name         Webpage Size Summary with Dual Calculation Overlay (Dynamic, URL Change Refresh)
// @namespace    http://tampermonkey.net/
// @version      2.0 
// @description  Displays page performance details grouped by resource type. The very first (initial) calculation is stored and subsequent updates are stored separately. The overlay shows a side‑by‑side comparison (Initial / Updated) for Count, Encoded Size, and Transfer Size. On URL changes the initial data is reset.
// @match        *://*/*
// @grant        none
// @run-at       document-end
// @license      free
// @author       pawag
// ==/UserScript==

(function() {
    'use strict';

    // Global variables to store the first (initial) calculation and the latest update.
    let initialCalculation = null;
    let latestCalculation = null;
    // Global variable to track the current URL.
    let currentUrl = window.location.href;

    // ------------------------------------------------------------
    // Function: bytesToSize
    // Purpose: Converts a number of bytes into a human‑readable string.
    // ------------------------------------------------------------
    function bytesToSize(bytes) {
        if (bytes < 1024) return bytes + ' B';
        const k = 1024;
        const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
        const i = Math.floor(Math.log(bytes) / Math.log(k));
        return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
    }

    // ------------------------------------------------------------
    // Function: calculatePageSize
    // Purpose: Retrieves performance data from the Performance API and groups resources by type.
    // For each entry, if encodedBodySize is a number greater than 0, that value is used for "size"; otherwise transferSize is used.
    // Returns an object with totalSize and groups.
    // ------------------------------------------------------------
    function calculatePageSize() {
        let totalSize = 0;
        const groups = {
            html: { count: 0, size: 0, transfer: 0 },
            script: { count: 0, size: 0, transfer: 0 },
            img: { count: 0, size: 0, transfer: 0 },
            css: { count: 0, size: 0, transfer: 0 },
            xmlhttprequest: { count: 0, size: 0, transfer: 0 },
            other: { count: 0, size: 0, transfer: 0 }
        };

        // Process the main document (navigation entry).
        const navEntries = performance.getEntriesByType("navigation");
        if (navEntries && navEntries.length > 0) {
            const nav = navEntries[0];
            // If encodedBodySize exists and is > 0, use it; otherwise, use transferSize.
            const navEncoded = nav.encodedBodySize;
            const navSize = (typeof navEncoded === "number" && navEncoded > 0) ? navEncoded : (nav.transferSize || 0);
            const navTransfer = nav.transferSize || 0;
            totalSize += navSize;
            groups.html.count += 1;
            groups.html.size += navSize;
            groups.html.transfer += navTransfer;
        }

        // Process each resource entry.
        const resources = performance.getEntriesByType("resource");
        resources.forEach(entry => {
            const encoded = entry.encodedBodySize;
            const size = (typeof encoded === "number" && encoded > 0) ? encoded : (entry.transferSize || 0);
            const transfer = entry.transferSize || 0;
            totalSize += size;
            let type = entry.initiatorType ? entry.initiatorType.toLowerCase() : 'other';
            if (type === 'img' || type === 'image') type = 'img';
            else if (type === 'script') type = 'script';
            else if (type === 'css') type = 'css';
            else if (type === 'xmlhttprequest') type = 'xmlhttprequest';
            else type = 'other';
            groups[type].count += 1;
            groups[type].size += size;
            groups[type].transfer += transfer;
        });
        return { totalSize, groups };
    }

    // ------------------------------------------------------------
    // Function: createOverlay
    // Purpose: Creates the overlay element and displays a table comparing initial (I) and updated (U) calculations.
    // Parameters:
    //    firstData   - The initial calculation result.
    //    updatedData - The latest update result (if available).
    // ------------------------------------------------------------
    function createOverlay(firstData, updatedData) {
        // Remove any existing overlay(s).
        document.querySelectorAll("#performanceOverlay").forEach(el => el.parentNode.removeChild(el));

        const overlay = document.createElement('div');
        overlay.id = "performanceOverlay";
        overlay.style.position = 'fixed';
        overlay.style.bottom = '10px';
        overlay.style.right = '10px';
        overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
        overlay.style.color = '#fff';
        overlay.style.padding = '10px';
        overlay.style.fontFamily = 'Arial, sans-serif';
        overlay.style.fontSize = '14px';
        overlay.style.zIndex = '9999';
        overlay.style.borderRadius = '5px';
        overlay.style.cursor = 'pointer';
        overlay.style.maxWidth = '95%';
        overlay.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)';

        // Create a close button.
        const closeButton = document.createElement('span');
        closeButton.textContent = '×';
        closeButton.style.position = 'absolute';
        closeButton.style.top = '2px';
        closeButton.style.right = '4px';
        closeButton.style.cursor = 'pointer';
        closeButton.style.fontSize = '16px';
        closeButton.style.fontWeight = 'bold';
        closeButton.addEventListener('click', function(e) {
            overlay.parentNode.removeChild(overlay);
            e.stopPropagation();
        });
        overlay.appendChild(closeButton);

        // Display overall total (from the initial calculation).
        const totalText = document.createElement('div');
        totalText.textContent = "Total: " + bytesToSize(firstData.totalSize);
        totalText.style.marginRight = '20px';
        overlay.appendChild(totalText);

        // Create a container for the details table (initially hidden).
        const detailsContainer = document.createElement('div');
        detailsContainer.style.marginTop = '10px';
        detailsContainer.style.maxHeight = '300px';
        detailsContainer.style.overflowY = 'auto';
        detailsContainer.style.display = 'none';
        detailsContainer.style.fontSize = '12px';

        // Create the table.
        const table = document.createElement('table');
        table.style.width = '100%';
        table.style.borderCollapse = 'collapse';

        // Header row: Type, Count (I/U), Encoded Size (I/U), Transfer Size (I/U)
        const headerRow = document.createElement('tr');
        const headers = ["Type", "Count (I/U)", "Encoded Size (I/U)", "Transfer Size (I/U)"];
        headers.forEach(text => {
            const th = document.createElement('th');
            th.textContent = text;
            th.style.borderBottom = "1px solid #fff";
            th.style.padding = '2px 5px';
            th.style.textAlign = 'center';
            headerRow.appendChild(th);
        });
        table.appendChild(headerRow);

        // For each resource group (using keys from the initial calculation).
        const groupTypes = Object.keys(firstData.groups);
        groupTypes.forEach(type => {
            if (firstData.groups[type].count > 0) {
                const row = document.createElement('tr');

                // Friendly name.
                let displayType = type;
                if (type === 'html') displayType = "HTML";
                else if (type === 'script') displayType = "Script";
                else if (type === 'img') displayType = "Image";
                else if (type === 'css') displayType = "CSS";
                else if (type === 'xmlhttprequest') displayType = "XHR";
                else if (type === 'other') displayType = "Other";

                const typeCell = document.createElement('td');
                typeCell.textContent = displayType;
                typeCell.style.borderBottom = "1px solid #ccc";
                typeCell.style.padding = '2px 5px';
                typeCell.style.textAlign = 'left';
                row.appendChild(typeCell);

                // Helper: for a given field, get the initial and updated values.
                const getCellText = (field) => {
                    let initialVal = firstData.groups[type][field];
                    let updatedVal = (updatedData && updatedData.groups[type]) ? updatedData.groups[type][field] : null;
                    return bytesToSize(initialVal) + " / " + (updatedVal !== null ? bytesToSize(updatedVal) : "-");
                };

                // Count cell.
                const countCell = document.createElement('td');
                let initialCount = firstData.groups[type].count;
                let updatedCount = (updatedData && updatedData.groups[type]) ? updatedData.groups[type].count : null;
                countCell.textContent = initialCount + " / " + (updatedCount !== null ? updatedCount : "-");
                countCell.style.borderBottom = "1px solid #ccc";
                countCell.style.padding = '2px 5px';
                countCell.style.textAlign = 'center';
                row.appendChild(countCell);

                // Encoded Size cell.
                const encodedCell = document.createElement('td');
                // Here, the "size" field in our calculation represents the chosen encoded size (if > 0) or fallback.
                encodedCell.textContent = getCellText("size");
                encodedCell.style.borderBottom = "1px solid #ccc";
                encodedCell.style.padding = '2px 5px';
                encodedCell.style.textAlign = 'right';
                row.appendChild(encodedCell);

                // Transfer Size cell.
                const transferCell = document.createElement('td');
                const getTransferText = (field) => {
                    let initialVal = firstData.groups[type][field];
                    let updatedVal = (updatedData && updatedData.groups[type]) ? updatedData.groups[type][field] : null;
                    return bytesToSize(initialVal) + " / " + (updatedVal !== null ? bytesToSize(updatedVal) : "-");
                };
                transferCell.textContent = getTransferText("transfer");
                transferCell.style.borderBottom = "1px solid #ccc";
                transferCell.style.padding = '2px 5px';
                transferCell.style.textAlign = 'right';
                row.appendChild(transferCell);

                table.appendChild(row);
            }
        });

        // Overall totals.
        const overall = { count: { i:0, u:0 }, size: { i:0, u:0 }, transfer: { i:0, u:0 } };
        for (let key in firstData.groups) {
            overall.count.i += firstData.groups[key].count;
            overall.size.i += firstData.groups[key].size;
            overall.transfer.i += firstData.groups[key].transfer;
            if (updatedData && updatedData.groups[key]) {
                overall.count.u += updatedData.groups[key].count;
                overall.size.u += updatedData.groups[key].size;
                overall.transfer.u += updatedData.groups[key].transfer;
            }
        }
        const totalRow = document.createElement('tr');
        const totalLabelCell = document.createElement('td');
        totalLabelCell.textContent = "TOTAL";
        totalLabelCell.style.fontWeight = "bold";
        totalLabelCell.style.padding = '2px 5px';
        totalLabelCell.style.borderTop = "2px solid #fff";
        totalLabelCell.style.textAlign = 'left';
        totalRow.appendChild(totalLabelCell);

        const totalCountCell = document.createElement('td');
        totalCountCell.textContent = overall.count.i + " / " + (updatedData ? overall.count.u : "-");
        totalCountCell.style.fontWeight = "bold";
        totalCountCell.style.padding = '2px 5px';
        totalCountCell.style.borderTop = "2px solid #fff";
        totalCountCell.style.textAlign = 'center';
        totalRow.appendChild(totalCountCell);

        const totalSizeCell = document.createElement('td');
        totalSizeCell.textContent = bytesToSize(overall.size.i) + " / " + (updatedData ? bytesToSize(overall.size.u) : "-");
        totalSizeCell.style.fontWeight = "bold";
        totalSizeCell.style.padding = '2px 5px';
        totalSizeCell.style.borderTop = "2px solid #fff";
        totalSizeCell.style.textAlign = 'right';
        totalRow.appendChild(totalSizeCell);

        const totalTransferCell = document.createElement('td');
        totalTransferCell.textContent = bytesToSize(overall.transfer.i) + " / " + (updatedData ? bytesToSize(overall.transfer.u) : "-");
        totalTransferCell.style.fontWeight = "bold";
        totalTransferCell.style.padding = '2px 5px';
        totalTransferCell.style.borderTop = "2px solid #fff";
        totalTransferCell.style.textAlign = 'right';
        totalRow.appendChild(totalTransferCell);
        table.appendChild(totalRow);

        detailsContainer.appendChild(table);
        overlay.appendChild(detailsContainer);

        // Toggle details on overlay click (except when clicking the close button).
        overlay.addEventListener('click', function(e) {
            detailsContainer.style.display = detailsContainer.style.display === 'none' ? 'block' : 'none';
            e.stopPropagation();
        });

        document.body.appendChild(overlay);
    }

    // ------------------------------------------------------------
    // Function: createErrorOverlay
    // Purpose: Creates an overlay to display an error message.
    // ------------------------------------------------------------
    function createErrorOverlay(message) {
        const overlay = document.createElement('div');
        overlay.id = "performanceOverlay";
        overlay.style.position = 'fixed';
        overlay.style.bottom = '10px';
        overlay.style.right = '10px';
        overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
        overlay.style.color = '#fff';
        overlay.style.padding = '10px';
        overlay.style.fontFamily = 'Arial, sans-serif';
        overlay.style.fontSize = '14px';
        overlay.style.zIndex = '9999';
        overlay.style.borderRadius = '5px';
        overlay.style.cursor = 'default';
        overlay.style.maxWidth = '90%';
        overlay.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)';

        const closeButton = document.createElement('span');
        closeButton.textContent = '×';
        closeButton.style.position = 'absolute';
        closeButton.style.top = '2px';
        closeButton.style.right = '4px';
        closeButton.style.cursor = 'pointer';
        closeButton.style.fontSize = '16px';
        closeButton.style.fontWeight = 'bold';
        closeButton.addEventListener('click', function(e) {
            overlay.parentNode.removeChild(overlay);
            e.stopPropagation();
        });
        overlay.appendChild(closeButton);

        const errorText = document.createElement('div');
        errorText.textContent = message;
        overlay.appendChild(errorText);

        document.body.appendChild(overlay);
    }

    // ------------------------------------------------------------
    // Function: initScript
    // Purpose: Checks for Performance API support, gathers performance data,
    // and updates the overlay. On the first run the result is stored as the initial calculation;
    // on subsequent runs the new result is stored as the updated calculation.
    // ------------------------------------------------------------
    function initScript() {
        if (!window.performance || typeof performance.getEntriesByType !== 'function') {
            createErrorOverlay("Your browser does not support the Performance API required for this tool.");
            return;
        }
        setTimeout(() => {
            const result = calculatePageSize();
            if (!initialCalculation) {
                initialCalculation = result;
            } else {
                latestCalculation = result;
            }
            createOverlay(initialCalculation, latestCalculation);
        }, 1500);
    }

    // ------------------------------------------------------------
    // Function: onUrlChange
    // Purpose: Checks if the URL has changed; if so, resets the stored calculations.
    // Then removes any existing overlay, clears old performance data, and reinitializes.
    // ------------------------------------------------------------
    function onUrlChange() {
        if (window.location.href !== currentUrl) {
            initialCalculation = null;
            latestCalculation = null;
            currentUrl = window.location.href;
        }
        document.querySelectorAll("#performanceOverlay").forEach(el => el.parentNode.removeChild(el));
        if (performance.clearResourceTimings) {
            performance.clearResourceTimings();
        }
        setTimeout(initScript, 2000);
    }

    // ------------------------------------------------------------
    // Function: overrideHistoryMethods
    // Purpose: Overrides history.pushState/replaceState and listens for popstate events so that URL changes trigger onUrlChange.
    // ------------------------------------------------------------
    function overrideHistoryMethods() {
        const originalPushState = history.pushState;
        history.pushState = function() {
            originalPushState.apply(history, arguments);
            onUrlChange();
        };
        const originalReplaceState = history.replaceState;
        history.replaceState = function() {
            originalReplaceState.apply(history, arguments);
            onUrlChange();
        };
        window.addEventListener('popstate', onUrlChange);
    }

    // ------------------------------------------------------------
    // Function: monitorDynamicContent
    // Purpose: Uses a MutationObserver to detect when new content is added (e.g., via infinite scroll)
    // and then triggers onUrlChange after a debounce delay.
    // ------------------------------------------------------------
    function monitorDynamicContent() {
        const observer = new MutationObserver((mutationsList) => {
            let nodesAdded = false;
            for (const mutation of mutationsList) {
                if (mutation.addedNodes && mutation.addedNodes.length > 0) {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE && node.id === "performanceOverlay") {
                            continue;
                        }
                        nodesAdded = true;
                        break;
                    }
                }
                if (nodesAdded) break;
            }
            if (nodesAdded) {
                if (window.dynamicContentTimeout) {
                    clearTimeout(window.dynamicContentTimeout);
                }
                window.dynamicContentTimeout = setTimeout(() => {
                    onUrlChange();
                }, 2000);
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // ------------------------------------------------------------
    // Run initialization, override history methods, and monitor dynamic content.
    // ------------------------------------------------------------
    if (document.readyState === "complete") {
        initScript();
    } else {
        window.addEventListener('load', initScript);
    }
    overrideHistoryMethods();
    monitorDynamicContent();

})(); // End of IIFE

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址