summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/lazy_loader.js
diff options
context:
space:
mode:
authorLukas Eipert <leipert@gitlab.com>2018-09-06 16:36:05 +0200
committerLukas Eipert <leipert@gitlab.com>2018-10-01 15:42:19 +0200
commit4552a9f9fb14b82ae9c0b6b8cf971b0517721a75 (patch)
treef12fddb7167e77e42d92063fe33b73be61bb6606 /app/assets/javascripts/lazy_loader.js
parentdf73116f75d4a7545fb4a7684aa76624efede7d0 (diff)
downloadgitlab-ce-4552a9f9fb14b82ae9c0b6b8cf971b0517721a75.tar.gz
Improve performance of LazyLoader by using IntersectionObserver35476-lazy-image-intersectionobserver
Every browser which supports IntersectionObserver will now use it over observing scroll and resize events. Older browsers without support fall back on the previous behavior. Additionally the MutationObserver can be enabled and disabled manually via the helper method `startContentObserver` and `stopContentObserver`. This might prove useful on pages where we manipulate the DOM extensively.
Diffstat (limited to 'app/assets/javascripts/lazy_loader.js')
-rw-r--r--app/assets/javascripts/lazy_loader.js107
1 files changed, 89 insertions, 18 deletions
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index bd2212edec7..61b4862b4e3 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -2,54 +2,114 @@ import _ from 'underscore';
export const placeholderImage =
'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
-const SCROLL_THRESHOLD = 300;
+const SCROLL_THRESHOLD = 500;
export default class LazyLoader {
constructor(options = {}) {
+ this.intersectionObserver = null;
this.lazyImages = [];
this.observerNode = options.observerNode || '#content-body';
- const throttledScrollCheck = _.throttle(() => this.scrollCheck(), 300);
- const debouncedElementsInView = _.debounce(() => this.checkElementsInView(), 300);
-
- window.addEventListener('scroll', throttledScrollCheck);
- window.addEventListener('resize', debouncedElementsInView);
-
const scrollContainer = options.scrollContainer || window;
- scrollContainer.addEventListener('load', () => this.loadCheck());
+ scrollContainer.addEventListener('load', () => this.register());
+ }
+
+ static supportsIntersectionObserver() {
+ return 'IntersectionObserver' in window;
}
+
searchLazyImages() {
- const that = this;
requestIdleCallback(
() => {
- that.lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
+ const lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
- if (that.lazyImages.length) {
- that.checkElementsInView();
+ if (LazyLoader.supportsIntersectionObserver()) {
+ if (this.intersectionObserver) {
+ lazyImages.forEach(img => this.intersectionObserver.observe(img));
+ }
+ } else if (lazyImages.length) {
+ this.lazyImages = lazyImages;
+ this.checkElementsInView();
}
},
{ timeout: 500 },
);
}
+
startContentObserver() {
const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
-
if (contentNode) {
- const observer = new MutationObserver(() => this.searchLazyImages());
+ this.mutationObserver = new MutationObserver(() => this.searchLazyImages());
- observer.observe(contentNode, {
+ this.mutationObserver.observe(contentNode, {
childList: true,
subtree: true,
});
}
}
- loadCheck() {
- this.searchLazyImages();
+
+ stopContentObserver() {
+ if (this.mutationObserver) {
+ this.mutationObserver.takeRecords();
+ this.mutationObserver.disconnect();
+ this.mutationObserver = null;
+ }
+ }
+
+ unregister() {
+ this.stopContentObserver();
+ if (this.intersectionObserver) {
+ this.intersectionObserver.takeRecords();
+ this.intersectionObserver.disconnect();
+ this.intersectionObserver = null;
+ }
+ if (this.throttledScrollCheck) {
+ window.removeEventListener('scroll', this.throttledScrollCheck);
+ }
+ if (this.debouncedElementsInView) {
+ window.removeEventListener('resize', this.debouncedElementsInView);
+ }
+ }
+
+ register() {
+ if (LazyLoader.supportsIntersectionObserver()) {
+ this.startIntersectionObserver();
+ } else {
+ this.startLegacyObserver();
+ }
this.startContentObserver();
+ this.searchLazyImages();
}
+
+ startIntersectionObserver = () => {
+ this.throttledElementsInView = _.throttle(() => this.checkElementsInView(), 300);
+ this.intersectionObserver = new IntersectionObserver(this.onIntersection, {
+ rootMargin: `${SCROLL_THRESHOLD}px 0px`,
+ thresholds: 0.1,
+ });
+ };
+
+ onIntersection = entries => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ this.intersectionObserver.unobserve(entry.target);
+ this.lazyImages.push(entry.target);
+ }
+ });
+ this.throttledElementsInView();
+ };
+
+ startLegacyObserver() {
+ this.throttledScrollCheck = _.throttle(() => this.scrollCheck(), 300);
+ this.debouncedElementsInView = _.debounce(() => this.checkElementsInView(), 300);
+ window.addEventListener('scroll', this.throttledScrollCheck);
+ window.addEventListener('resize', this.debouncedElementsInView);
+ }
+
scrollCheck() {
requestAnimationFrame(() => this.checkElementsInView());
}
+
checkElementsInView() {
const scrollTop = window.pageYOffset;
const visHeight = scrollTop + window.innerHeight + SCROLL_THRESHOLD;
@@ -61,18 +121,29 @@ export default class LazyLoader {
const imgTop = scrollTop + imgBoundRect.top;
const imgBound = imgTop + imgBoundRect.height;
- if (scrollTop < imgBound && visHeight > imgTop) {
+ if (scrollTop <= imgBound && visHeight >= imgTop) {
requestAnimationFrame(() => {
LazyLoader.loadImage(selectedImage);
});
return false;
}
+ /*
+ If we are scrolling fast, the img we watched intersecting could have left the view port.
+ So we are going watch for new intersections.
+ */
+ if (LazyLoader.supportsIntersectionObserver()) {
+ if (this.intersectionObserver) {
+ this.intersectionObserver.observe(selectedImage);
+ }
+ return false;
+ }
return true;
}
return false;
});
}
+
static loadImage(img) {
if (img.getAttribute('data-src')) {
let imgUrl = img.getAttribute('data-src');