Lazyloadfier Userscript

9/8/2023, 1:42:06 AM

目前为 2023-11-13 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Lazyloadfier Userscript
  3. // @namespace Lazyloadfier Userscript - Violentmonkey Scripts
  4. // @match *://*/*
  5. // @grant none
  6. // @version 1.0
  7. // @run-at document-start
  8. // @author -
  9. // @description 9/8/2023, 1:42:06 AM
  10. // ==/UserScript==
  11.  
  12. /* global document, addEventListener, MutationObserver, IntersectionObserver, innerHeight, innerWidth, setTimeout, clearTimeout */
  13.  
  14. const SRC_ATTRIBUTE_NAME = "src";
  15. const SRCSET_ATTRIBUTE_NAME = "srcset";
  16. const POSTER_ATTRIBUTE_NAME = "poster";
  17. const LOADING_ATTRIBUTE_NAME = "loading";
  18. const LAZY_LOADING_ATTRIBUTE_VALUE = "lazy";
  19. const IMG_TAG_NAME = "IMG";
  20. const VIDEO_TAG_NAME = "VIDEO";
  21. const AUDIO_TAG_NAME = "AUDIO";
  22. const SOURCE_TAG_NAME = "SOURCE";
  23. const IFRAME_TAG_NAME = "IFRAME";
  24. const FRAME_TAG_NAME = "FRAME";
  25. const EMBED_TAG_NAME = "EMBED";
  26. const TAG_NAMES_WITH_SRC_ATTRIBUTE = new Set([IMG_TAG_NAME, VIDEO_TAG_NAME, AUDIO_TAG_NAME, SOURCE_TAG_NAME, IFRAME_TAG_NAME, FRAME_TAG_NAME, EMBED_TAG_NAME]);
  27. const TAG_NAMES_WITH_SRCSET_ATTRIBUTE = new Set([IMG_TAG_NAME, SOURCE_TAG_NAME]);
  28. const TAG_NAMES_WITH_POSTER_ATTRIBUTE = new Set([VIDEO_TAG_NAME]);
  29. const OBSERVED_TAGS_SELECTOR = Array.from(TAG_NAMES_WITH_SRC_ATTRIBUTE).join(",");
  30. const UNSENT_READY_STATE = 0;
  31. const DOM_CONTENT_LOADED_EVENT = "DOMContentLoaded";
  32. const EMPTY_DEFAULT_DATA_URI = "data:,";
  33. const EMPTY_IMAGE_DATA_URI = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
  34. const EMPTY_TEXT_DATA_URI = "data:text/plain,";
  35. const EMPTY_DATA_URI = new Map([
  36. [VIDEO_TAG_NAME, EMPTY_DEFAULT_DATA_URI],
  37. [AUDIO_TAG_NAME, EMPTY_DEFAULT_DATA_URI],
  38. [SOURCE_TAG_NAME, EMPTY_IMAGE_DATA_URI],
  39. [IMG_TAG_NAME, EMPTY_IMAGE_DATA_URI],
  40. [IFRAME_TAG_NAME, EMPTY_TEXT_DATA_URI],
  41. [FRAME_TAG_NAME, EMPTY_TEXT_DATA_URI],
  42. [EMBED_TAG_NAME, EMPTY_TEXT_DATA_URI]
  43. ]);
  44. const MUTATION_OBSERVER_OPTIONS = { childList: true, subtree: true };
  45. const MINIMUM_INTERSECTION_RATIO = 0;
  46. const MUTATION_OBSERVER_TIMEOUT = 2500;
  47.  
  48. observeDocumentMutations();
  49.  
  50. function observeDocumentMutations() {
  51. const disconnectObserverTimeout = {};
  52. const mutationObserver = new MutationObserver(mutationsList => mutationObserverCallback(mutationsList, callDeferDisconnectObserver));
  53. mutationObserver.observe(document, MUTATION_OBSERVER_OPTIONS);
  54. addEventListener(DOM_CONTENT_LOADED_EVENT, callDeferDisconnectObserver);
  55.  
  56. function callDeferDisconnectObserver() {
  57. deferDisconnectObserver(mutationObserver, disconnectObserverTimeout);
  58. }
  59. }
  60.  
  61. function deferDisconnectObserver(mutationObserver, disconnectObserverTimeout) {
  62. if (disconnectObserverTimeout.id) {
  63. clearTimeout(disconnectObserverTimeout.id);
  64. }
  65. disconnectObserverTimeout.id = setTimeout(() => mutationObserver.disconnect(), MUTATION_OBSERVER_TIMEOUT);
  66. }
  67.  
  68. function mutationObserverCallback(mutationsList, onProgressCallback) {
  69. const observedNodes = getObservedNodes(mutationsList);
  70. if (observedNodes.length) {
  71. observedNodes.forEach(observeNodeIntersection);
  72. onProgressCallback(observedNodes);
  73. }
  74. }
  75.  
  76. function getObservedNodes(mutationsList) {
  77. const observedNodes = [];
  78. mutationsList.forEach(mutationRecord => {
  79. const newNodes = new Set(mutationRecord.addedNodes);
  80. newNodes.forEach(node => {
  81. if (node.querySelectorAll) {
  82. node.querySelectorAll(OBSERVED_TAGS_SELECTOR).forEach(node => newNodes.add(node));
  83. }
  84. });
  85. observedNodes.splice(0, 0, ...Array.from(newNodes).filter(matchObservedNode));
  86. });
  87. return observedNodes;
  88. }
  89.  
  90. function matchObservedNode(node) {
  91. return TAG_NAMES_WITH_SRC_ATTRIBUTE.has(node.tagName) &&
  92. nodeIsHidden(node) &&
  93. node[LOADING_ATTRIBUTE_NAME] != LAZY_LOADING_ATTRIBUTE_VALUE;
  94. }
  95.  
  96. function nodeIsHidden(node) {
  97. const boundingClientRect = node.getBoundingClientRect();
  98. return boundingClientRect.bottom < 0 ||
  99. boundingClientRect.top > innerHeight ||
  100. boundingClientRect.left < 0 ||
  101. boundingClientRect.right > innerWidth;
  102. }
  103.  
  104. function observeNodeIntersection(node) {
  105. const src = resetSource(node, SRC_ATTRIBUTE_NAME);
  106. const srcset = resetSource(node, SRCSET_ATTRIBUTE_NAME);
  107. const poster = resetSource(node, POSTER_ATTRIBUTE_NAME);
  108. const intersectionObserver = new IntersectionObserver((entries, observer) => intersectionObserverCallback(entries, node, observer, { src, srcset, poster }));
  109. intersectionObserver.observe(node.tagName == SOURCE_TAG_NAME ? node.parentElement : node);
  110. }
  111.  
  112. function intersectionObserverCallback(entries, node, observer, values) {
  113. const entry = entries[0];
  114. if (entry) {
  115. if (entry.intersectionRatio > MINIMUM_INTERSECTION_RATIO) {
  116. replaceSource(node, values);
  117. observer.disconnect();
  118. }
  119. }
  120. }
  121.  
  122. function replaceSource(node, values) {
  123. setSource(node, SRC_ATTRIBUTE_NAME, values.src);
  124. if (TAG_NAMES_WITH_SRCSET_ATTRIBUTE.has(node.tagName)) {
  125. setSource(node, SRCSET_ATTRIBUTE_NAME, values.srcset);
  126. }
  127. if (TAG_NAMES_WITH_POSTER_ATTRIBUTE.has(node.tagName)) {
  128. setSource(node, POSTER_ATTRIBUTE_NAME, values.poster);
  129. }
  130. }
  131.  
  132. function resetSource(node, attributeName) {
  133. const originalValue = node[attributeName];
  134. if (originalValue) {
  135. node[attributeName] = EMPTY_DATA_URI.get(node.tagName);
  136. return originalValue;
  137. }
  138. }
  139.  
  140. function setSource(node, attributeName, value) {
  141. if (node[attributeName] == EMPTY_DATA_URI.get(node.tagName)) {
  142. if (value) {
  143. node[attributeName] = value;
  144. } else {
  145. node.removeAttribute(attributeName);
  146. if (node.readyState === UNSENT_READY_STATE) {
  147. node.load();
  148. }
  149. }
  150. }
  151. }