├── package.json ├── responsively-lazy.js └── responsively-lazy.min.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "responsively-lazy", 3 | "title": "Responsively Lazy", 4 | "version": "3.2.1", 5 | "description": "Lazy load responsive images and HTML", 6 | "homepage": "https://ivopetkov.com/responsively-lazy/", 7 | "author": { 8 | "name": "Ivo Petkov", 9 | "email": "ivo@ivopetkov.com", 10 | "url": "https://ivopetkov.com/" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/ivopetkov/responsively-lazy.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/ivopetkov/responsively-lazy/issues" 18 | }, 19 | "keywords": [ 20 | "images", 21 | "lazyload", 22 | "responsive", 23 | "seo" 24 | ], 25 | "licenses": [ 26 | { 27 | "type": "MIT", 28 | "url": "https://opensource.org/licenses/MIT" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /responsively-lazy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Responsively Lazy 3 | * https://ivopetkov.com/responsively-lazy/ 4 | * Copyright (c) Ivo Petkov 5 | * Free to use under the MIT license. 6 | * 7 | * Debug: 8 | * document.cookie = "ivopetkov-responsively-lazy=debug"; 9 | */ 10 | 11 | var responsivelyLazy = typeof responsivelyLazy !== 'undefined' ? responsivelyLazy : (function () { 12 | 13 | if (typeof window.addEventListener !== 'undefined' && typeof document.querySelectorAll !== 'undefined') { // Check for old browsers 14 | 15 | var debug = document.cookie.indexOf('ivopetkov-responsively-lazy=debug') !== -1; 16 | 17 | var hasWebPSupport = null; 18 | var hasAVIFSupport = null; 19 | var hasSrcSetSupport = 'srcset' in document.createElement('img'); 20 | var windowWidth = null; 21 | var windowHeight = null; 22 | var hasIntersectionObserverSupport = typeof IntersectionObserver !== 'undefined'; 23 | var mutationObserverIsDisabled = false; 24 | 25 | var getVisibilityPriority = function (element) { 26 | var thresholdHorizontal = 0; 27 | var thresholdVertical = 0; 28 | var thresholdValue = element.getAttribute('data-responsively-lazy-threshold'); 29 | if (thresholdValue !== null) { 30 | if (thresholdValue.substr(-2) === 'px') { 31 | thresholdHorizontal = thresholdVertical = parseInt(thresholdValue.substr(0, thresholdValue.length - 2), 10); 32 | } else if (thresholdValue.substr(-1) === '%') { 33 | var percent = parseInt(thresholdValue.substr(0, thresholdValue.length - 1), 10) / 100; 34 | thresholdHorizontal = Math.floor(windowWidth * percent); 35 | thresholdVertical = Math.floor(windowHeight * percent); 36 | } 37 | } 38 | var rect = element.getBoundingClientRect(); 39 | var elementTop = rect.top; 40 | var elementLeft = rect.left; 41 | var elementWidth = rect.width; 42 | var elementHeight = rect.height; 43 | if (elementTop === 0 && elementLeft === 0 && elementWidth === 0 && elementHeight === 0) { 44 | return 0; 45 | } 46 | if (elementWidth === 0) { 47 | elementWidth = 1; 48 | } 49 | if (elementHeight === 0) { 50 | elementHeight = 1; 51 | } 52 | 53 | var getVisibleAreaSize = function (elementPoint, elementSize, windowSize) { 54 | return elementPoint < windowSize && elementPoint + elementSize > 0 ? Math.min(windowSize, elementPoint + elementSize) - Math.max(0, elementPoint) : 0; 55 | }; 56 | 57 | return (getVisibleAreaSize(elementLeft - thresholdHorizontal, elementWidth + 2 * thresholdHorizontal, windowWidth) * getVisibleAreaSize(elementTop - thresholdVertical, elementHeight + 2 * thresholdVertical, windowHeight)) / ((elementWidth + 2 * thresholdHorizontal) * (elementHeight + 2 * thresholdVertical)) * 100; 58 | } 59 | 60 | var evalScripts = function (scripts, startIndex) { 61 | var scriptsCount = scripts.length; 62 | for (var i = startIndex; i < scriptsCount; i++) { 63 | var breakAfterThisScript = false; 64 | var script = scripts[i]; 65 | var newScript = document.createElement('script'); 66 | var type = script.getAttribute('type'); 67 | if (type !== null) { 68 | newScript.setAttribute("type", type); 69 | } 70 | var src = script.getAttribute('src'); 71 | if (src !== null) { 72 | newScript.setAttribute("src", src); 73 | if ((typeof script.async === 'undefined' || script.async === false) && i + 1 < scriptsCount) { 74 | breakAfterThisScript = true; 75 | newScript.addEventListener('load', function () { 76 | evalScripts(scripts, i + 1); 77 | }); 78 | } 79 | } 80 | newScript.innerHTML = script.innerHTML; 81 | script.parentNode.insertBefore(newScript, script); 82 | script.parentNode.removeChild(script); 83 | if (breakAfterThisScript) { 84 | break; 85 | } 86 | } 87 | }; 88 | 89 | var loadImageQueue = []; 90 | 91 | var processLoadImageQueueLock = false; 92 | var processLoadImageQueue = function () { 93 | if (processLoadImageQueueLock) { 94 | return; 95 | } 96 | processLoadImageQueueLock = true; 97 | var maxConcurrentImages = 3; 98 | loadImageQueue = loadImageQueue.filter(function (item) { return item[2] !== 2 }); // Remove completed 99 | for (var i = 0; i < loadImageQueue.length; i++) { // Update visibility priority 100 | loadImageQueue[i][4] = getVisibilityPriority(loadImageQueue[i][1]); 101 | } 102 | loadImageQueue.sort(function (item1, item2) { // Sort by visibility priority 103 | return item2[4] - item1[4]; 104 | }); 105 | var currentlyLoadingCount = loadImageQueue.filter(function (item) { return item[3] === 1 }).length; 106 | for (var i = 0; i < loadImageQueue.length; i++) { 107 | if (currentlyLoadingCount >= maxConcurrentImages) { 108 | break; 109 | } 110 | var item = loadImageQueue[i]; 111 | if (item[3] === 0) { 112 | item[3] = 1; // Status: loading 113 | item[2](); // Call load() 114 | currentlyLoadingCount++; 115 | } 116 | } 117 | processLoadImageQueueLock = false; 118 | }; 119 | 120 | var loadImageCounter = 0; 121 | 122 | var loadImage = function (contextElement, url, callback) { 123 | loadImageCounter++; 124 | var key = 'i' + loadImageCounter; 125 | var timeout = null; 126 | var onDone = function () { 127 | clearTimeout(timeout); 128 | for (var i = 0; i < loadImageQueue.length; i++) { 129 | var item = loadImageQueue[i]; 130 | if (item[0] === key) { 131 | item[3] = 2; // Status: completed 132 | break; 133 | } 134 | } 135 | processLoadImageQueue(); 136 | }; 137 | var image = new Image(); 138 | image.onload = function () { 139 | onDone(); 140 | callback(true); 141 | }; 142 | image.onerror = function () { 143 | onDone(); 144 | callback(false); 145 | }; 146 | var load = function () { 147 | image.src = url; 148 | timeout = setTimeout(onDone, 60000); 149 | }; 150 | loadImageQueue.push([key, contextElement, load, 0, 0]); // key, element, on load function, status, visiblity priority 151 | processLoadImageQueue(); 152 | }; 153 | 154 | var updateImage = function (type, element) { 155 | var options = []; 156 | var value = element.getAttribute('data-responsively-lazy'); 157 | var maxOptionWidth = null; 158 | if (value !== null) { 159 | value = value.trim(); 160 | if (value.length > 0) { 161 | value = value.split(','); 162 | for (var j = 0; j < value.length; j++) { 163 | var optionImage = null; 164 | var optionWidth = 999998; 165 | var skipOption = false; 166 | var optionParts = value[j].trim().split(' '); 167 | for (var k = 0; k < optionParts.length; k++) { 168 | var optionPart = optionParts[k]; 169 | var optionPartLength = optionPart.length; 170 | if (optionPartLength === 0) { 171 | continue; 172 | } 173 | if (optionImage === null) { 174 | optionImage = optionPart; 175 | } else { 176 | if (optionPart[optionPartLength - 1] === 'w') { 177 | optionWidth = parseInt(optionPart.substr(0, optionPartLength - 1), 10); 178 | } else if (optionPart === 'webp' && !hasWebPSupport) { 179 | skipOption = true; 180 | } else if (optionPart === 'avif' && !hasAVIFSupport) { 181 | skipOption = true; 182 | } 183 | } 184 | } 185 | if (skipOption) { 186 | continue; 187 | } 188 | if ((optionImage.indexOf('%2F') !== -1 || optionImage.indexOf('%3F') !== -1) && optionImage.indexOf('/') === -1 && optionImage.indexOf('?') === -1) {// path is encoded 189 | optionImage = decodeURIComponent(optionImage); 190 | } 191 | options.push([optionImage, optionWidth]); 192 | if (maxOptionWidth < optionWidth) { 193 | maxOptionWidth = optionWidth; 194 | } 195 | } 196 | options.sort(function (a, b) { 197 | return a[1] - b[1]; 198 | }); 199 | var temp = []; 200 | for (var j = 0; j < options.length; j++) { 201 | var option = options[j]; 202 | if (j > 0) { 203 | if (option[1] === temp[temp.length - 1][1]) { 204 | continue; 205 | } 206 | } 207 | temp.push([option[0], option[1]]); 208 | } 209 | options = temp; 210 | } 211 | } 212 | var elementWidth = element.getBoundingClientRect().width * (typeof window.devicePixelRatio !== 'undefined' ? window.devicePixelRatio : 1); 213 | 214 | var bestSelectedOption = null; 215 | for (var j = 0; j < options.length; j++) { 216 | var option = options[j]; 217 | if (option[1] >= elementWidth || option[1] === maxOptionWidth) { // Show the largest available option even if the element width is larger (can be webp or avif) 218 | bestSelectedOption = option; 219 | break; 220 | } 221 | } 222 | 223 | if (bestSelectedOption === null) { 224 | if (type === 'img') { 225 | bestSelectedOption = [element.getAttribute('src'), 999999]; // No options found 226 | } else { // background 227 | bestSelectedOption = [null, 999999]; 228 | } 229 | } 230 | 231 | if (typeof element.responsivelyLazyOption === 'undefined') { 232 | element.responsivelyLazyOption = ['', 0]; 233 | } 234 | 235 | if (element.responsivelyLazyOption[1] < bestSelectedOption[1]) { 236 | element.responsivelyLazyOption = bestSelectedOption; 237 | var url = bestSelectedOption[0]; 238 | if (url === null) { 239 | return; 240 | } 241 | 242 | loadImage(element, url, function (result) { 243 | if (result && element.responsivelyLazyOption[0] === url) { 244 | if (type === 'img') { 245 | if (url === element.getAttribute('src')) { 246 | element.removeAttribute('srcset'); 247 | } else { 248 | element.setAttribute('srcset', url); 249 | } 250 | } else { // background 251 | element.style.backgroundImage = 'url(' + url + ')'; 252 | } 253 | if (typeof element.responsivelyLazyLoadDispached === 'undefined') { 254 | element.responsivelyLazyLoadDispached = true; 255 | var handler = element.getAttribute('data-on-responsively-lazy-load'); 256 | if (handler !== null) { 257 | (new Function(handler).bind(element))(); 258 | } 259 | if (typeof Event !== 'undefined') { 260 | var event = new Event('responsively-lazy-load'); 261 | element.dispatchEvent(event); 262 | } 263 | } 264 | } else { 265 | element.responsivelyLazyOption = ['', 0]; 266 | } 267 | }); 268 | } 269 | }; 270 | 271 | var updateWindowSize = function () { 272 | windowWidth = window.innerWidth; 273 | windowHeight = window.innerHeight; 274 | }; 275 | 276 | var updateElement = function (element, options) { 277 | 278 | if (typeof element.responsivelyLazyDone !== 'undefined') { 279 | return; 280 | } 281 | 282 | var ignoreThreshold = typeof options.ignoreThreshold !== 'undefined' ? options.ignoreThreshold : false; 283 | 284 | if (!ignoreThreshold && getVisibilityPriority(element) === 0) { 285 | return; 286 | } 287 | 288 | var type = element.getAttribute('data-responsively-lazy-type'); 289 | if (type !== 'background' && type !== 'html') { 290 | type = 'img'; 291 | } 292 | 293 | if (type === 'html') { 294 | element.responsivelyLazyDone = true; 295 | mutationObserverIsDisabled = true; 296 | element.innerHTML = element.getAttribute('data-responsively-lazy'); 297 | var scripts = element.querySelectorAll('script'); 298 | if (scripts.length > 0) { 299 | evalScripts(scripts, 0); 300 | } 301 | mutationObserverIsDisabled = false; 302 | } else if (type === 'img') { 303 | if (hasSrcSetSupport) { 304 | updateImage(type, element); 305 | } 306 | } else { // background 307 | updateImage(type, element); 308 | } 309 | 310 | }; 311 | 312 | var run = function (element, options) { 313 | if (hasWebPSupport === null) { 314 | return; 315 | } 316 | if (hasAVIFSupport === null) { 317 | return; 318 | } 319 | if (typeof options === 'undefined') { 320 | options = {}; 321 | } 322 | if (debug) { 323 | var timerLabel = 'responsivelyLazy::run'; 324 | console.time(timerLabel); 325 | } 326 | if (typeof element !== 'undefined' && element !== null) { 327 | if (element.getAttribute('data-responsively-lazy') !== null) { 328 | updateElement(element, options); 329 | } 330 | } else { 331 | var elements = document.querySelectorAll('[data-responsively-lazy]'); 332 | for (var i = 0; i < elements.length; i++) { 333 | var element = elements[i]; 334 | updateElement(elements[i], options); 335 | } 336 | } 337 | if (debug) { 338 | console.timeEnd(timerLabel); 339 | } 340 | }; 341 | 342 | var testImageSupport = function (base64, callback) { 343 | var image = new Image(); 344 | image.onload = image.onerror = function () { 345 | callback(image); 346 | }; 347 | image.src = 'data:image/webp;base64,' + base64; 348 | }; 349 | 350 | var runIfAllTestsDone = function () { 351 | if (hasWebPSupport !== null && hasAVIFSupport !== null) { 352 | run(); 353 | } 354 | }; 355 | 356 | testImageSupport('UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAD8D+JaQAA3AA/ua1AAA=', function (image) { 357 | hasWebPSupport = image.width === 1; 358 | runIfAllTestsDone(); 359 | }); 360 | 361 | testImageSupport('AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUEAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAACAAAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAEAAAABAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgSAAAAAAABNjb2xybmNseAABAA0AAIAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAAChtZGF0EgAKBzgABpAQ0AIyExAAAAAP+j9adAx6kYPdyoRe9BA=', function (image) { 362 | hasAVIFSupport = image.width === 1; 363 | runIfAllTestsDone(); 364 | }); 365 | 366 | updateWindowSize(); 367 | 368 | var requestAnimationFrameFunction = window.requestAnimationFrame || function (callback) { 369 | window.setTimeout(callback, 20); 370 | }; 371 | 372 | var hasChange = false; 373 | var process = function () { 374 | if (hasChange) { 375 | hasChange = false; 376 | run(); 377 | } 378 | requestAnimationFrameFunction.call(null, process); 379 | }; 380 | process(); 381 | 382 | if (hasIntersectionObserverSupport) { 383 | var intersectionObserver = new IntersectionObserver(function (entries) { 384 | for (var i in entries) { 385 | var entry = entries[i]; 386 | if (entry.intersectionRatio > 0) { 387 | updateElement(entry.target, {}); 388 | } 389 | } 390 | }); 391 | var updateIntersectionObservers = function () { 392 | var elements = document.querySelectorAll('[data-responsively-lazy]'); 393 | for (var i = 0; i < elements.length; i++) { 394 | var element = elements[i]; 395 | if (typeof element.responsivelyLazyObserver === 'undefined') { 396 | element.responsivelyLazyObserver = true; 397 | intersectionObserver.observe(element); 398 | } 399 | } 400 | }; 401 | var changeTimeout = null; 402 | } 403 | 404 | var setChanged = function () { 405 | if (hasIntersectionObserverSupport) { 406 | window.clearTimeout(changeTimeout); 407 | changeTimeout = window.setTimeout(function () { 408 | hasChange = true; 409 | }, 50); 410 | } else { 411 | hasChange = true; 412 | } 413 | }; 414 | 415 | var updateParentNodesScrollListeners = function () { 416 | var elements = document.querySelectorAll('[data-responsively-lazy]'); 417 | for (var i = 0; i < elements.length; i++) { 418 | var parentNode = elements[i].parentNode; 419 | while (parentNode && parentNode.tagName.toLowerCase() !== 'html') { 420 | if (typeof parentNode.responsivelyLazyScroll === 'undefined') { 421 | parentNode.responsivelyLazyScroll = true; 422 | parentNode.addEventListener('scroll', setChanged); 423 | } 424 | parentNode = parentNode.parentNode; 425 | } 426 | } 427 | }; 428 | 429 | var initialized = false; 430 | var initialize = function () { 431 | if (initialized) { 432 | return; 433 | } 434 | initialized = true; 435 | window.addEventListener('resize', function () { 436 | updateWindowSize(); 437 | setChanged(); 438 | }); 439 | window.addEventListener('scroll', setChanged); 440 | window.addEventListener('load', setChanged); 441 | window.addEventListener('orientationchange', function () { 442 | updateWindowSize(); 443 | setChanged(); 444 | }); 445 | if (hasIntersectionObserverSupport) { 446 | updateIntersectionObservers(); 447 | } 448 | updateParentNodesScrollListeners(); 449 | if (typeof MutationObserver !== 'undefined') { 450 | var observer = new MutationObserver(function () { 451 | if (!mutationObserverIsDisabled) { 452 | if (hasIntersectionObserverSupport) { 453 | updateIntersectionObservers(); 454 | } 455 | updateParentNodesScrollListeners(); 456 | setChanged(); 457 | } 458 | }); 459 | observer.observe(document.querySelector('body'), { childList: true, subtree: true }); 460 | } 461 | }; 462 | document.addEventListener('readystatechange', () => { // interactive or complete 463 | initialize(); 464 | run(); 465 | }); 466 | if (document.readyState === 'complete') { 467 | initialize(); 468 | run(); 469 | } 470 | } else { 471 | var run = function () { }; 472 | } 473 | 474 | return { 475 | 'run': run 476 | }; 477 | 478 | }()); 479 | -------------------------------------------------------------------------------- /responsively-lazy.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Responsively Lazy 3 | * https://ivopetkov.com/responsively-lazy/ 4 | * Copyright (c) Ivo Petkov 5 | * Free to use under the MIT license. 6 | */ 7 | var responsivelyLazy=void 0!==responsivelyLazy?responsivelyLazy:function(){if(void 0!==window.addEventListener&&void 0!==document.querySelectorAll){var e=-1!==document.cookie.indexOf("ivopetkov-responsively-lazy=debug"),n=null,t=null,A="srcset"in document.createElement("img"),r=null,i=null,o="undefined"!=typeof IntersectionObserver,l=!1,a=function(e){var n=0,t=0,A=e.getAttribute("data-responsively-lazy-threshold");if(null!==A)if("px"===A.substr(-2))n=t=parseInt(A.substr(0,A.length-2),10);else if("%"===A.substr(-1)){var o=parseInt(A.substr(0,A.length-1),10)/100;n=Math.floor(r*o),t=Math.floor(i*o)}var l=e.getBoundingClientRect(),a=l.top,s=l.left,u=l.width,d=l.height;if(0===a&&0===s&&0===u&&0===d)return 0;0===u&&(u=1),0===d&&(d=1);var v=function(e,n,t){return e0?Math.min(t,e+n)-Math.max(0,e):0};return v(s-n,u+2*n,r)*v(a-t,d+2*t,i)/((u+2*n)*(d+2*t))*100},s=function(e,n){for(var t=e.length,A=n;A=3);e++){var t=u[e];0===t[3]&&(t[3]=1,t[2](),n++)}d=!1}},c=0,f=function(e,A){var r=[],i=A.getAttribute("data-responsively-lazy"),o=null;if(null!==i&&(i=i.trim()).length>0){i=i.split(",");for(var l=0;l0&&m[1]===h[h.length-1][1]||h.push([m[0],m[1]])}r=h}var b=A.getBoundingClientRect().width*(void 0!==window.devicePixelRatio?window.devicePixelRatio:1),w=null;for(l=0;l=b||m[1]===o){w=m;break}}if(null===w&&(w="img"===e?[A.getAttribute("src"),999999]:[null,999999]),void 0===A.responsivelyLazyOption&&(A.responsivelyLazyOption=["",0]),A.responsivelyLazyOption[1]0&&s(r,0),l=!1}else"img"===t?A&&f(t,e):f(t,e)}},g=function(A,r){if(null!==n&&null!==t){if(void 0===r&&(r={}),e){var i="responsivelyLazy::run";console.time(i)}if(null!=A)null!==A.getAttribute("data-responsively-lazy")&&p(A,r);else for(var o=document.querySelectorAll("[data-responsively-lazy]"),l=0;l0&&p(t.target,{})}})),B=function(){for(var e=document.querySelectorAll("[data-responsively-lazy]"),n=0;n{k(),g()}),"complete"===document.readyState&&(k(),g())}else g=function(){};return{run:g}}(); --------------------------------------------------------------------------------