Last active
July 6, 2019 20:49
-
-
Save m3g4p0p/e44a1317d2197782faaacebdb2437ece to your computer and use it in GitHub Desktop.
A brief introduction to lazy loading with an eye on performance
This file contains hidden or 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
/** | |
* This class will encapsulate the lazy loading logic. | |
* | |
* We're going to implement it the old-fashioned way using a | |
* scroll event listener, rather than the new Intersection | |
* Observer API; however we will take special care of common | |
* performance issues with this approach. | |
* | |
* Feel free to use this piece of code in production. :-) | |
* | |
* @implements {EventListener} | |
* @license MIT | |
*/ | |
export class LazyLoader { | |
/** | |
* Get all elements with a `data-src` attribute; the value | |
* of that attribute will be set to the actual `src` of the | |
* element when it enters the viewport. We're then converting | |
* the NodeList to an Array which has more useful methods on | |
* its prototype (we're going to use `filter()` later). | |
*/ | |
constructor () { | |
/** | |
* The elements to lazy-load | |
* | |
* @type {HTMLElement[]} | |
* @private | |
*/ | |
this.elements = Array.from( | |
document.querySelectorAll('[data-src]') | |
) | |
/** | |
* Here we're setting a flag that we're using to schedule | |
* the check of the elements to the next animation frame | |
* for better performance | |
* | |
* @type {boolean} | |
* @private | |
*/ | |
this.isCheckScheduled = false | |
} | |
/** | |
* Start listening to scroll events. We're going to implement | |
* the EventListener interface so that we don't have to bother | |
* with `this` bindings; and by making the event listener passive, | |
* we're telling the browser that we're not going to prevent | |
* the event's default behaviour, which makes for better performance. | |
* | |
* @link https://developer.mozilla.org/en-US/docs/Web/API/EventListener | |
* @link https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Improving_scrolling_performance_with_passive_listeners | |
* | |
* Also, we're performing an initial check to load all elements | |
* that are currently in the viewport. | |
*/ | |
start () { | |
window.addEventListener('scroll', this, { passive: true }) | |
this.checkElements() | |
} | |
/** | |
* At some point we'll want to stop listening to scroll events | |
* -- most notably when all elements have been loaded. | |
*/ | |
stop () { | |
window.removeEventListener('scroll', this) | |
} | |
/** | |
* Iterate over all elements, and load those which are currently | |
* in the viewport. After that, filter the elements to those which | |
* have not been loaded yet so that we don't have to unnecessarily | |
* check them again the next time. | |
*/ | |
checkElements () { | |
this.elements.forEach(element => { | |
/** | |
* Get the top and bottom position of the element relative to | |
* the top of the viewport. | |
* | |
* @link https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect | |
*/ | |
const { top, bottom } = element.getBoundingClientRect() | |
/** | |
* If the element is within the viewport, set its `src` to | |
* its `data-src` value. | |
*/ | |
if (top <= window.innerHeight && bottom >= 0) { | |
element.src = element.dataset.src | |
} | |
}) | |
/** | |
* Filter out elements that got their `src` set. Note that we could | |
* have combined that with the actual check above, which would have | |
* saved us one iteration over the elements; but for the sake of | |
* clarity we don't want a filter callback to perform side effects. | |
* | |
* If you want to earn some bonus points though, use a transducer instead. B-) | |
*/ | |
this.elements = this.elements.filter(element => !element.src) | |
/** | |
* If all elements have been loaded, stop listening to scroll events. | |
*/ | |
if (!this.elements.length) { | |
this.stop() | |
} | |
/** | |
* Finally, set the schedule flag to `false`. | |
*/ | |
this.isCheckScheduled = false | |
} | |
/** | |
* Handle scroll events: if a check has already been scheduled, | |
* do nothing; otherwise, schedule `checkElements()` to the next | |
* animation frame. This is a good way to throttle function calls | |
* affecting the DOM (or, more importantly in this case, being | |
* triggered by DOM events), which can get rather expensive otherwise. | |
* | |
* @link https://developer.mozilla.org/en-US/docs/Web/API/EventListener/handleEvent | |
*/ | |
handleEvent () { | |
if (!this.isCheckScheduled) { | |
this.isCheckScheduled = true | |
window.requestAnimationFrame(() => this.checkElements()) | |
} | |
} | |
} |
This file contains hidden or 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"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge"> | |
<title>Lazy Loading Demo</title> | |
<style> | |
body { | |
padding: 2em; | |
background-color: black; | |
} | |
img { | |
display: block; | |
border-radius: 10px; | |
margin: 2em auto; | |
color: white; | |
background-color: #230023; | |
box-shadow: 0px 0px 5px 5px rgba(35,0,35,0.5); | |
} | |
</style> | |
</head> | |
<body> | |
<img data-src="http://lorempixel.com/640/480/city/2" alt="Dummy Image 1" width="640" height="400"> | |
<img data-src="http://lorempixel.com/640/480/city/1" alt="Dummy Image 2" width="640" height="400"> | |
<img data-src="http://lorempixel.com/640/480/city/3" alt="Dummy Image 3" width="640" height="400"> | |
<img data-src="http://lorempixel.com/640/480/city/4" alt="Dummy Image 4" width="640" height="400"> | |
<img data-src="http://lorempixel.com/640/480/city/5" alt="Dummy Image 5" width="640" height="400"> | |
<img data-src="http://lorempixel.com/640/480/city/6" alt="Dummy Image 6" width="640" height="400"> | |
<img data-src="http://lorempixel.com/640/480/city/7" alt="Dummy Image 7" width="640" height="400"> | |
<script type="module"> | |
import { LazyLoader } from './lazy-loader.js' | |
new LazyLoader().start() | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment