6
\$\begingroup\$

I've a code which applies marquee to certain elements by using the requestAnimFrame method. However, when I test my application on a lower spec PC (Intel Celeron 2.13GHz dou core) the CPU usage is skyrocketing and gets to a minimum of around 80%! (I have to mentioned that I have 2 elements that the custom maruqee is applied to).

Also, there are cases where even 3 or 4 elements are being targeted by the marquee. There is also a case in my application, not so common, that there is another animation that scrolls text from the right to the left.

My development environment is a lot different, I'm using a 3.8GHz 8-cores CPU with a strong GPU so my CPU usage is no more than 11%.

I'm not sure if it matters but locally I'm using xampp and on the tested PC I've been using wamp.

The code for my marquee is splitted into few functions:

/**
 * Applies the marquee function to the elements who are overflowing
 */
function tryMarquee(/**/) {

    var args = arguments;

    for(var i=0; i<args.length; i++) {
        var elem = $(args[i]);
        var containerHeight = elem.outerHeight(true);
        var contentHeight = calculateContentHeight(elem);

        // extract args
        var settings = $('.marqueeSettings');
        var speed = settings.find('input[data-target="'+args[i]+'"]input[name=marqueeSpeed]').val() || 1;
        var spacer = settings.find('input[data-target="'+args[i]+'"]input[name=marqueeSpacer]').val() == "1";
        var spacerHeight = settings.find('input[data-target="'+args[i]+'"]input[name=marqueeSpacerHeight]').val() || 60;

        if(contentHeight > containerHeight && containerHeight > 10) {
            marquee(args[i], speed, spacer, spacerHeight);
        }

    }

}

/**
 * Calculates the content height of an element by it's children's height
 * @param elem
 * @returns {number}
 */
function calculateContentHeight(elem) {

    var total = 0;
    elem.children().not('.clone').each(function() {
        if(parseInt($(this).css('margin-top')) >= 0)
            total += $(this).height() + parseInt($(this).css('margin-top'));
        else
            total += $(this).height();
    });

    total -= 10;
    return total;

}

/**
 * Generates a spacer.
 * @param marginTop
 * @param height
 * @returns {*|jQuery|HTMLElement}
 */
function generateSpacer(marginTop, height) {

    var spacer = $('<div class="marquee-spacer clone" style="margin-top: '+marginTop+'px;"></div>');

    height = height || 60;

    spacer.css({
        height: height
    });

    return spacer;

}

/**
 * returns a clone of an element's children
 * @param elem
 * @returns {*|jQuery|HTMLElement}
 */
function duplicateContent(elem) {
    return elem.children().clone().addClass('clone');
}
/**
 * Checks the prayers element still needs the marquee.
 * @returns {boolean}
 */
function keepPrayersMaruqee() {

    var containerHeight = elem.outerHeight(true);
    var contentHeight = calculateContentHeight(prayers);

    return contentHeight > containerHeight;

}

These functions are only triggered once so I'm not sure if it has something to do when the application is fully loaded and already running.

function marquee(className, scrollAmount, spacer, spacerHeight) {

    // parse spacer
    spacer = spacer || false;

    // select the elements
    var elemSet = $(className);

    // loop through the element set
    elemSet.each(function() {

        var $this = $(this);

        $this.addClass('marquee');

        /**
         *  TODO: if the container is taller than the content we should clone the content so there's no gap in the loop
         */

        var initialMargin = parseInt($this.find("div").first().css('margin-top'));

        if(spacer)
            $this.append(generateSpacer($this, initialMargin, spacerHeight));

        $this.append(duplicateContent($this));

        (function loop(){

            /**
             *  This block of code is only executed on a certain element so check if it still needs to be scrolled
             *  because there is a possibility that the element's children will be removed dynamically.
             */
            if($this.hasClass('prayers'))
                if( ! keepPrayersMaruqee()) {
                    // remove clone elements
                    $this.children('.clone').remove();
                    // cancel the maruqee
                    return;
                }


            var first = $this.find("div").first();
            var top = parseInt(first.css('margin-top'));
            var height = first.outerHeight();

            if ((height+top) > 0){
                first.css('margin-top','-='+scrollAmount);
            } else {
                first.appendTo($this);
                first.css('margin-top',initialMargin);
            }

            /**
             * repeat the animation
             * @see window.requestAnimFrame
             */
            requestAnimFrame(loop);
        })();

    });
}

A working fiddle

This is where the magic happens. I was trying to make this code as efficient as possible. Any suggestion on how I can actually improve this code and make it consume much less CPU power?

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Profiling in Opera suggests that it is mainly the time taken to draw the text which is consuming CPU (I do this in Opera simply because Opera is the only browser I know of that has a comprehensive profiler which profiles all aspects of page generation). Rendering fonts is one of the slowest things a web browser can do, and it appears that the browser re-renders the text on each frame, and that is basically what is consuming the cpu like crazy. And it appears that regardless of what mechanism is used to scroll the text the repaint is triggered. Some fonts are more work to render than others and you'd benefit from a font which is quick to render, unfortunately I don't know if

As an experiment I tried putting a (large) image in the marquee and the CPU usage is much less when scrolling an image. This makes sense because it's a lot less work for a computer to blit an image than to render fonts. Hence one option would be to marquee an image of text instead of text. I know this isn't really a good solution for a lot of reasons, but it would be fast.

The other solution is a really obvious and simple one - reduce the number of frames rendered per second. At the moment your code runs at 60fps which is way higher than is really needed. Actually, at a reasonable text scrolling rate for human reading speed, you could get away with 20fps or maybe even 10fps.

At the moment you use a constant scroll amount per frame. This is not a good way to do it. Instead you should use a constant scroll rate per unit time, meaning the text scrolls at the same rate even if the browser chooses to render fewer than 60fps. Essentially what you need to do is calculate the elapsed time since the last frame, and use that to determine how far to scroll. The callback for requestAnimationFrame is passed the current time (in milliseconds) so this is quite easy, here is a minimalistic example:

function gogogo(scrollRatePerSecond, frame_skip) {
    var lastFrameTime = null,
        i = 0;
    function loop(now) {
        var delta = lastFrameTime ? now - lastFrameTime : 0;
        if (++i % (frame_skip + 1) == 0) {
            lastFrameTime = now;

            var scrollAmount = delta * scrollRatePerSecond / 1000;
            // Do something with scrollAmount
        }
        requestAnimationFrame(loop);
    }
    requestAnimationFrame(loop);
}

In that code I've also added a frameskip option, which means 'skip this many frames for each one rendered'. You can set the rate per second to get the desired speed, and then adjust frameskip to get the desired performance.

\$\endgroup\$
7
  • \$\begingroup\$ Wow, thanks! MUCH appreciated! Just one thing, I'm not sure how to integrate the code you just posted here with my current code. It seems like you are overriding my scrollAmount variable, thus I'm not sure how do you take into account the option where I can change the speed of the animation (perhaps scrollRatePerSecond?). BTW, what does now represent? \$\endgroup\$
    – kfirba
    Commented Oct 23, 2014 at 7:41
  • \$\begingroup\$ Here is my attempt to integrate it: jsfiddle.net/e5qkhb3e/2. However, the animation is so laggy, I wanted it to be smooth, is there any way to do that? \$\endgroup\$
    – kfirba
    Commented Oct 23, 2014 at 8:11
  • \$\begingroup\$ Your code actually has quite a lot of bugs, for example the settings aren't actually loading because the first part of the input selector fails, meaning it always uses '1'. Frameskip is the number of frames skipped per 1 rendered, so you want a low value (i.e. skip=1=30fps, skip=2=20fps, skip=10=6fps). \$\endgroup\$ Commented Oct 23, 2014 at 8:35
  • \$\begingroup\$ I fixed that, was a minor issue. jsfiddle.net/e5qkhb3e/3. BTW, I've seen another solution to limit the FPS rate: and I've also tested it: jsfiddle.net/e5qkhb3e/4 Which of the approach you believe is more efficient? \$\endgroup\$
    – kfirba
    Commented Oct 23, 2014 at 8:54
  • \$\begingroup\$ They're actually the same, requestAnimationFrame passes the callback the value now so there is no need to calculate it manually using new Date.getTime(). But if you were going to shim requestAnimationFrame you would do it that way. Doing it manually is a little inferior because the value passed by requestAnimationFrame is from a high resolution timer, unlike getTime. Using an interval method lets you use arbitrary values for fps (like 25fps) though in practise that's not that useful. \$\endgroup\$ Commented Oct 23, 2014 at 9:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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