我正在使用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的大小会增加?如下图所示。我已经删除了可能导致此问题的所有其他脚本。
任何人都可以帮我解决上述问题,并就如何改进我的代码给出建议吗?
答案 0 :(得分:5)
(1& 2 - 相同的答案)您使用的模式会创建一个重复的动画循环,尝试以与浏览器刷新相同的速率触发。这通常是每秒60次,所以你看到的活动是大约每1000/60 = 16ms执行的循环。如果没有工作要做,它仍然会每16ms发射一次。
(3)浏览器根据动画的需要消耗内存,但浏览器不会立即回收该内存。相反,它偶尔会在一个名为垃圾收集的过程中回收任何孤立的内存。所以你的内存消耗应该会持续一段时间然后大量下降。如果它没有这种方式,那么你就有内存泄漏。
答案 1 :(得分:3)
编辑:在我写这篇文章的时候,我没有看到@ user1455003和@mpd的答案。他们在我写这本书的时候回答。
requestAnimationFrame
类似于setTimeout
,但浏览器不会触发您的回调函数,直到它出现在"渲染"循环,通常每秒发生约60次。另一方面,setTimeout
可以像你想要的那样快速启动。
requestAnimationFrame
和setTimeout
都必须等到下一个可用的" 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个变量(speed
和delta
)。这些很容易重复使用(见代码)。
如果您的内存使用量不断增加,那么代码中的其他地方就会出现内存泄漏问题。抓住啤酒开始研究,因为您在这里发布的代码是无泄漏的。最大的罪魁祸首是引用一个对象(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)'
});
});
});