Hibernate Idle Tabs

If a tab is unused for a long time, it switches to a light holding page until the tab is focused again. This helps the browser to recover memory, and can speed up the re-opening of large sessions.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name           Hibernate Idle Tabs
// @namespace      HIT
// @description    If a tab is unused for a long time, it switches to a light holding page until the tab is focused again.  This helps the browser to recover memory, and can speed up the re-opening of large sessions.
// @version        1.3.0
// @downstreamURL  http://userscripts.org/scripts/source/123252.user.js
// @include        *
// ==/UserScript==


/* +++ Config +++ */

var hibernateIfIdleForMoreThan = 36*60*60; // 36 hours
var restoreTime = 0.5; // in seconds

// We need an always-available basically blank HTML page we can navigate to
// when we hibernate a tab.  The userscript will run on that page, and await
// re-activation.
//
// This page is not really ideal, since it provides an image and unneeded CSS.
//
// Also NOTE FOR SECURITY that whatever page you navigate to, the server admin
// will be able to see which page you hibernated, in their logfile!
//
// If you have a blank page somewhere on the net, belonging to an admin you
// trust, I recommend using that instead.
//
var holdingPage = "https://www.google.com/hibernated_tab";
// Throws error: Not allowed to navigate top frame to data URL
//var holdingPage = "data:text/html,<html><head><title>Hibernate Idle Tabs</title></head><body></body></html>";

// If you do change the holding, put the old one here, so that any existing
// hibernated tabs still on the old page will be able to unhibernate.
//
var oldHoldingPages = ["http://neuralyte.org/~joey/hibernated_tab.html", "http://www.google.com/hibernated_tab", "https://www.google.com/hibernated_tab"];

var passFaviconToHoldingPage = true;
var fadeHibernatedFavicons = true;

var forceHibernateWhenRunTwice = true;



/* +++ Documentation +++ */
//
// On a normal page, checks to see if the user goes idle.  (Mouse movements,
// key actions and page focus reset the idle timer.)  If the page is left idle
// past the timeout, then the window navigates to a lighter holding page,
// hopefully freeing up memory in the browser.
//
// On a holding page, if the user focuses the window, the window navigates back
// to the original page.  (This can be cancelled in Chrome by clicking on
// another tab, but not by paging to another tab with the keyboard!)
//
// I think single-click is also a cancellation now.
//
// In order for the tab of the holding page to present the same favicon as the
// original page, we must capture this image before leaving the original page,
// and pass it to the holding page as a CGI parameter.
//
// (A simpler alternative might be to aim for a 404 on the same domain and use
// that as the holding page.)
//
// If you use Google Chrome or Chromium, then I would recommend using this
// extension instead, which provides exactly the same functionality, but with
// better security and probaly better performance too:
//
// https://chrome.google.com/webstore/detail/the-great-suspender/klbibkeccnjlkjkiokjodocebajanakg


// (TODO: This aforementioned security concern could probably be fixed by passing data to the target page using # rather than ? - although it would only prevent the data from being passed over HTTP, but Javascript running on the target page could still read it.)
//
// Sadly, userscripts do not run on about:blank in Firefox 6.0 or Chromium 2011.  I doubt a file:///... URL would work either.

// BUG: Sometimes when un-hibernating, the webserver of the page we return to
// complains that the referrer URL header is too long!

// TODO: Some users may want the hibernated page to restore immediately when the tab is *refocused*, rather than waiting for a mouseover.

// TESTING: Expose a function to allow a bookmarklet to force-hibernate the current tab?

// CONSIDER: If we forget about fading the favicon, couldn't we simplify things by just sending the favicon URL rather than its image data?  I think I tested this, and although I could load the favicon into the document, I was not successful at getting it into the browser's title tab by adding a new <link rel="icon">.



/* +++ Main +++ */

var onHoldingPage = document.location.href.match(holdingPage+"?") != null;

// If you change holding page, this keeps the old one working for a while, for
// the sake of running browsers or saved sessions.
oldHoldingPages.forEach(oldHoldingPage => {
	if (document.location.href.match(oldHoldingPage+"?")) {
		onHoldingPage = true;
	}
});

function handleNormalPage() {

	whenIdleDo(hibernateIfIdleForMoreThan,hibernatePage);

	function hibernatePage() {

		var params = {
			title: document.title,
			url: document.location.href
		};

		function processFavicon(canvas) {
			document.body.appendChild(canvas);
			if (canvas) {
				try {
					if (fadeHibernatedFavicons) {
						makeCanvasMoreTransparent(canvas);
					}
					var faviconDataURL = canvas.toDataURL("image/png");
					params.favicon_data = faviconDataURL;
				} catch (e) {
					var extra = ( window != top ? " (running in frame or iframe)" : "" );
					console.error("[HIT] Got error"+extra+": "+e+" doc.loc="+document.location.href);
					// We get "Error: SECURITY_ERR: DOM Exception 18" (Chrome) if
					// the favicon is from a different host.
				}
			}
			reallyHibernatePage();
		}

		function reallyHibernatePage() {
			var queryString = buildQueryParameterString(params);
			document.location = holdingPage + "?" + queryString;
		}

		if (passFaviconToHoldingPage) {
			// I don't know how to grab the contents of the current favicon, so we
			// try to directly load a copy for ourselves.
			var url = document.location.href;
			var targetHost = url.replace(/.*:\/\//,'').replace(/\/.*/,'');
			loadFaviconIntoCanvas(targetHost,processFavicon);
		} else {
			reallyHibernatePage();
		}

	}

	function makeCanvasMoreTransparent(canvas) {
		var wid = canvas.width;
		var hei = canvas.height;
		var ctx = canvas.getContext("2d");
		var img = ctx.getImageData(0,0,wid,hei);
		var data = img.data;
		var len = 4*wid*hei;
		for (var ptr=0;ptr<len;ptr+=4) {
			data[ptr+3] /= 4;  // alpha channel
		}
		// May or may not be needed:
		ctx.putImageData(img,0,0);
	}



	if (forceHibernateWhenRunTwice) {
		if (window.hibernate_idle_tabs_loaded) {
			hibernatePage();
		}
		window.hibernate_idle_tabs_loaded = true;
	}


}

function handleHoldingPage() {

	var params = getQueryParameters();

	// setHibernateStatus("Holding page for " + params.title + "\n with URL: "+params.url);
	// var titleReport = params.title + " (Holding Page)";
	var titleReport = "(" + (params.title || params.url) + " :: Hibernated)";
	setWindowTitle(titleReport);

	var mainReport = titleReport;
	if (params.title) {
		/*
		statusElement.appendChild(document.createElement("P"));
		var div = document.createElement("tt");
		div.style.fontSize = "0.8em";
		div.appendChild(document.createTextNode(params.url));
		statusElement.appendChild(div);
		*/
		mainReport += "\n" + params.url;
	}

	setHibernateStatus(mainReport);

	try {
		var faviconDataURL = params.favicon_data;
		if (!faviconDataURL) {
			// If we do not have a favicon, it is preferable to present an empty/transparent favicon, rather than let the browser show the favicon of the holding page site!
			faviconDataURL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAEklEQVQ4jWNgGAWjYBSMAggAAAQQAAF/TXiOAAAAAElFTkSuQmCC";
		}
		writeFaviconFromDataString(faviconDataURL);
	} catch (e) {
		console.error(""+e);
	}

	function restoreTab(evt) {
		var url = decodeURIComponent(params.url);
		setHibernateStatus("Returning to: "+url);
		document.location.replace(url);
		/*
		// Alternative; preserves "forward"
		window.history.back();  // TESTING!  With the fallback below, this seemed to work 90% of the time?
		// Sometimes it doesn't work.  So we fallback to old method:
		setTimeout(function(){
			setHibernateStatus("window.history.back() FAILED - setting document.location");
			setTimeout(function(){
				document.location.replace(url);   // I once saw this put ':'s when it should have put '%35's or whatever.  (That broke 'Up' bookmarklet.)
			},1000);
		},2500);
		*/
		evt.preventDefault();   // Accept responsibility for the double-click.
		return false;   // Prevent browser from doing anything else with it (e.g. selecting the word under the cursor).
	}

	checkForActivity();

	function checkForActivity() {

		var countdownTimer = null;

		// listen(document.body,'mousemove',startCountdown); // In Firefox this ignore mousemove on empty space (outside the document content), so trying window...
		listen(window,'mousemove',startCountdown); // Likewise for click below!
		// listen(document.body,'blur',clearCountdown); // Does not fire in Chrome?
		listen(window,'blur',clearCountdown); // For Chrome
		//listen(window,'mouseout',clearCountdown); // Firefox appears to be firing this when my mouse is still over the window, preventing navigation!  Let's just rely on 'blur' instead.
		// listen(document.body,'click',clearCountdown);
		listen(window,'click',clearCountdown);
		listen(window,'dblclick',restoreTab);

		function startCountdown(e) {
			if (countdownTimer != null) {
				// There is already a countdown running - do not start.
				return;
			}
			var togo = restoreTime*1000;
			function countDown() {
				setHibernateStatus(mainReport +
						'\n' + "Tab will restore in "+(togo/1000).toFixed(1)+" seconds." +
						'  ' + "Click or defocus to pause." +
						'  ' + "Or double click to restore now!"
						);
				if (togo <= 0) {
					restoreTab();
				} else {
					togo -= 1000;
					if (countdownTimer)
						clearTimeout(countdownTimer);
					countdownTimer = setTimeout(countDown,1000);
				}
			}
			countDown();
		}

		function clearCountdown(ev) {
			if (countdownTimer) {
				clearTimeout(countdownTimer);
			}
			countdownTimer = null;
			var evReport = "";
			if (ev) {
				evReport = " by "+ev.type+" on "+this;
			}
			var report = mainReport + '\n' + "Paused" + evReport + "";
			setHibernateStatus(report);
		}

	}

}

if (onHoldingPage) {
	handleHoldingPage();
} else {
	handleNormalPage();
}



/* +++ Library Functions +++ */

function listen(target,eventType,handler,capture) {
	target.addEventListener(eventType,handler,capture);
}

function ignore(target,eventType,handler,capture) {
	target.removeEventListener(eventType,handler,capture);
}

// Given an object, encode its properties and values into a URI-ready CGI query string.
function buildQueryParameterString(params) {
	return Object.keys(params).map( function(key) { return key+"="+encodeURIComponent(params[key]); } ).join("&");
}

// Returns an object whose keys and values match those of the CGI query string of the current document.
function getQueryParameters() {
	var queryString = document.location.search;
	var params = {};
	queryString.replace(/^\?/,'').split("&").map( function(s) {
		var part = s.split("=");
		var key = part[0];
		var value = decodeURIComponent(part[1]);
		params[key] = value;
	});
	return params;
}

function whenIdleDo(idleTimeoutSecs,activateIdleEvent) {

	var timer = null;
	var pageLastUsed = new Date().getTime();

	function setNotIdle() {
		pageLastUsed = new Date().getTime();
	}

	function checkForIdle() {
		var msSinceLastUsed = new Date().getTime() - pageLastUsed;
		if (msSinceLastUsed > idleTimeoutSecs * 1000) {
			activateIdleEvent();
		}
		setTimeout(checkForIdle,idleTimeoutSecs/5*1000);
	}

	setTimeout(checkForIdle,idleTimeoutSecs*1000);

	listen(document.body,'mousemove',setNotIdle);
	listen(document.body,'focus',setNotIdle);
	listen(document.body,'keydown',setNotIdle);

}



/* +++ Local Convenience Functions +++ */

var statusElement = null;
function checkStatusElement() {
	if (!statusElement) {
		while (document.body.firstChild) {
			document.body.removeChild(document.body.firstChild);
		}
		statusElement = document.createElement("div");
		document.body.insertBefore(statusElement,document.body.firstChild);
		statusElement.style.textAlign = "center";
	}
}

function setWindowTitle(msg) {
	msg = ""+msg;
	document.title = msg;
}

function setHibernateStatus(msg) {
	msg = ""+msg;
	checkStatusElement();
	statusElement.textContent = msg;
	statusElement.innerText   = msg;   // IE
	// Currently '\n' works in Chrome, but not in Firefox.
}






/* +++ Favicon, Canvas and DataURL Magic +++ */

function loadFaviconForHost(targetHost,callback) {

	// Try to load a favicon image for the given host, and pass it to callback.
	// Except: If there is a link with rel="icon" in the page, with host
	// matching the current page location, load that image file instead of
	// guessing the extension!

	var favicon = document.createElement('img');
	favicon.addEventListener('load',function() {
		callback(favicon);
	});

	var targetProtocol = document.location.protocol || "http:";

	// If there is a <link rel="icon" ...> in the current page, then I think that overrides the site-global favicon.
	// NOTE: This is not appropriate if a third party targetHost was requested, only if they really wanted the favicon for the current page!
	var foundLink = null;
	var linkElems = document.getElementsByTagName("link");
	for (var i=0;i<linkElems.length;i++) {
		var link = linkElems[i];
		if (link.rel === "icon" || link.rel === "shortcut icon") {
			// Since we can't read the image data of images from third-party hosts, we skip them and keep searching.
			if (link.host == document.location.host) {
				foundLink = link;
				break;
			}
		}
	}
	if (foundLink) {
		favicon.addEventListener('error',function(){ callback(favicon); });
		favicon.src = foundLink.href;   // Won't favicon.onload cause an additional callback to the one below?
		// NOTE: If we made the callback interface pass favicon as 'this' rather than an argument, then we wouldn't need to wrap it here (the argument may be evt).
		favicon = foundLink;
		callback(favicon);
		return;
	}

	var extsToTry = ["jpg","gif","png","ico"];   // iterated in reverse order
	function tryNextExtension() {
		var ext = extsToTry.pop();
		if (ext == null) {
			console.log("Ran out of extensions to try for "+targetHost+"/favicon.???");
			// We run the callback anyway!
			callback(null);
		} else {
			favicon.src = targetProtocol+"//"+targetHost+"/favicon."+ext;
		}
	}
	favicon.addEventListener('error',tryNextExtension);
	tryNextExtension();
	// When the favicon is working we can remove the canvas, but until then we may as well keep it visible!
}

function writeFaviconFromCanvas(canvas) {
	var faviconDataURL = canvas.toDataURL("image/png");
	// var faviconDataURL = canvas.toDataURL("image/x-icon;base64");
	// console.log("Got data URL: "+faviconDataURL.substring(0,10+"... (length "+faviconDataURL.length+")");
	writeFaviconFromDataString(faviconDataURL);
}

function writeFaviconFromDataString(faviconDataURL) {

	var d = document, h = document.getElementsByTagName('head')[0];

	// Create this favicon
	var ss = d.createElement('link');
	ss.rel = 'shortcut icon';
	ss.type = 'image/x-icon';
	ss.href = faviconDataURL;
	/*
	// Remove any existing favicons
	var links = h.getElementsByTagName('link');
	for (var i=0; i<links.length; i++) {
		if (links[i].href == ss.href) return;
		if (links[i].rel == "shortcut icon" || links[i].rel=="icon")
			h.removeChild(links[i]);
	}
	*/
	// Add this favicon to the head
	h.appendChild(ss);

	// Force browser to acknowledge
	// I saw this trick somewhere.  I don't know what browser requires it.  But I just left it in!
	var shim = document.createElement('iframe');
	shim.width = shim.height = 0;
	document.body.appendChild(shim);
	shim.src = "icon";
	document.body.removeChild(shim);

}

function loadFaviconIntoCanvas(targetHost,callback) {

	// console.log("Getting favicon for: "+targetHost);

	var canvas = document.createElement('canvas');
	var ctx = canvas.getContext('2d');

	loadFaviconForHost(targetHost,gotFavicon);

	function gotFavicon(favicon) {
		if (favicon) {
			// console.log("Got favicon from: "+favicon.src);
			canvas.width = favicon.width;
			canvas.height = favicon.height;
			ctx.drawImage( favicon, 0, 0 );
		}
		callback(canvas);
	}

}

/* This throws a security error from canvas.toDataURL(), I think because we are
trying to read something from a different domain than the script!
In Chrome: "SECURITY_ERR: DOM Exception 18"
*/
// loadFaviconIntoCanvas(document.location.host,writeFaviconFromCanvas);