I'm looking for some performance suggestions. The purpose is to toggle a data attribute value indicating when an element is either in view or out of view. You can also assign callbacks to be called when the element comes in and out of view. Secondarily, it indicates the direction of the scroll by adding a data attribute on the body tag. The module utilizes a UMD pattern.
First and foremost, should I remove jquery?? Methods like .offset() and .innerHeight() are convenient for cross-browser issues but if it's worth the gain, i could do without.
Please let me know if I can provide additional clarity. Any other feedback would be greatly appreciated.
This module requires FrameEvents, which assign requestAnimationFrame callbacks when an event occurs. In this case, rAF is used to handle scroll and resize events.
Usage example:
var InView = require('inView'),
inView = new InView();
inView.addStage({
element: '.inner-hero__vmid__content'
// other options described in the script
});
And the script:
/**
* InView
*/
var MOD_NAME = 'InView';
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define(['frameEvent'], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(require('frameEvent'));
} else {
root[MOD_NAME] = factory(root.FrameEvent);
}
}(this, function(FrameEvent) {
var _defaults = {
stage: {
element: 'section', // the element to look for
attr: 'data-inview', // the name of the inview attribute
inVal: 'in', // the value of the attribute when in view
outVal: 'out', // the value of the attribute when out of view
offsetTop: 0, // pixels
offsetBottom: 0, // pixels
staggerIn: 0, // in milliseconds
staggerOut: 0, // in milliseconds
inCallback: false, // callback when element comes into view
outCallback: false // callback when element goes out of view
}
};
function Module() {
this.name = MOD_NAME;
this.init();
}
$.extend(Module.prototype, {
config: {},
winHgt: 0,
currPos: 0,
prevPos: 0,
direction: null,
stages: [],
init: function() {
this.fe = new FrameEvent();
this.fe.add('resize', this.handleResize, this);
this.fe.add('scroll', this.handleScroll, this);
},
addStage: function(options) {
var options = options || {},
stage = {};
for (var o in _defaults.stage) stage[o] = (options[o]) ? options[o] : _defaults.stage[o];
$(stage.element).attr(stage.attr, stage.outVal);
this.stages.push(stage);
this.handleResize();
this.handleScroll();
},
clearStages: function() {
this.stages = [];
},
handleScroll: function() {
this.prevPos = this.currPos;
this.currPos = $(window).scrollTop();
this.checkDirection();
this.checkStagePositions();
},
handleResize: function() {
this.winHgt = $(window).innerHeight();
},
checkDirection: function() {
// Indicate scroll direction via body attribute
this.direction = this.currPos > this.prevPos ? 'down' : 'up';
$('body').attr('scroll-direction', this.direction);
},
checkStagePositions: function() {
var self = this;
$(self.stages).each(function(i, stage) {
$(stage.element).each(function(j, el) {
var el = $(el),
elTop = el.offset().top,
topPos = elTop + stage.offsetTop,
botPos = (elTop + el.innerHeight()) - stage.offsetBottom;
// If element comes into view and has an attribute value of outVal
if (self.currPos + self.winHgt > topPos && self.currPos < botPos && el.attr(stage.attr) == stage.outVal) {
if(stage.staggerIn > 0) {
el.data('stagger-in-' + j, setTimeout(function() {
el.attr(stage.attr, stage.inVal);
clearTimeout(el.data('stagger-in-' + j));
}, (j + 1) * stage.staggerIn));
} else {
el.attr(stage.attr, stage.inVal);
}
if ('function' == typeof stage.inCallback) stage.inCallback.call(el);
}
// If element goes out of view and has an attribute value of inVal
if ((self.currPos + self.winHgt < topPos || self.currPos > botPos) && el.attr(stage.attr) == stage.inVal) {
if(stage.staggerOut > 0) {
el.data('stagger-out-' + j, setTimeout(function() {
el.attr(stage.attr, stage.outVal);
clearTimeout(el.data('stagger-out-' + j));
}, (j + 1) * stage.staggerOut));
} else {
el.attr(stage.attr, stage.outVal);
}
if ('function' == typeof stage.outCallback) stage.outCallback.call(el);
}
});
});
}
});
return Module;
}));
And the FrameEvent module:
/**
* FrameEvent
*/
var MOD_NAME = 'FrameEvent';
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
root[MOD_NAME] = factory();
}
}(this, function() {
function Module() {
this.name = MOD_NAME;
this.init();
};
$.extend(Module.prototype, {
events: {
'resize': [],
'scroll': []
},
running: {
'resize': false,
'scroll': false
},
timeouts: {
'resize': false,
'scroll': false
},
init: function() {
for (var type in this.events) {
this[type + 'callback'] = this.run.bind(this);
window.addEventListener(type, this[type + 'callback']);
}
console.dir(this);
},
add: function(type, cb, context) {
this.events[type].push({
cb: cb,
context: context,
reqId: null
});
},
run: function(evt) {
var self = this,
type = evt.type;
for (var i = 0; i < self.events[type].length; i++) {
var cb = self.events[type][i].cb,
context = self.events[type][i].context,
reqId = null;
if (self.running[type] === false) {
self.running[type] = true;
if (window.requestAnimationFrame) {
reqId = requestAnimationFrame(function() {
cb.call(context);
self.running[type] = false;
});
} else {
if (self.timeouts[type] !== false) {
clearTimeout(self.timeouts[type]);
self.timeouts[type] = false;
}
self.timeouts[type] = setTimeout(function() {
cb.call(context);
self.timeouts[type] = false;
}, 100);
}
}
}
}
});
return Module;
}));
1 Answer 1
First and foremost, should I remove jquery??
If you can safely assume they use jQuery then by all means use it.
From what I see, checking if the element is in the view port is simply comparing the element's bounding rectangle with the viewport rectangle. Here's an example of checking if the entire rectangle of the element is fully inside the viewport. This part, no jQuery required.
function isElementInViewport(element){
var viewportWidth = document.documentElement.clientWidth;
var viewportHeight = document.documentElement.clientHeight;
var elementRectangle = element.getBoundingClientRect();
return elementRectangle.left > 0
&& elementRectangle.top > 0
&& elementRectangle.right < viewportWidth
&& elementRectangle.bottom < viewportHeight;
}
Now if you use jQuery, take advantage of the ability to hook it into the $()
call. That way, you can do it like:
$('.items-to-check').inView({
onEnter: function(){...},
onExit: function(){...}
});
The next problem I see is this:
checkStagePositions: function() {
var self = this;
$(self.stages).each(function(i, stage) {
The issue is that checkStagePositions
is being called on scroll events. Scroll events fire a bunch of times even when you moved the mouse wheel a notch. Imagine if I scrolled a few page folds. This means, whatever self.stages
is, it's being wrapped into a jQuery object every time the scroll handler fires. This isn't good for performance.
To preserve the assumption that stages
can be a selector, a reference to a DOM element, or a jQuery object, you could store an internal mapping of (selector | DOM | jQuery object) => internal jQuery object
and operate on the jQuery object value instead. That way, you can safely assume that you're working on a jQuery object while avoiding the performance cost of wrapping as well as avoid the potential event of the user adding a duplicate entry.
this.currPos = $(window).scrollTop();
Same issue as above. I'm pretty sure $(window)
's value won't change throughout the lifetime of the app. Calling it over and over will only make your app slower due to the rewrapping. I suggest you cache the value of $(window)
to a variable and call scrollTop
from it. Same goes for everything that remains constant in the app.
checkStagePositions: function() {
var self = this;
$(self.stages).each(function(i, stage) {
$(stage.element).each(function(j, el) {
var el = $(el),
elTop = el.offset().top,
topPos = elTop + stage.offsetTop,
botPos = (elTop + el.innerHeight()) - stage.offsetBottom;
// If element comes into view and has an attribute value of outVal
if (self.currPos + self.winHgt > topPos && self.currPos < botPos && el.attr(stage.attr) == stage.outVal) {
if(stage.staggerIn > 0) {
el.data('stagger-in-' + j, setTimeout(function() {
el.attr(stage.attr, stage.inVal);
clearTimeout(el.data('stagger-in-' + j));
}, (j + 1) * stage.staggerIn));
} else {
el.attr(stage.attr, stage.inVal);
}
if ('function' == typeof stage.inCallback) stage.inCallback.call(el);
}
// If element goes out of view and has an attribute value of inVal
if ((self.currPos + self.winHgt < topPos || self.currPos > botPos) && el.attr(stage.attr) == stage.inVal) {
if(stage.staggerOut > 0) {
el.data('stagger-out-' + j, setTimeout(function() {
el.attr(stage.attr, stage.outVal);
clearTimeout(el.data('stagger-out-' + j));
}, (j + 1) * stage.staggerOut));
} else {
el.attr(stage.attr, stage.outVal);
}
if ('function' == typeof stage.outCallback) stage.outCallback.call(el);
}
});
});
}
Pulls out vacuum cleaner. I suggest you break this apart into functions. This is a monster, really. I can't even understand what it's doing at first glance. A good metric I use to gauge code readability is when I understand what the code does without tracing through. Same goes for run
in your other module. Looks like it also suffers from "the pyramid of doom" (nested callbacks/conditions).