Skip to content

Instantly share code, notes, and snippets.

@m3g4p0p
Last active July 6, 2019 20:49
Show Gist options
  • Save m3g4p0p/e44a1317d2197782faaacebdb2437ece to your computer and use it in GitHub Desktop.
Save m3g4p0p/e44a1317d2197782faaacebdb2437ece to your computer and use it in GitHub Desktop.
A brief introduction to lazy loading with an eye on performance
/**
* 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())
}
}
}
<!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