4
\$\begingroup\$

I wanted to get some feedback on this JS I am working on. It works wonderfully on my desktop, but on my and my friends' laptops (especially Safari) it is very slow. I need some pointers on improving the efficiency so it works nicely across browsers/computers.

http://www.dapper-apps.com

// If jQuery was not loaded, offload it.
if (!typeof jQuery) {
    document.write(unescape("%3Cscript src='/resources/jquery-2.1.0.min.js' type='text/javascript'%3E%3C/script%3E"));
}

/*
 * Called when the DOM has been loaded.
 * Create all parallaxElements with their respective properties, pool them
 * in an array and begin the parallax animations.
 */
$(document).ready(function () {
    var kami = new ParallaxElement("#kami", 1, 1, $("#parallaxcanvas").width(), $("#parallaxcanvas").height(), 1);
    var mainrock = new ParallaxElement("#mainrock", .25, .25, 75, 25, 1);
    var bgrock1 = new ParallaxElement("#bgrock1", .12, .12, 75, 75, 1);
    var bgrock2 = new ParallaxElement("#bgrock2", .06, .06, 75, 75, 1);
    var bgrock3 = new ParallaxElement("#bgrock3", .03, .03, 75, 75, 1);

    var parallaxElements = [ kami, mainrock, bgrock1, bgrock2, bgrock3];

    Parallax("#parallaxcanvas", parallaxElements);
});

/*
 * jstr - The css identifier of the parallax element(s).
 * xStep - The amount to move, every update, along the x.
 * yStep - The amount to move, every update, along the y.
 * xRange - The range this element is allowed to move along the x.
 * yRange - The range this element is allowed to move along the y.
 * update - How often to update this element's position in milliseconds.
 */
function ParallaxElement(jstr, xStep, yStep, xRange, yRange, update) {
    var init_pos = $(jstr).position();
    var followPos = { left: init_pos.left, top: init_pos.top };
    var moveInterval;
    var minX = init_pos.left - xRange;
    var maxX = init_pos.left + xRange;
    var minY = init_pos.top - yRange;
    var maxY = init_pos.top + yRange;
    /*
     * Gets called every update milliseconds.
     * Adjusts this parallaxelement to follow the followPos.
     */
    this.BeginFollow = function() {
        moveInterval = setInterval(function() {
            var curPos = $(jstr).position();
            if (curPos.left < followPos.left && (xStep >= 0 ? curPos.left < maxX : curPos.left > minX)) {
                curPos.left += xStep;
            }
            if (curPos.left > followPos.left && (xStep >= 0 ? curPos.left > minX : curPos.left < maxX)) {
                curPos.left -= xStep;
            }
            if (curPos.top < followPos.top && (yStep >= 0 ? curPos.top < maxY : curPos.top > minY)) {
                curPos.top += yStep;
            }
            if (curPos.top > followPos.top && (yStep >= 0 ? curPos.top > minY : curPos.top < maxY)) {
                curPos.top -= yStep;
            }
            $(jstr).css({ "left": curPos.left, "top": curPos.top });
        }, update);
    };

    /*
     * Changes the followPos to a copy of the provided top and left coordinates.
     */
    this.UpdateFollow = function(left, top) {
        // Adjust left and top origin to be the lower middle of the element.
        followPos.left = left - ($(jstr).width() / 2);
        followPos.top = top - ($(jstr).height() / 1.5);
    }

    /*
     * Stops this element from following the followPos, resetting such
     * position and the element's position to it's original location.
     */
    this.EndFollow = function() {
        // Reset the position of all elements.
        clearInterval(moveInterval);
        $(jstr).css({ "left": init_pos.left, "top": init_pos.top });
        followPos = { left: init_pos.left, top: init_pos.top };
    };
}

/*
 * Driver function for the parallax effect.
 * Listens to the mouseenter, mousemove, mouseleave events of the provided
 * parallax canvas element and relays event information to the 
 * provided parallaxElements.
 * Also performs all image source swapping for animations.
 */
function Parallax(canvas, parallaxElements) {

    // Hook mouse events.
    $(canvas).mouseenter(initParallax);
    $(canvas).mousemove(deltParallax);
    $(canvas).mouseleave(resetParallax);

    // timeout for when the user stops moving the mouse for 500ms.
    var stillTimeout;
    // Canvas argument.
    var canvasOffset = $(canvas).offset();
    // The last x and y coordinates used.
    var x, y;

    /*
     * Initializes the parallax animations by making all parallaxElements
     * begin following, and inits the stillTimeout which changes the
     * image source after 500 milliseconds, when there is no mouse activity.
     */
    function initParallax(event) {
        for (i = 0; i < parallaxElements.length; i++) {
            parallaxElements[i].BeginFollow();
        }

        // Swap to the regular skin when the user has not moved the mouse.
        stillTimeout = setTimeout(function() {
            if ($("#kami img").attr("src") != "media/kami-static.png") {
                $("#kami img").attr("src", "media/kami-static.png");
            }
        }, 500);
    }

    /*
     * Function called when the user moves the mouse around the
     * parallax canvas element. Updates the position to follow for
     * each of the parallaxElements.
     * Also swaps the image source depending on the direction of mouse
     * movement for animations. Resets the stillTimeout since the user
     * has moved the mouse.
     */
    function deltParallax(event) {
        // Calculate the canvas' offset.
        x1 = event.pageX - canvasOffset.left;
        y1 = event.pageY - canvasOffset.top;

        // Perform img src swapping animations.
        var srcAttr = $("#kami img").attr("src");
        if (x1 < x && srcAttr != "media/kami-left.png") {
            $("#kami img").attr("src", "media/kami-left.png");
        }
        if (x1 > x && srcAttr != "media/kami-right.png") {
            $("#kami img").attr("src", "media/kami-right.png");
        }

        // Update parallaxElement follow coordinates.
        for (var i = 0; i < parallaxElements.length; i++) {
            parallaxElements[i].UpdateFollow(x, y);
        }

        // Reset the still timer.
        clearTimeout(stillTimeout);
        stillTimeout = setTimeout(function() {
            if ($("#kami img").attr("src") != "media/kami-static.png") {
                $("#kami img").attr("src", "media/kami-static.png");
            }
        }, 500);

        // Last coordinates used.
        x = x1;
        y = y1;
    }

    /*
     * Called when the user's mouse leaves the parallaxCanvas element.
     * Resets the position of all parallaxElements to their original position,
     * and clears the stillTimeout.
     */
    function resetParallax(event) {
        for (var i = 0; i < parallaxElements.length; i++) {
            parallaxElements[i].EndFollow();
        }
        clearTimeout(stillTimeout);
    }
}

The first iteration I had would move these images however much the user would move the mouse, and this was even snappier than what I have now, except again, on laptops, and most especially, Safari. I changed it to "follow" in the hopes that it would perform better.

It's also on Github if anyone would prefer to review through there.

\$\endgroup\$
4
+50
\$\begingroup\$

The one millisecond movement timer is extremely low. With 10 ms, you'll still provide a fluent movement, and the browser actually gets time to finish before the next tick is started. Even 5ms will do, but 1 seems overkill.

To get the same step movement and be able to tweak the update timer, you could put the following in the init of ParallaxElement:

xStep *= update;
yStep *= update;

Although that might get further outside of the intended bounds for larger updates.


A small gain can be obtained by using a direct variable to the element inside a ParallaxElement. Each time $ is used, a lookup is started. Since you did use an id, (so getelementbyid is used), the lookup is plenty fast, but no lookup at all is faster. Since the lookup is done so much (multiple times per element per tick), the difference could be notable.

function ParallaxElement(jstr, xStep, yStep, xRange, yRange, update) {
    var element = $(jstr); //refer directly to the element
    var init_pos = element.position(); //replace the $(jstr) references with the direct variable
    //...
    //var curPos = element.position();
    //element.css({ "left": curPos.left, "top": curPos.top });
    //etc.

Minor issue, the first call to UpdateFollow is moot, because x and y are set after the call. x1 and y1 could be used instead parallaxElements[i].UpdateFollow(x1, y1);


Some minor tweaks, such as moving the range checking, could be done, but that would all be small potatoes compared to changing the interval of 1ms. A lot can be done as well with the jquery animate functionality, if you're not happy with the look and feel if the interval is upped.

\$\endgroup\$
  • \$\begingroup\$ You could even make element a global variable (outside the function) so that the lookup is only performed once per page load instead of once per update. \$\endgroup\$ – Eliot May 13 '14 at 23:40
  • \$\begingroup\$ I think having the scope be the ParallaxElement for the element variable is more appropriate. \$\endgroup\$ – Francisco Aguilera May 14 '14 at 0:26
  • \$\begingroup\$ See the latest on the site or github. I may tweak it a bit in the future, but as of now performance-wise it is a bit better. \$\endgroup\$ – Francisco Aguilera May 15 '14 at 3:03
3
\$\begingroup\$

One thing I thought of is having a "static" interval rather than an individual timeout per element. What other things could I improve? I feel like i should definitely be getting better performance, especially in safari. It's painful to watch there.

Some thoughts: Is there a way to make the timer tick less often and maybe I could update the follow coordinates less often and extrapolate or model the path the image needs to take rather than correct it so often? That could present problems if the update rate Is not fast enough, however.

\$\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.