Last active
February 17, 2018 14:54
-
-
Save aFarkas/f3ae9f7f9c40169851ebb0307d9c3e67 to your computer and use it in GitHub Desktop.
lazysizes requestIdleCallback
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Title</title> | |
<script> | |
window.lazySizesConfig = window.lazySizesConfig || {}; | |
window.lazySizesConfig.ricTimeout = 0; // test: 0, 50, 100, 300 | |
window.lazySizesConfig.throttleDelay = 40; //test: 0, 40, 100 | |
</script> | |
<script src="lazysizes-ric-throttle-delay.js"></script> | |
</head> | |
<body> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(window, factory) { | |
var lazySizes = factory(window, window.document); | |
window.lazySizes = lazySizes; | |
if(typeof module == 'object' && module.exports){ | |
module.exports = lazySizes; | |
} | |
}(window, function l(window, document) { | |
'use strict'; | |
/*jshint eqnull:true */ | |
if(!document.getElementsByClassName){return;} | |
var lazysizes, lazySizesConfig; | |
var docElem = document.documentElement; | |
var Date = window.Date; | |
var supportPicture = window.HTMLPictureElement; | |
var _addEventListener = 'addEventListener'; | |
var _getAttribute = 'getAttribute'; | |
var addEventListener = window[_addEventListener]; | |
var setTimeout = window.setTimeout; | |
var requestAnimationFrame = window.requestAnimationFrame || setTimeout; | |
var requestIdleCallback = window.requestIdleCallback; | |
var regPicture = /^picture$/i; | |
var loadEvents = ['load', 'error', 'lazyincluded', '_lazyloaded']; | |
var regClassCache = {}; | |
var forEach = Array.prototype.forEach; | |
var hasClass = function(ele, cls) { | |
if(!regClassCache[cls]){ | |
regClassCache[cls] = new RegExp('(\\s|^)'+cls+'(\\s|$)'); | |
} | |
return regClassCache[cls].test(ele[_getAttribute]('class') || '') && regClassCache[cls]; | |
}; | |
var addClass = function(ele, cls) { | |
if (!hasClass(ele, cls)){ | |
ele.setAttribute('class', (ele[_getAttribute]('class') || '').trim() + ' ' + cls); | |
} | |
}; | |
var removeClass = function(ele, cls) { | |
var reg; | |
if ((reg = hasClass(ele,cls))) { | |
ele.setAttribute('class', (ele[_getAttribute]('class') || '').replace(reg, ' ')); | |
} | |
}; | |
var addRemoveLoadEvents = function(dom, fn, add){ | |
var action = add ? _addEventListener : 'removeEventListener'; | |
if(add){ | |
addRemoveLoadEvents(dom, fn); | |
} | |
loadEvents.forEach(function(evt){ | |
dom[action](evt, fn); | |
}); | |
}; | |
var triggerEvent = function(elem, name, detail, noBubbles, noCancelable){ | |
var event = document.createEvent('CustomEvent'); | |
if(!detail){ | |
detail = {}; | |
} | |
detail.instance = lazysizes; | |
event.initCustomEvent(name, !noBubbles, !noCancelable, detail); | |
elem.dispatchEvent(event); | |
return event; | |
}; | |
var updatePolyfill = function (el, full){ | |
var polyfill; | |
if( !supportPicture && ( polyfill = (window.picturefill || lazySizesConfig.pf) ) ){ | |
polyfill({reevaluate: true, elements: [el]}); | |
} else if(full && full.src){ | |
el.src = full.src; | |
} | |
}; | |
var getCSS = function (elem, style){ | |
return (getComputedStyle(elem, null) || {})[style]; | |
}; | |
var getWidth = function(elem, parent, width){ | |
width = width || elem.offsetWidth; | |
while(width < lazySizesConfig.minSize && parent && !elem._lazysizesWidth){ | |
width = parent.offsetWidth; | |
parent = parent.parentNode; | |
} | |
return width; | |
}; | |
var rAF = (function(){ | |
var running, waiting; | |
var firstFns = []; | |
var secondFns = []; | |
var fns = firstFns; | |
var run = function(){ | |
var runFns = fns; | |
fns = firstFns.length ? secondFns : firstFns; | |
running = true; | |
waiting = false; | |
while(runFns.length){ | |
runFns.shift()(); | |
} | |
running = false; | |
}; | |
var rafBatch = function(fn, queue){ | |
if(running && !queue){ | |
fn.apply(this, arguments); | |
} else { | |
fns.push(fn); | |
if(!waiting){ | |
waiting = true; | |
(document.hidden ? setTimeout : requestAnimationFrame)(run); | |
} | |
} | |
}; | |
rafBatch._lsFlush = run; | |
return rafBatch; | |
})(); | |
var rAFIt = function(fn, simple){ | |
return simple ? | |
function() { | |
rAF(fn); | |
} : | |
function(){ | |
var that = this; | |
var args = arguments; | |
rAF(function(){ | |
fn.apply(that, args); | |
}); | |
} | |
; | |
}; | |
var throttle = function(fn){ | |
var timeStart; | |
var running; | |
var lastTime = 0; | |
var gDelay = lazySizesConfig.throttleDelay; | |
var rICTimeout = lazySizesConfig.ricTimeout; | |
var run = function(){ | |
running = false; | |
lastTime = Date.now(); | |
console.log('throtteled', lastTime - timeStart); | |
fn(); | |
}; | |
var idleCallback = requestIdleCallback && lazySizesConfig.ricTimeout > 49 ? | |
function(){ | |
requestIdleCallback(run, {timeout: rICTimeout}); | |
if(rICTimeout !== lazySizesConfig.ricTimeout){ | |
rICTimeout = lazySizesConfig.ricTimeout; | |
} | |
} : | |
rAFIt(function(){ | |
setTimeout(run); | |
}, true) | |
; | |
console.log('config: throttleDelay: ' + gDelay +', ricTimeout: ' + rICTimeout); | |
return function(isPriority){ | |
var delay; | |
if((isPriority = isPriority === true)){ | |
rICTimeout = 33; | |
} | |
if(running){ | |
return; | |
} | |
running = true; | |
delay = gDelay - (Date.now() - lastTime); | |
if(delay < 0){ | |
delay = 0; | |
} | |
if(isPriority || delay < 9){ | |
console.log('fast call'); | |
timeStart = Date.now(); | |
idleCallback(); | |
} else { | |
console.log('slow call'); | |
timeStart = Date.now(); | |
setTimeout(idleCallback, delay); | |
} | |
}; | |
}; | |
//based on http://modernjavascript.blogspot.de/2013/08/building-better-debounce.html | |
var debounce = function(func) { | |
var timeout, timestamp; | |
var wait = 99; | |
var run = function(){ | |
timeout = null; | |
func(); | |
}; | |
var later = function() { | |
var last = Date.now() - timestamp; | |
if (last < wait) { | |
setTimeout(later, wait - last); | |
} else { | |
(requestIdleCallback || run)(run); | |
} | |
}; | |
return function() { | |
timestamp = Date.now(); | |
if (!timeout) { | |
timeout = setTimeout(later, wait); | |
} | |
}; | |
}; | |
(function(){ | |
var prop; | |
var lazySizesDefaults = { | |
lazyClass: 'lazyload', | |
loadedClass: 'lazyloaded', | |
loadingClass: 'lazyloading', | |
preloadClass: 'lazypreload', | |
errorClass: 'lazyerror', | |
//strictClass: 'lazystrict', | |
autosizesClass: 'lazyautosizes', | |
srcAttr: 'data-src', | |
srcsetAttr: 'data-srcset', | |
sizesAttr: 'data-sizes', | |
//preloadAfterLoad: false, | |
minSize: 40, | |
customMedia: {}, | |
init: true, | |
expFactor: 1.5, | |
hFac: 0.8, | |
loadMode: 2, | |
loadHidden: true, | |
ricTimeout: 0, | |
throttleDelay: 50, | |
}; | |
lazySizesConfig = window.lazySizesConfig || window.lazysizesConfig || {}; | |
for(prop in lazySizesDefaults){ | |
if(!(prop in lazySizesConfig)){ | |
lazySizesConfig[prop] = lazySizesDefaults[prop]; | |
} | |
} | |
window.lazySizesConfig = lazySizesConfig; | |
setTimeout(function(){ | |
if(lazySizesConfig.init){ | |
init(); | |
} | |
}); | |
})(); | |
var loader = (function(){ | |
var preloadElems, isCompleted, resetPreloadingTimer, loadMode, started; | |
var eLvW, elvH, eLtop, eLleft, eLright, eLbottom; | |
var defaultExpand, preloadExpand, hFac; | |
var regImg = /^img$/i; | |
var regIframe = /^iframe$/i; | |
var supportScroll = ('onscroll' in window) && !(/glebot/.test(navigator.userAgent)); | |
var shrinkExpand = 0; | |
var currentExpand = 0; | |
var isLoading = 0; | |
var lowRuns = -1; | |
var resetPreloading = function(e){ | |
isLoading--; | |
if(e && e.target){ | |
addRemoveLoadEvents(e.target, resetPreloading); | |
} | |
if(!e || isLoading < 0 || !e.target){ | |
isLoading = 0; | |
} | |
}; | |
var isNestedVisible = function(elem, elemExpand){ | |
var outerRect; | |
var parent = elem; | |
var visible = getCSS(document.body, 'visibility') == 'hidden' || getCSS(elem, 'visibility') != 'hidden'; | |
eLtop -= elemExpand; | |
eLbottom += elemExpand; | |
eLleft -= elemExpand; | |
eLright += elemExpand; | |
while(visible && (parent = parent.offsetParent) && parent != document.body && parent != docElem){ | |
visible = ((getCSS(parent, 'opacity') || 1) > 0); | |
if(visible && getCSS(parent, 'overflow') != 'visible'){ | |
outerRect = parent.getBoundingClientRect(); | |
visible = eLright > outerRect.left && | |
eLleft < outerRect.right && | |
eLbottom > outerRect.top - 1 && | |
eLtop < outerRect.bottom + 1 | |
; | |
} | |
} | |
return visible; | |
}; | |
var checkElements = function() { | |
var eLlen, i, rect, autoLoadElem, loadedSomething, elemExpand, elemNegativeExpand, elemExpandVal, beforeExpandVal; | |
var lazyloadElems = lazysizes.elements; | |
if((loadMode = lazySizesConfig.loadMode) && isLoading < 8 && (eLlen = lazyloadElems.length)){ | |
i = 0; | |
lowRuns++; | |
if(preloadExpand == null){ | |
if(!('expand' in lazySizesConfig)){ | |
lazySizesConfig.expand = docElem.clientHeight > 500 && docElem.clientWidth > 500 ? 500 : 370; | |
} | |
defaultExpand = lazySizesConfig.expand; | |
preloadExpand = defaultExpand * lazySizesConfig.expFactor; | |
} | |
if(currentExpand < preloadExpand && isLoading < 1 && lowRuns > 2 && loadMode > 2 && !document.hidden){ | |
currentExpand = preloadExpand; | |
lowRuns = 0; | |
} else if(loadMode > 1 && lowRuns > 1 && isLoading < 6){ | |
currentExpand = defaultExpand; | |
} else { | |
currentExpand = shrinkExpand; | |
} | |
for(; i < eLlen; i++){ | |
if(!lazyloadElems[i] || lazyloadElems[i]._lazyRace){continue;} | |
if(!supportScroll){unveilElement(lazyloadElems[i]);continue;} | |
if(!(elemExpandVal = lazyloadElems[i][_getAttribute]('data-expand')) || !(elemExpand = elemExpandVal * 1)){ | |
elemExpand = currentExpand; | |
} | |
if(beforeExpandVal !== elemExpand){ | |
eLvW = innerWidth + (elemExpand * hFac); | |
elvH = innerHeight + elemExpand; | |
elemNegativeExpand = elemExpand * -1; | |
beforeExpandVal = elemExpand; | |
} | |
rect = lazyloadElems[i].getBoundingClientRect(); | |
if ((eLbottom = rect.bottom) >= elemNegativeExpand && | |
(eLtop = rect.top) <= elvH && | |
(eLright = rect.right) >= elemNegativeExpand * hFac && | |
(eLleft = rect.left) <= eLvW && | |
(eLbottom || eLright || eLleft || eLtop) && | |
(lazySizesConfig.loadHidden || getCSS(lazyloadElems[i], 'visibility') != 'hidden') && | |
((isCompleted && isLoading < 3 && !elemExpandVal && (loadMode < 3 || lowRuns < 4)) || isNestedVisible(lazyloadElems[i], elemExpand))){ | |
unveilElement(lazyloadElems[i]); | |
loadedSomething = true; | |
if(isLoading > 9){break;} | |
} else if(!loadedSomething && isCompleted && !autoLoadElem && | |
isLoading < 4 && lowRuns < 4 && loadMode > 2 && | |
(preloadElems[0] || lazySizesConfig.preloadAfterLoad) && | |
(preloadElems[0] || (!elemExpandVal && ((eLbottom || eLright || eLleft || eLtop) || lazyloadElems[i][_getAttribute](lazySizesConfig.sizesAttr) != 'auto')))){ | |
autoLoadElem = preloadElems[0] || lazyloadElems[i]; | |
} | |
} | |
if(autoLoadElem && !loadedSomething){ | |
unveilElement(autoLoadElem); | |
} | |
} | |
}; | |
var throttledCheckElements = throttle(checkElements); | |
var switchLoadingClass = function(e){ | |
addClass(e.target, lazySizesConfig.loadedClass); | |
removeClass(e.target, lazySizesConfig.loadingClass); | |
addRemoveLoadEvents(e.target, rafSwitchLoadingClass); | |
triggerEvent(e.target, 'lazyloaded'); | |
}; | |
var rafedSwitchLoadingClass = rAFIt(switchLoadingClass); | |
var rafSwitchLoadingClass = function(e){ | |
rafedSwitchLoadingClass({target: e.target}); | |
}; | |
var changeIframeSrc = function(elem, src){ | |
try { | |
elem.contentWindow.location.replace(src); | |
} catch(e){ | |
elem.src = src; | |
} | |
}; | |
var handleSources = function(source){ | |
var customMedia; | |
var sourceSrcset = source[_getAttribute](lazySizesConfig.srcsetAttr); | |
if( (customMedia = lazySizesConfig.customMedia[source[_getAttribute]('data-media') || source[_getAttribute]('media')]) ){ | |
source.setAttribute('media', customMedia); | |
} | |
if(sourceSrcset){ | |
source.setAttribute('srcset', sourceSrcset); | |
} | |
}; | |
var lazyUnveil = rAFIt(function (elem, detail, isAuto, sizes, isImg){ | |
var src, srcset, parent, isPicture, event, firesLoad; | |
if(!(event = triggerEvent(elem, 'lazybeforeunveil', detail)).defaultPrevented){ | |
if(sizes){ | |
if(isAuto){ | |
addClass(elem, lazySizesConfig.autosizesClass); | |
} else { | |
elem.setAttribute('sizes', sizes); | |
} | |
} | |
srcset = elem[_getAttribute](lazySizesConfig.srcsetAttr); | |
src = elem[_getAttribute](lazySizesConfig.srcAttr); | |
if(isImg) { | |
parent = elem.parentNode; | |
isPicture = parent && regPicture.test(parent.nodeName || ''); | |
} | |
firesLoad = detail.firesLoad || (('src' in elem) && (srcset || src || isPicture)); | |
event = {target: elem}; | |
if(firesLoad){ | |
addRemoveLoadEvents(elem, resetPreloading, true); | |
clearTimeout(resetPreloadingTimer); | |
resetPreloadingTimer = setTimeout(resetPreloading, 2500); | |
addClass(elem, lazySizesConfig.loadingClass); | |
addRemoveLoadEvents(elem, rafSwitchLoadingClass, true); | |
} | |
if(isPicture){ | |
forEach.call(parent.getElementsByTagName('source'), handleSources); | |
} | |
if(srcset){ | |
elem.setAttribute('srcset', srcset); | |
} else if(src && !isPicture){ | |
if(regIframe.test(elem.nodeName)){ | |
changeIframeSrc(elem, src); | |
} else { | |
elem.src = src; | |
} | |
} | |
if(isImg && (srcset || isPicture)){ | |
updatePolyfill(elem, {src: src}); | |
} | |
} | |
if(elem._lazyRace){ | |
delete elem._lazyRace; | |
} | |
removeClass(elem, lazySizesConfig.lazyClass); | |
rAF(function(){ | |
if( !firesLoad || (elem.complete && elem.naturalWidth > 1)){ | |
if(firesLoad){ | |
resetPreloading(event); | |
} else { | |
isLoading--; | |
} | |
switchLoadingClass(event); | |
} | |
}, true); | |
}); | |
var unveilElement = function (elem){ | |
var detail; | |
var isImg = regImg.test(elem.nodeName); | |
//allow using sizes="auto", but don't use. it's invalid. Use data-sizes="auto" or a valid value for sizes instead (i.e.: sizes="80vw") | |
var sizes = isImg && (elem[_getAttribute](lazySizesConfig.sizesAttr) || elem[_getAttribute]('sizes')); | |
var isAuto = sizes == 'auto'; | |
if( (isAuto || !isCompleted) && isImg && (elem[_getAttribute]('src') || elem.srcset) && !elem.complete && !hasClass(elem, lazySizesConfig.errorClass) && hasClass(elem, lazySizesConfig.lazyClass)){return;} | |
detail = triggerEvent(elem, 'lazyunveilread').detail; | |
if(isAuto){ | |
autoSizer.updateElem(elem, true, elem.offsetWidth); | |
} | |
elem._lazyRace = true; | |
isLoading++; | |
lazyUnveil(elem, detail, isAuto, sizes, isImg); | |
}; | |
var onload = function(){ | |
if(isCompleted){return;} | |
if(Date.now() - started < 999){ | |
setTimeout(onload, 999); | |
return; | |
} | |
var afterScroll = debounce(function(){ | |
lazySizesConfig.loadMode = 3; | |
throttledCheckElements(); | |
}); | |
isCompleted = true; | |
lazySizesConfig.loadMode = 3; | |
throttledCheckElements(); | |
addEventListener('scroll', function(){ | |
if(lazySizesConfig.loadMode == 3){ | |
lazySizesConfig.loadMode = 2; | |
} | |
afterScroll(); | |
}, true); | |
}; | |
return { | |
_: function(){ | |
started = Date.now(); | |
lazysizes.elements = document.getElementsByClassName(lazySizesConfig.lazyClass); | |
preloadElems = document.getElementsByClassName(lazySizesConfig.lazyClass + ' ' + lazySizesConfig.preloadClass); | |
hFac = lazySizesConfig.hFac; | |
addEventListener('scroll', throttledCheckElements, true); | |
addEventListener('resize', throttledCheckElements, true); | |
if(window.MutationObserver){ | |
new MutationObserver( throttledCheckElements ).observe( docElem, {childList: true, subtree: true, attributes: true} ); | |
} else { | |
docElem[_addEventListener]('DOMNodeInserted', throttledCheckElements, true); | |
docElem[_addEventListener]('DOMAttrModified', throttledCheckElements, true); | |
setInterval(throttledCheckElements, 999); | |
} | |
addEventListener('hashchange', throttledCheckElements, true); | |
//, 'fullscreenchange' | |
['focus', 'mouseover', 'click', 'load', 'transitionend', 'animationend', 'webkitAnimationEnd'].forEach(function(name){ | |
document[_addEventListener](name, throttledCheckElements, true); | |
}); | |
if((/d$|^c/.test(document.readyState))){ | |
onload(); | |
} else { | |
addEventListener('load', onload); | |
document[_addEventListener]('DOMContentLoaded', throttledCheckElements); | |
setTimeout(onload, 20000); | |
} | |
if(lazysizes.elements.length){ | |
checkElements(); | |
rAF._lsFlush(); | |
} else { | |
throttledCheckElements(); | |
} | |
}, | |
checkElems: throttledCheckElements, | |
unveil: unveilElement | |
}; | |
})(); | |
var autoSizer = (function(){ | |
var autosizesElems; | |
var sizeElement = rAFIt(function(elem, parent, event, width){ | |
var sources, i, len; | |
elem._lazysizesWidth = width; | |
width += 'px'; | |
elem.setAttribute('sizes', width); | |
if(regPicture.test(parent.nodeName || '')){ | |
sources = parent.getElementsByTagName('source'); | |
for(i = 0, len = sources.length; i < len; i++){ | |
sources[i].setAttribute('sizes', width); | |
} | |
} | |
if(!event.detail.dataAttr){ | |
updatePolyfill(elem, event.detail); | |
} | |
}); | |
var getSizeElement = function (elem, dataAttr, width){ | |
var event; | |
var parent = elem.parentNode; | |
if(parent){ | |
width = getWidth(elem, parent, width); | |
event = triggerEvent(elem, 'lazybeforesizes', {width: width, dataAttr: !!dataAttr}); | |
if(!event.defaultPrevented){ | |
width = event.detail.width; | |
if(width && width !== elem._lazysizesWidth){ | |
sizeElement(elem, parent, event, width); | |
} | |
} | |
} | |
}; | |
var updateElementsSizes = function(){ | |
var i; | |
var len = autosizesElems.length; | |
if(len){ | |
i = 0; | |
for(; i < len; i++){ | |
getSizeElement(autosizesElems[i]); | |
} | |
} | |
}; | |
var debouncedUpdateElementsSizes = debounce(updateElementsSizes); | |
return { | |
_: function(){ | |
autosizesElems = document.getElementsByClassName(lazySizesConfig.autosizesClass); | |
addEventListener('resize', debouncedUpdateElementsSizes); | |
}, | |
checkElems: debouncedUpdateElementsSizes, | |
updateElem: getSizeElement | |
}; | |
})(); | |
var init = function(){ | |
if(!init.i){ | |
init.i = true; | |
autoSizer._(); | |
loader._(); | |
} | |
}; | |
lazysizes = { | |
cfg: lazySizesConfig, | |
autoSizer: autoSizer, | |
loader: loader, | |
init: init, | |
uP: updatePolyfill, | |
aC: addClass, | |
rC: removeClass, | |
hC: hasClass, | |
fire: triggerEvent, | |
gW: getWidth, | |
rAF: rAF, | |
}; | |
return lazysizes; | |
} | |
)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment