如何提高视差滚动脚本的性能?

时间:2014-12-29 08:55:28

标签: javascript html performance requestanimationframe

我正在使用Javascript& jQuery构建一个视差滚动脚本,使用figure操作transform:translate3d元素中的图像,并基于我已经完成的阅读(Paul Irish的博客等),I&已被告知此任务的最佳解决方案是出于性能原因使用requestAnimationFrame

虽然我理解如何编写Javascript,但我总是发现自己不确定如何编写好的 Javascript。特别是,虽然下面的代码似乎运行正常且顺利,但我想解决一些我在Chrome开发工具中看到的问题。

$(document).ready(function() {
    function parallaxWrapper() {
        // Get the viewport dimensions
        var viewportDims = determineViewport();         
        var parallaxImages = [];
        var lastKnownScrollTop;

        // Foreach figure containing a parallax 
        $('figure.parallax').each(function() {
            // Save information about each parallax image
            var parallaxImage = {};
            parallaxImage.container = $(this);
            parallaxImage.containerHeight = $(this).height();
            // The image contained within the figure element
            parallaxImage.image = $(this).children('img.lazy');
            parallaxImage.offsetY = parallaxImage.container.offset().top;

            parallaxImages.push(parallaxImage);
        });

        $(window).on('scroll', function() {
            lastKnownScrollTop = $(window).scrollTop();
        });

        function animateParallaxImages() {
            $.each(parallaxImages, function(index, parallaxImage) {
                var speed = 3;
                var delta = ((lastKnownScrollTop + ((viewportDims.height - parallaxImage.containerHeight) / 2)) - parallaxImage.offsetY) / speed;
                parallaxImage.image.css({ 
                    'transform': 'translate3d(0,'+ delta +'px,0)'
                });
            });     
            window.requestAnimationFrame(animateParallaxImages);
        }
        animateParallaxImages();
    }

    parallaxWrapper();
});

首先,当我前往时间轴' Chrome开发者工具中的标签,并开始录制,即使在正在执行的页面上没有任何操作,记录的"操作"覆盖计数继续攀升,速度约为每秒40次。

其次,为什么一个"动画帧被解雇"即使我没有滚动或与页面交互,每隔约16毫秒执行一次,如下图所示?

第三,为什么在没有我与页面交互的情况下,Used JS Heap的大小会增加?如下图所示。我已经删除了可能导致此问题的所有其他脚本。

Chrome dev tools.

任何人都可以帮我解决上述问题,并就如何改进我的代码给出建议吗?

4 个答案:

答案 0 :(得分:5)

(1& 2 - 相同的答案)您使用的模式会创建一个重复的动画循环,尝试以与浏览器刷新相同的速率触发。这通常是每秒60次,所以你看到的活动是大约每1000/60 = 16ms执行的循环。如果没有工作要做,它仍然会每16ms发射一次。

(3)浏览器根据动画的需要消耗内存,但浏览器不会立即回收该内存。相反,它偶尔会在一个名为垃圾收集的过程中回收任何孤立的内存。所以你的内存消耗应该会持续一段时间然后大量下降。如果它没有这种方式,那么你就有内存泄漏。

答案 1 :(得分:3)

编辑:在我写这篇文章的时候,我没有看到@ user1455003和@mpd的答案。他们在我写这本书的时候回答。

requestAnimationFrame类似于setTimeout,但浏览器不会触发您的回调函数,直到它出现在"渲染"循环,通常每秒发生约60次。另一方面,setTimeout可以像你想要的那样快速启动。

requestAnimationFramesetTimeout都必须等到下一个可用的" tick" (因为没有更好的术语),直到它运行。因此,例如,如果您使用requestAnimationFrame,那么每秒运行约60次,但如果浏览器的帧率降至30fps(因为您正在尝试旋转巨人) PNG带有大盒子阴影)你的回调函数每秒只会激发30次。同样,如果您使用setTimeout(..., 1000)在1000毫秒后运行。但是,如果一些繁重的任务导致CPU陷入工作状态,那么你的回调就会被激活,直到CPU有周期给出。 John Resig有great article on JavaScript timers

那么为什么不使用setTimeout(..., 16)而不是请求动画帧呢?因为您的CPU可能有足够的空间,而浏览器的帧速率已下降到30fps。在这种情况下,您将每秒运行60次计算并尝试渲染这些更改,但浏览器只能处理这么多的一半。如果你这样做,你的浏览器将处于不断追赶状态......因此requestAnimationFrame的性能优势。

为简洁起见,我在下面的一个示例中包含了所有建议的更改。

您经常看到动画帧被触发的原因是因为您有一个"递归"不断射击的动画功能。如果你不想让它不断射击,你可以确保它只在用户滚动时触发。

您看到内存使用率攀升的原因与垃圾收集有关,垃圾收集是清除陈旧内存的浏览器方式。每次定义变量或函数时,浏览器都必须为该信息分配一块内存。浏览器足够聪明,可以知道何时使用某个变量或函数并释放该内存以供重用 - 但是,只有在有足够的陈旧内存值得收集时才会收集垃圾。我无法在屏幕截图中看到内存图的比例,但如果内存以千字节大小增加,浏览器可能无法将其清理几分钟。您可以通过重用变量名称和函数来最小化新内存的分配。在您的示例中,每个动画帧(60x秒)定义一个新函数(在$.each中使用)和2个变量(speeddelta)。这些很容易重复使用(见代码)。

如果您的内存使用量不断增加,那么代码中的其他地方就会出现内存泄漏问题。抓住啤酒开始研究,因为您在这里发布的代码是无泄漏的。最大的罪魁祸首是引用一个对象(JS对象或DOM节点)然后被删除,引用仍然挂起。例如,如果将click事件绑定到DOM节点,请删除该节点,并且永远不要取消绑定事件处理程序...那就是内存泄漏。

$(document).ready(function() {
    function parallaxWrapper() {
        // Get the viewport dimensions
        var $window = $(window),
            speed = 3,
            viewportDims = determineViewport(),
            parallaxImages = [],
            isScrolling = false,
            scrollingTimer = 0,
            lastKnownScrollTop;

        // Foreach figure containing a parallax 
        $('figure.parallax').each(function() {
            // The browser should clean up this function and $this variable - no need for reuse
            var $this = $(this);
            // Save information about each parallax image
            parallaxImages.push({
                container = $this,
                containerHeight: $this.height(),
                // The image contained within the figure element
                image: $this.children('img.lazy'),
                offsetY: $this.offset().top
            });
        });

        // This is a bit overkill and could probably be defined inline below
        // I just wanted to illustrate reuse...
        function onScrollEnd() {
            isScrolling = false;
        }

        $window.on('scroll', function() {
            lastKnownScrollTop = $window.scrollTop();
            if( !isScrolling ) {
                isScrolling = true;
                animateParallaxImages();
            }
            clearTimeout(scrollingTimer);
            scrollingTimer = setTimeout(onScrollEnd, 100);
        });

        function transformImage (index, parallaxImage) {
            parallaxImage.image.css({ 
                'transform': 'translate3d(0,' + (
                     (
                         lastKnownScrollTop + 
                         (viewportDims.height - parallaxImage.containerHeight) / 2 - 
                         parallaxImage.offsetY
                     ) / speed 
                ) + 'px,0)'
            });
        }

        function animateParallaxImages() {
            $.each(parallaxImages, transformImage);
            if (isScrolling) {    
                window.requestAnimationFrame(animateParallaxImages);
            }
        }
    }

    parallaxWrapper();
});

答案 2 :(得分:1)

@ markE的回答是正确的1& 2

(3)由于你的动画循环是无限递归的事实:

 function animateParallaxImages() {
        $.each(parallaxImages, function(index, parallaxImage) {
            var speed = 3;
            var delta = ((lastKnownScrollTop + ((viewportDims.height - parallaxImage.containerHeight) / 2)) - parallaxImage.offsetY) / speed;
            parallaxImage.image.css({ 
                'transform': 'translate3d(0,'+ delta +'px,0)'
            });
        });     
        window.requestAnimationFrame(animateParallaxImages); //recursing here, but there is no base base
    }
    animateParallaxImages(); //Kick it off

如果您查看MDN上的示例:

var start = null;
var element = document.getElementById("SomeElementYouWantToAnimate");

function step(timestamp) {
  if (!start) start = timestamp;
  var progress = timestamp - start;
  element.style.left = Math.min(progress/10, 200) + "px";
  if (progress < 2000) {
    window.requestAnimationFrame(step);
  }
} 

window.requestAnimationFrame(step);

我建议在某些时候停止递归,或者重构代码,这样函数/变量就不会在循环中声明:

 var SPEED = 3; //constant so only declare once
 var delta; // declare  outside of the function to reduce the number of allocations needed
 function imageIterator(index, parallaxImage){
     delta = ((lastKnownScrollTop + ((viewportDims.height - parallaxImage.containerHeight) / 2)) - parallaxImage.offsetY) / SPEED;
     parallaxImage.image.css({ 
         'transform': 'translate3d(0,'+ delta +'px,0)'
     });
 }

 function animateParallaxImages() {
    $.each(parallaxImages, imageIterator);  // you could also change this to a traditional loop for a small performance gain for(...)
     window.requestAnimationFrame(animateParallaxImages); //recursing here, but there is no base base
 }
 animateParallaxImages(); //Kick it off

答案 3 :(得分:0)

尝试摆脱动画循环并将滚动更改放在&#39;滚动&#39;功能。当lastKnownScrollTop不变时,这将阻止脚本进行转换。

$(window).on('scroll', function() {
    lastKnownScrollTop = $(window).scrollTop();
    $.each(parallaxImages, function(index, parallaxImage) {
        var speed = 3;
        var delta = ((lastKnownScrollTop + ((viewportDims.height - parallaxImage.containerHeight) / 2)) - parallaxImage.offsetY) / speed;
        parallaxImage.image.css({ 
            'transform': 'translate3d(0,'+ delta +'px,0)'
        });
    }); 
});