13

Why oh why is this so slow? I have made an isometric grid of cubes with css3 that bobs up and down on mouseover. It works great on Firefox with a grid size less than about 12 and with Chrome works pretty good under about 18. I have a decent video card and CPU and the thing that is bugging me is why it is so slow to animate just one cube's animations if I make sure I only mouseover one cube. Does my JavaScript need optimisation or is this just to be expected from the current implementation of browser CSS3 and JavaScript engines? Full test case below includes slider to change grid size on the fly, can either download for yourself or visit this jsfiddle version graciously provided by Doug.

<html>
    <head>
        <style type="text/css">
            body
            {
                background: black;
            }

            .cube
            {
                position: relative;
            }

            .cube .rightFace, .cube .leftFace
            {
                height: 25px; width: 10px; padding: 5px;
            }

            .leftFace
            {
                position: absolute;

                -webkit-transform: skew(0deg, 30deg);
                -moz-transform: skew(0deg, 30deg);
                -o-transform: skew(0deg, 30deg);

                -moz-box-shadow: rgba(0, 0, 0, 0.4) 1px 2px 10px;
                -webkit-box-shadow: rgba(0, 0, 0, 0.4) 1px 2px 10px;
                -o-box-shadow: rgba(0, 0, 0, 0.4) 1px 2px 10px;
                box-shadow: rgba(0, 0, 0, 0.4) 1px 2px 10px;

                border: 1px solid black;
            }

            .rightFace
            {
                -webkit-transform: skew(0deg, -30deg);
                -moz-transform: skew(0deg, -30deg);
                -o-transform: skew(0deg, -30deg);

                position: absolute;
                left: 19.5px;

                border: 1px solid black;
            }

            .topFace div
            {    
                width: 19px;
                height: 19px;


                border: 1px solid black;

                -webkit-transform: skew(0deg, -30deg) scale(1, 1.16);
                -moz-transform: skew(0deg, -30deg) scale(1, 1.16);
                -o-transform: skew(0deg, -30deg) scale(1, 1.16);
            }

            .topFace
            {
                position: absolute;

                left: 10.25px;
                top: -16.5px;

                -webkit-transform: rotate(60deg);
                -moz-transform: rotate(60deg);
                -o-transform: rotate(60deg);
            }

            #slider
            {
                width: 200px;
                margin: 0 auto;
            }
        </style>
        <link type="text/css" rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/base/jquery-ui.css" />
        <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/jquery-ui.min.js"></script>
        <script type="text/javascript">
            function refreshCubes()
            {
                $('.box').empty();
                var x = $("#slider").slider("value");
                var initialTop = 50;
                var initialLeft = 450;

                for(var i = 1; i < x; i++)
                {
                    for(var j = 1; j < x; j++)
                    {
                        var cube = $('<div class="cube"><div class="topFace"><div></div></div><div class="leftFace"></div><div class="rightFace"></div></div>');

                        cube.css(
                        {
                            left : initialLeft + (20 * i) + (-19 * j) + 'px',
                            top : initialTop + (11 * i) + (11 * j) + 'px'
                        });

                        cube.find('.topFace div').css('background', 'rgb(100,' + Math.ceil(255 - 16 * i) + ',' + Math.ceil(255 - 16 * j) + ')');
                        cube.find('.rightFace').css('background', 'rgb(35,' + Math.ceil(190 - 16 * i) + ',' + Math.ceil(190 - 16 * j) + ')');
                        cube.find('.leftFace').css('background', 'rgb(35,' + Math.ceil(190 - 16 * i) + ',' + Math.ceil(190 - 16 * j) + ')');
                        cube.children('div').css('opacity', '.9');

                        cube.hover(function()
                        {
                            $(this).animate({top: '-=25px'}, 400, 'easeInCubic');

                        }, function()
                        {
                            $(this).animate({top: '+=25px'}, 400, 'easeOutBounce');
                        });

                        $('.box').append(cube);
                    }
                }
            }

            $(document).ready(function()
            {    
                $('#slider').slider(
                {
                    value: 9,
                    max: 30,
                    min: 2,
                    slide: refreshCubes,
                    change: refreshCubes
                });

                refreshCubes();
            });
        </script>
    </head>
    <body>
        <div id="slider"></div>
        <div class="box"></div>
    </body>
</html>
trusktr
  • 44,284
  • 53
  • 191
  • 263
AmbrosiaDevelopments
  • 2,576
  • 21
  • 28
  • That's interesting. Its pretty slow with a bigger grid size. It must be because it is software rendering??? Have you tried it in IE9+? IE9 seems to have hardware acceleration for DOM manipulation. I'm reinstalling IE9 right now... – trusktr Nov 05 '11 at 05:15
  • I grabbed IE9 and Chrome as soon as I noticed it was slower with the bigger grid but not much changed – AmbrosiaDevelopments Nov 05 '11 at 06:06
  • I wonder if hardcoding a series of animations that would simulate a mouse moving over the large grid (instead of using the mouseover events) would still be too slow. – AmbrosiaDevelopments Nov 05 '11 at 06:15

3 Answers3

9

This one is faster: http://jsfiddle.net/JycdN/1/

I optimized the jQuery code itself. Also, its a bad idea to animate a "+=25px" style CSS value because each time you do that you are forcing the browser to make extra CSS calculations on top of the animation calculations. Instead of doing that, you might as well just use plain CSS3 animations. Check this out: http://matthewlein.com/ceaser/

Its better for animate() to have a static value (in this case, the original and raised positions which i've stored for each cube in the data- attributes) so the only calculations being done are the ones by the javascript interpreter itself.

The live() method automatically adds the events to the cubes whenever they are recreated, so there's no need to set the events inside the refreshcubes function. Also, James Montagne pointed out that using the live() method makes the whole thing behave like Daniel described in his answer (which is faster). EDIT: I've made it even faster by using the on() method instead of live(). The event context for hovering is now on each cube instead of on the whole document. See this question for details: How does jQuery's new on() method compare to the live() method in performance?.

Wherever possible, make one single jquery object and reference to it with a variable, that way you don't recreate a new object every time.

Overall, I noticed quite an increase in performance, but its still slower than I'd imagine capable. Maybe the CSS transforms are re-calculated every time the cubes are moved (even by a pixel).

EDIT: As addedlovely suggested, modifying the jQuery.fx.interval helped make it a little faster.

I think you'll have more luck drawing the cubes in 2D canvas or with 3D webGL canvas.

Try using three.js or processing.js to draw the cubes and animate them.

EDIT: I've made it even faster using hardware-accelerated CSS3 animations in Google Chrome (other browsers are still developing these CSS3 features so it only works in CHrome for now): http://jsfiddle.net/trusktr/JycdN/35/

Community
  • 1
  • 1
trusktr
  • 44,284
  • 53
  • 191
  • 263
  • Interesting point about the `live()` method. I will definitely implement that. Thanks for the animation optimisation pointer. I have one written in 2D Canvas but it also slowed down considerably after reaching grid sizes above 15. Haven't dabbled in 3D webGL or used three.js or processing.js yet but might soon. – AmbrosiaDevelopments Nov 05 '11 at 06:09
  • The cool thing about WebGL will be that everything will run smooth with the video acceleration. :D – trusktr Nov 05 '11 at 06:19
  • I just tested it in IE9. Its pretty bad! – trusktr Nov 05 '11 at 06:21
  • Just tried removing the mouseover event handlers and added CSS3 transitions/transformations(translateY) and its even slower or less responsive on the hovers anyway. – AmbrosiaDevelopments Nov 05 '11 at 07:50
  • 1
    Hey, check it out, I made it better: http://jsfiddle.net/trusktr/JycdN/ ..Now the animations don't repeat over and over for each time the mouse enters and leaves. However, the whole animations still animate entirely. You might have noticed that simply adding `.stop()` before `.animate()` caused the animations to stop and the boxes wouldn't pop out all the way. This is what I mean by that: http://jsfiddle.net/trusktr/JycdN/16/ ...I think that's pretty much as fast as its going to get. – trusktr Nov 09 '11 at 03:05
  • Yeah, its not really the javascript animation that causing the problem. If you remove all the CSS transformations (the ones that transform the shape of the elements) then it runs perfectly with as many cubes as possible (slider set to 30): http://jsfiddle.net/trusktr/JycdN/17/ – trusktr Nov 09 '11 at 03:15
  • What you can do in this case is instead of transforming the elements to create cubes, draw each cube inside a canvas element, then continue to animate the position of the `.cube` divs just like we are now. That should be pretty smooth I think. – trusktr Nov 09 '11 at 03:17
  • Check this out. Just removing the shadows from the CSS made it considerably faster, but still not as fast as with all the transformations removed: http://jsfiddle.net/trusktr/JycdN/19/ The lack of shadow hardly makes a difference in the way it looks either. – trusktr Nov 09 '11 at 03:20
  • Hey, check it out, I've made it even faster: http://jsfiddle.net/JycdN/ By using jQuery's deprecated delegate() method or the new on() method I've binded the hover events directly to the cubes so the event context happens on each cube instead of on the entire document for each cube. Its a tiny bit faster now. See my question here for more info on these performance gains: http://stackoverflow.com/questions/8541825/. – trusktr Dec 17 '11 at 01:34
  • Hmmmmm..... Its debateable though. If you read that question, you can decide for yourself which method of setting up the hover event is better. :) – trusktr Dec 17 '11 at 01:42
  • It seems like the only real performance loss comes from using the `skew` property from CSS3. I think that the problem is that the skew property has to be applied after every single frame in the animation, making it really slow. I don't see why browsers don't just use OpenGL for rendering their DOMs when the user has OpenGL available. – trusktr Dec 17 '11 at 01:45
  • Also, changing the fx interval like @addedlovely suggested in his answer made it a little better too! – trusktr Dec 17 '11 at 01:49
  • Hey, check it out, I made it way WAY faster in Google Chrome using hardware-accelerated CSS3 transforms/transitions. The improvement is huge! http://jsfiddle.net/trusktr/JycdN/35/ This only works in Webkit browsers though, because the standard isn't fully implemented in all the browsers yet, but will be soon. – trusktr Jan 11 '12 at 07:07
  • After you check that out, check this out, here's how you can make a bounce animation with the cubic-bezier function in CSS3: http://cubic-bezier.com/#.39,1.59,.28,.74 (Set the time to 0.4 seconds to make it look more like a real bounce) Next imagine using that as a second animation to emulate the bounce you originally had with jQuery animations. – trusktr Jan 11 '12 at 07:16
3

You have n^2 listeners, honestly surprised things work as well as they do. Jquery does not have enough information to optimize the algorithm, but we do. Instead of caring about where the mouse is n^2 times we actually only need to check once.

Add a listener to the entire board. On call calculate which cell is being hovered over and animate that one. Keep a list of cells in the 'on' state and calculate if the mouse is still hovering over them, if not animate them to their original state and remove them from the list.

Now instead of n^2 operations a frame you have 1. More, but much faster, code and the same functionality. Yes jquery is awesome and having to do this yourself is a pity. Keep in mind though that jquery is designed for those 10s of listeners which an average site will use, not for 100s or 1000s.

@trusktr: It is indeed faster but still many times slower than necessary.

Daniel
  • 126
  • 3
  • That's interesting. So are you saying that every time you move the mouse all the hover events are being activated, but not necessarily acted upon? I never thought it was like that. Also, can you fork Ambrosia's or My fiddle and implement your idea? – trusktr Nov 05 '11 at 05:39
  • `live` or even better `delegate` works very similar to what you have described. It only attaches one listener and then determines the matching element within. – James Montagne Nov 05 '11 at 05:39
  • @James Montagne If that's the case, then that might explain why my answer was a bit faster. – trusktr Nov 05 '11 at 05:41
  • Interesting, I wouldn't have thought the browser would have such a hard time with the event handlers as you say. I wouldn't mind a jsfiddle fork if you have the time but I will probably reread your answer and play around with it later tonight or tomorrow if I have time. – AmbrosiaDevelopments Nov 05 '11 at 06:11
3

In addition to the excellent points above you may want to check out the Jquery fx Interval.

http://api.jquery.com/jQuery.fx.interval/

By default the animate intervals are set to 13ms, about 76 frames a second. Seems silly, so setting this down to a more 20 frames a seconds (50ms), will give you a performance boost.

addedlovely
  • 3,076
  • 3
  • 22
  • 37
  • I tested what u said, but it didn't have any obvious effects. In my code I just move a large image by 2000px to left. – Ata Iravani Sep 10 '12 at 06:53