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.

  1. // ==UserScript==
  2. // @name Hibernate Idle Tabs
  3. // @namespace HIT
  4. // @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.
  5. // @version 1.3.0
  6. // @downstreamURL http://userscripts.org/scripts/source/123252.user.js
  7. // @include *
  8. // ==/UserScript==
  9.  
  10.  
  11. /* +++ Config +++ */
  12.  
  13. var hibernateIfIdleForMoreThan = 36*60*60; // 36 hours
  14. var restoreTime = 0.5; // in seconds
  15.  
  16. // We need an always-available basically blank HTML page we can navigate to
  17. // when we hibernate a tab. The userscript will run on that page, and await
  18. // re-activation.
  19. //
  20. // This page is not really ideal, since it provides an image and unneeded CSS.
  21. //
  22. // Also NOTE FOR SECURITY that whatever page you navigate to, the server admin
  23. // will be able to see which page you hibernated, in their logfile!
  24. //
  25. // If you have a blank page somewhere on the net, belonging to an admin you
  26. // trust, I recommend using that instead.
  27. //
  28. var holdingPage = "https://www.google.com/hibernated_tab";
  29. // Throws error: Not allowed to navigate top frame to data URL
  30. //var holdingPage = "data:text/html,<html><head><title>Hibernate Idle Tabs</title></head><body></body></html>";
  31.  
  32. // If you do change the holding, put the old one here, so that any existing
  33. // hibernated tabs still on the old page will be able to unhibernate.
  34. //
  35. var oldHoldingPages = ["http://neuralyte.org/~joey/hibernated_tab.html", "http://www.google.com/hibernated_tab", "https://www.google.com/hibernated_tab"];
  36.  
  37. var passFaviconToHoldingPage = true;
  38. var fadeHibernatedFavicons = true;
  39.  
  40. var forceHibernateWhenRunTwice = true;
  41.  
  42.  
  43.  
  44. /* +++ Documentation +++ */
  45. //
  46. // On a normal page, checks to see if the user goes idle. (Mouse movements,
  47. // key actions and page focus reset the idle timer.) If the page is left idle
  48. // past the timeout, then the window navigates to a lighter holding page,
  49. // hopefully freeing up memory in the browser.
  50. //
  51. // On a holding page, if the user focuses the window, the window navigates back
  52. // to the original page. (This can be cancelled in Chrome by clicking on
  53. // another tab, but not by paging to another tab with the keyboard!)
  54. //
  55. // I think single-click is also a cancellation now.
  56. //
  57. // In order for the tab of the holding page to present the same favicon as the
  58. // original page, we must capture this image before leaving the original page,
  59. // and pass it to the holding page as a CGI parameter.
  60. //
  61. // (A simpler alternative might be to aim for a 404 on the same domain and use
  62. // that as the holding page.)
  63. //
  64. // If you use Google Chrome or Chromium, then I would recommend using this
  65. // extension instead, which provides exactly the same functionality, but with
  66. // better security and probaly better performance too:
  67. //
  68. // https://chrome.google.com/webstore/detail/the-great-suspender/klbibkeccnjlkjkiokjodocebajanakg
  69.  
  70.  
  71. // (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.)
  72. //
  73. // Sadly, userscripts do not run on about:blank in Firefox 6.0 or Chromium 2011. I doubt a file:///... URL would work either.
  74.  
  75. // BUG: Sometimes when un-hibernating, the webserver of the page we return to
  76. // complains that the referrer URL header is too long!
  77.  
  78. // TODO: Some users may want the hibernated page to restore immediately when the tab is *refocused*, rather than waiting for a mouseover.
  79.  
  80. // TESTING: Expose a function to allow a bookmarklet to force-hibernate the current tab?
  81.  
  82. // 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">.
  83.  
  84.  
  85.  
  86. /* +++ Main +++ */
  87.  
  88. var onHoldingPage = document.location.href.match(holdingPage+"?") != null;
  89.  
  90. // If you change holding page, this keeps the old one working for a while, for
  91. // the sake of running browsers or saved sessions.
  92. oldHoldingPages.forEach(oldHoldingPage => {
  93. if (document.location.href.match(oldHoldingPage+"?")) {
  94. onHoldingPage = true;
  95. }
  96. });
  97.  
  98. function handleNormalPage() {
  99.  
  100. whenIdleDo(hibernateIfIdleForMoreThan,hibernatePage);
  101.  
  102. function hibernatePage() {
  103.  
  104. var params = {
  105. title: document.title,
  106. url: document.location.href
  107. };
  108.  
  109. function processFavicon(canvas) {
  110. document.body.appendChild(canvas);
  111. if (canvas) {
  112. try {
  113. if (fadeHibernatedFavicons) {
  114. makeCanvasMoreTransparent(canvas);
  115. }
  116. var faviconDataURL = canvas.toDataURL("image/png");
  117. params.favicon_data = faviconDataURL;
  118. } catch (e) {
  119. var extra = ( window != top ? " (running in frame or iframe)" : "" );
  120. console.error("[HIT] Got error"+extra+": "+e+" doc.loc="+document.location.href);
  121. // We get "Error: SECURITY_ERR: DOM Exception 18" (Chrome) if
  122. // the favicon is from a different host.
  123. }
  124. }
  125. reallyHibernatePage();
  126. }
  127.  
  128. function reallyHibernatePage() {
  129. var queryString = buildQueryParameterString(params);
  130. document.location = holdingPage + "?" + queryString;
  131. }
  132.  
  133. if (passFaviconToHoldingPage) {
  134. // I don't know how to grab the contents of the current favicon, so we
  135. // try to directly load a copy for ourselves.
  136. var url = document.location.href;
  137. var targetHost = url.replace(/.*:\/\//,'').replace(/\/.*/,'');
  138. loadFaviconIntoCanvas(targetHost,processFavicon);
  139. } else {
  140. reallyHibernatePage();
  141. }
  142.  
  143. }
  144.  
  145. function makeCanvasMoreTransparent(canvas) {
  146. var wid = canvas.width;
  147. var hei = canvas.height;
  148. var ctx = canvas.getContext("2d");
  149. var img = ctx.getImageData(0,0,wid,hei);
  150. var data = img.data;
  151. var len = 4*wid*hei;
  152. for (var ptr=0;ptr<len;ptr+=4) {
  153. data[ptr+3] /= 4; // alpha channel
  154. }
  155. // May or may not be needed:
  156. ctx.putImageData(img,0,0);
  157. }
  158.  
  159.  
  160.  
  161. if (forceHibernateWhenRunTwice) {
  162. if (window.hibernate_idle_tabs_loaded) {
  163. hibernatePage();
  164. }
  165. window.hibernate_idle_tabs_loaded = true;
  166. }
  167.  
  168.  
  169. }
  170.  
  171. function handleHoldingPage() {
  172.  
  173. var params = getQueryParameters();
  174.  
  175. // setHibernateStatus("Holding page for " + params.title + "\n with URL: "+params.url);
  176. // var titleReport = params.title + " (Holding Page)";
  177. var titleReport = "(" + (params.title || params.url) + " :: Hibernated)";
  178. setWindowTitle(titleReport);
  179.  
  180. var mainReport = titleReport;
  181. if (params.title) {
  182. /*
  183. statusElement.appendChild(document.createElement("P"));
  184. var div = document.createElement("tt");
  185. div.style.fontSize = "0.8em";
  186. div.appendChild(document.createTextNode(params.url));
  187. statusElement.appendChild(div);
  188. */
  189. mainReport += "\n" + params.url;
  190. }
  191.  
  192. setHibernateStatus(mainReport);
  193.  
  194. try {
  195. var faviconDataURL = params.favicon_data;
  196. if (!faviconDataURL) {
  197. // 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!
  198. faviconDataURL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAEklEQVQ4jWNgGAWjYBSMAggAAAQQAAF/TXiOAAAAAElFTkSuQmCC";
  199. }
  200. writeFaviconFromDataString(faviconDataURL);
  201. } catch (e) {
  202. console.error(""+e);
  203. }
  204.  
  205. function restoreTab(evt) {
  206. var url = decodeURIComponent(params.url);
  207. setHibernateStatus("Returning to: "+url);
  208. document.location.replace(url);
  209. /*
  210. // Alternative; preserves "forward"
  211. window.history.back(); // TESTING! With the fallback below, this seemed to work 90% of the time?
  212. // Sometimes it doesn't work. So we fallback to old method:
  213. setTimeout(function(){
  214. setHibernateStatus("window.history.back() FAILED - setting document.location");
  215. setTimeout(function(){
  216. document.location.replace(url); // I once saw this put ':'s when it should have put '%35's or whatever. (That broke 'Up' bookmarklet.)
  217. },1000);
  218. },2500);
  219. */
  220. evt.preventDefault(); // Accept responsibility for the double-click.
  221. return false; // Prevent browser from doing anything else with it (e.g. selecting the word under the cursor).
  222. }
  223.  
  224. checkForActivity();
  225.  
  226. function checkForActivity() {
  227.  
  228. var countdownTimer = null;
  229.  
  230. // listen(document.body,'mousemove',startCountdown); // In Firefox this ignore mousemove on empty space (outside the document content), so trying window...
  231. listen(window,'mousemove',startCountdown); // Likewise for click below!
  232. // listen(document.body,'blur',clearCountdown); // Does not fire in Chrome?
  233. listen(window,'blur',clearCountdown); // For Chrome
  234. //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.
  235. // listen(document.body,'click',clearCountdown);
  236. listen(window,'click',clearCountdown);
  237. listen(window,'dblclick',restoreTab);
  238.  
  239. function startCountdown(e) {
  240. if (countdownTimer != null) {
  241. // There is already a countdown running - do not start.
  242. return;
  243. }
  244. var togo = restoreTime*1000;
  245. function countDown() {
  246. setHibernateStatus(mainReport +
  247. '\n' + "Tab will restore in "+(togo/1000).toFixed(1)+" seconds." +
  248. ' ' + "Click or defocus to pause." +
  249. ' ' + "Or double click to restore now!"
  250. );
  251. if (togo <= 0) {
  252. restoreTab();
  253. } else {
  254. togo -= 1000;
  255. if (countdownTimer)
  256. clearTimeout(countdownTimer);
  257. countdownTimer = setTimeout(countDown,1000);
  258. }
  259. }
  260. countDown();
  261. }
  262.  
  263. function clearCountdown(ev) {
  264. if (countdownTimer) {
  265. clearTimeout(countdownTimer);
  266. }
  267. countdownTimer = null;
  268. var evReport = "";
  269. if (ev) {
  270. evReport = " by "+ev.type+" on "+this;
  271. }
  272. var report = mainReport + '\n' + "Paused" + evReport + "";
  273. setHibernateStatus(report);
  274. }
  275.  
  276. }
  277.  
  278. }
  279.  
  280. if (onHoldingPage) {
  281. handleHoldingPage();
  282. } else {
  283. handleNormalPage();
  284. }
  285.  
  286.  
  287.  
  288. /* +++ Library Functions +++ */
  289.  
  290. function listen(target,eventType,handler,capture) {
  291. target.addEventListener(eventType,handler,capture);
  292. }
  293.  
  294. function ignore(target,eventType,handler,capture) {
  295. target.removeEventListener(eventType,handler,capture);
  296. }
  297.  
  298. // Given an object, encode its properties and values into a URI-ready CGI query string.
  299. function buildQueryParameterString(params) {
  300. return Object.keys(params).map( function(key) { return key+"="+encodeURIComponent(params[key]); } ).join("&");
  301. }
  302.  
  303. // Returns an object whose keys and values match those of the CGI query string of the current document.
  304. function getQueryParameters() {
  305. var queryString = document.location.search;
  306. var params = {};
  307. queryString.replace(/^\?/,'').split("&").map( function(s) {
  308. var part = s.split("=");
  309. var key = part[0];
  310. var value = decodeURIComponent(part[1]);
  311. params[key] = value;
  312. });
  313. return params;
  314. }
  315.  
  316. function whenIdleDo(idleTimeoutSecs,activateIdleEvent) {
  317.  
  318. var timer = null;
  319. var pageLastUsed = new Date().getTime();
  320.  
  321. function setNotIdle() {
  322. pageLastUsed = new Date().getTime();
  323. }
  324.  
  325. function checkForIdle() {
  326. var msSinceLastUsed = new Date().getTime() - pageLastUsed;
  327. if (msSinceLastUsed > idleTimeoutSecs * 1000) {
  328. activateIdleEvent();
  329. }
  330. setTimeout(checkForIdle,idleTimeoutSecs/5*1000);
  331. }
  332.  
  333. setTimeout(checkForIdle,idleTimeoutSecs*1000);
  334.  
  335. listen(document.body,'mousemove',setNotIdle);
  336. listen(document.body,'focus',setNotIdle);
  337. listen(document.body,'keydown',setNotIdle);
  338.  
  339. }
  340.  
  341.  
  342.  
  343. /* +++ Local Convenience Functions +++ */
  344.  
  345. var statusElement = null;
  346. function checkStatusElement() {
  347. if (!statusElement) {
  348. while (document.body.firstChild) {
  349. document.body.removeChild(document.body.firstChild);
  350. }
  351. statusElement = document.createElement("div");
  352. document.body.insertBefore(statusElement,document.body.firstChild);
  353. statusElement.style.textAlign = "center";
  354. }
  355. }
  356.  
  357. function setWindowTitle(msg) {
  358. msg = ""+msg;
  359. document.title = msg;
  360. }
  361.  
  362. function setHibernateStatus(msg) {
  363. msg = ""+msg;
  364. checkStatusElement();
  365. statusElement.textContent = msg;
  366. statusElement.innerText = msg; // IE
  367. // Currently '\n' works in Chrome, but not in Firefox.
  368. }
  369.  
  370.  
  371.  
  372.  
  373.  
  374.  
  375. /* +++ Favicon, Canvas and DataURL Magic +++ */
  376.  
  377. function loadFaviconForHost(targetHost,callback) {
  378.  
  379. // Try to load a favicon image for the given host, and pass it to callback.
  380. // Except: If there is a link with rel="icon" in the page, with host
  381. // matching the current page location, load that image file instead of
  382. // guessing the extension!
  383.  
  384. var favicon = document.createElement('img');
  385. favicon.addEventListener('load',function() {
  386. callback(favicon);
  387. });
  388.  
  389. var targetProtocol = document.location.protocol || "http:";
  390.  
  391. // If there is a <link rel="icon" ...> in the current page, then I think that overrides the site-global favicon.
  392. // NOTE: This is not appropriate if a third party targetHost was requested, only if they really wanted the favicon for the current page!
  393. var foundLink = null;
  394. var linkElems = document.getElementsByTagName("link");
  395. for (var i=0;i<linkElems.length;i++) {
  396. var link = linkElems[i];
  397. if (link.rel === "icon" || link.rel === "shortcut icon") {
  398. // Since we can't read the image data of images from third-party hosts, we skip them and keep searching.
  399. if (link.host == document.location.host) {
  400. foundLink = link;
  401. break;
  402. }
  403. }
  404. }
  405. if (foundLink) {
  406. favicon.addEventListener('error',function(){ callback(favicon); });
  407. favicon.src = foundLink.href; // Won't favicon.onload cause an additional callback to the one below?
  408. // 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).
  409. favicon = foundLink;
  410. callback(favicon);
  411. return;
  412. }
  413.  
  414. var extsToTry = ["jpg","gif","png","ico"]; // iterated in reverse order
  415. function tryNextExtension() {
  416. var ext = extsToTry.pop();
  417. if (ext == null) {
  418. console.log("Ran out of extensions to try for "+targetHost+"/favicon.???");
  419. // We run the callback anyway!
  420. callback(null);
  421. } else {
  422. favicon.src = targetProtocol+"//"+targetHost+"/favicon."+ext;
  423. }
  424. }
  425. favicon.addEventListener('error',tryNextExtension);
  426. tryNextExtension();
  427. // When the favicon is working we can remove the canvas, but until then we may as well keep it visible!
  428. }
  429.  
  430. function writeFaviconFromCanvas(canvas) {
  431. var faviconDataURL = canvas.toDataURL("image/png");
  432. // var faviconDataURL = canvas.toDataURL("image/x-icon;base64");
  433. // console.log("Got data URL: "+faviconDataURL.substring(0,10+"... (length "+faviconDataURL.length+")");
  434. writeFaviconFromDataString(faviconDataURL);
  435. }
  436.  
  437. function writeFaviconFromDataString(faviconDataURL) {
  438.  
  439. var d = document, h = document.getElementsByTagName('head')[0];
  440.  
  441. // Create this favicon
  442. var ss = d.createElement('link');
  443. ss.rel = 'shortcut icon';
  444. ss.type = 'image/x-icon';
  445. ss.href = faviconDataURL;
  446. /*
  447. // Remove any existing favicons
  448. var links = h.getElementsByTagName('link');
  449. for (var i=0; i<links.length; i++) {
  450. if (links[i].href == ss.href) return;
  451. if (links[i].rel == "shortcut icon" || links[i].rel=="icon")
  452. h.removeChild(links[i]);
  453. }
  454. */
  455. // Add this favicon to the head
  456. h.appendChild(ss);
  457.  
  458. // Force browser to acknowledge
  459. // I saw this trick somewhere. I don't know what browser requires it. But I just left it in!
  460. var shim = document.createElement('iframe');
  461. shim.width = shim.height = 0;
  462. document.body.appendChild(shim);
  463. shim.src = "icon";
  464. document.body.removeChild(shim);
  465.  
  466. }
  467.  
  468. function loadFaviconIntoCanvas(targetHost,callback) {
  469.  
  470. // console.log("Getting favicon for: "+targetHost);
  471.  
  472. var canvas = document.createElement('canvas');
  473. var ctx = canvas.getContext('2d');
  474.  
  475. loadFaviconForHost(targetHost,gotFavicon);
  476.  
  477. function gotFavicon(favicon) {
  478. if (favicon) {
  479. // console.log("Got favicon from: "+favicon.src);
  480. canvas.width = favicon.width;
  481. canvas.height = favicon.height;
  482. ctx.drawImage( favicon, 0, 0 );
  483. }
  484. callback(canvas);
  485. }
  486.  
  487. }
  488.  
  489. /* This throws a security error from canvas.toDataURL(), I think because we are
  490. trying to read something from a different domain than the script!
  491. In Chrome: "SECURITY_ERR: DOM Exception 18"
  492. */
  493. // loadFaviconIntoCanvas(document.location.host,writeFaviconFromCanvas);
  494.