如何改善我的视差滚动脚本的性能?

9
我正在使用Javascript和jQuery构建一个视差滚动脚本,使用transform:translate3d来操作figure元素中的图像。根据我所了解的(Paul Irish的博客等),为了性能考虑,这个任务的最佳解决方案是使用requestAnimationFrame
虽然我知道如何编写Javascript,但我总是不确定如何编写好的Javascript。特别是,尽管下面的代码似乎可以正确流畅地运行,但我想解决一些在Chrome Dev Tools中看到的问题。
$(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 Dev Tools的“时间轴”选项卡中开始录制时,即使页面上没有执行任何操作,“记录的操作”叠加计数仍然以每秒约40次的速度不断增加。
其次,为什么“动画帧触发”每隔约16毫秒执行一次,即使我没有滚动或与页面交互,如下图所示?
第三,为什么使用的JS堆大小会在我不与页面交互的情况下增加?如下图所示。我已经排除了所有可能引起此问题的其他脚本。
有人能帮我指出如何解决以上问题,并给我改进代码的建议吗?

你需要提供更多的代码才能得到更详细的答案。 - markE
你能否创建一个 jsfiddle.net 的工作示例? - Sash
你有用 Modernizr 吗? - Todd
@markE:这就是整个代码。没有更多了。 - marked-down
@Todd:我没有使用Modernizr。谢谢! - marked-down
我打算尝试帮忙,但是作为一个快速的指针,Modernizr让我的高级Web开发项目变得更加清晰明了。因此,在考虑跨设备设计时,它将是一个有用的补充。 - Todd
4个回答

5

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

(3) 浏览器根据需要为您的动画消耗内存,但浏览器不会立即回收该内存。相反,它偶尔会在垃圾回收过程中回收任何孤立的内存。因此,您的内存消耗应该会上升一段时间,然后以一个大块下降。如果它不表现出这种行为,那么您就有一个内存泄漏。


有什么方法可以避免1和2中发生的情况吗?我知道window.cancelAnimationFrame,但不确定如何将其应用于此上下文。关于3,无论时间如何,内存使用量都会继续上升。 - marked-down
保持动画不停止(一个不执行任何操作的动画循环并不昂贵)。你可以设置/清除一个标志(doAnimate),指示动画循环是否需要执行操作,然后在animateParallaxImages中进行如下操作:if(doAnimate) {...animate stuff...}. 关于你的内存泄漏问题...你只能使用开发工具来追踪问题。祝你项目顺利! - markE

3

编辑:在我写下这篇文章的时候,我还没有看到@user1455003和@mpd的答案。

requestAnimationFrame类似于setTimeout,不同之处在于,浏览器只有在“渲染”循环中才会调用你的回调函数,而这个循环通常每秒发生约60次。另一方面,如果需要,setTimeout可以尽可能快地触发。

无论是requestAnimationFrame还是setTimeout,都必须等待到下一个可用的“节拍”(暂且这样称呼)才能运行。例如,如果你使用requestAnimationFrame,它应该每秒运行大约60次,但如果浏览器的帧速率降至30fps(因为你试图旋转具有大型阴影的巨大PNG),你的回调函数将只运行30次每秒。类似地,如果你使用setTimeout(..., 1000),它应该在1000毫秒后运行。然而,如果某个重任务导致CPU忙于工作,直到CPU有足够的资源,你的回调函数才会被执行。John Resig的文章介绍了JavaScript定时器的工作方式

那么,为什么不使用setTimeout(..., 16)而是要用requestAnimationFrame呢?因为在浏览器的帧速率降至30fps时,你的CPU可能还有很多资源可以利用。在这种情况下,你每秒运行60次计算并试图渲染这些变化,但浏览器只能处理其中一半。如果你这样做,你的浏览器将处于不断追赶的状态...因此requestAnimationFrame的性能更好。

为简洁起见,下面的示例包括所有建议的更改。

您看到动画被频繁触发的原因是因为您有一个“递归”的动画函数,它在不断地触发。如果你不想它经常触发,你可以确保它只在用户滚动时触发。

你看到内存使用量不断上升是由于垃圾回收,这是浏览器清理过时内存的方式。每次定义变量或函数时,浏览器都必须为该信息分配一块内存。浏览器足够聪明,知道你何时停止使用某个特定变量或函数,并释放该内存以供重用——但只有在有足够多的过时内存值得回收时它才会回收垃圾。我无法从你的截图中看到内存图的大小,但如果内存以KB级别递增,则浏览器可能需要数分钟才能清理它。您可以通过重复使用变量名和函数来最小化新内存的分配。在你的例子中,每个动画帧(60x/秒)都定义了一个新函数(在$.each中使用),同时也定义了2个变量(speeddelta)。这些都可以很容易地重复使用(请参见代码)。
如果你的内存使用量持续无限增加,那么你的代码中就有其他的内存泄漏问题。抓杯啤酒开始做研究,因为你发表的代码没有泄露。最大的罪魁祸首是引用一个对象(JS对象或DOM节点),然后删除该节点,但引用仍然存在。例如,如果你将一个点击事件绑定到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();
});

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

0
尝试去掉动画循环,并将滚动变化放入“scroll”函数中。这将防止您的脚本在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)'
        });
    }); 
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接