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