3
\$\begingroup\$

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;
}));
\$\endgroup\$
1
\$\begingroup\$

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).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.