带 transform3d、油门、requestAnimationFrame、will-change 的视差动画在移动和桌面、Chrome 和 Safari 上仍然滞后
Parallax-Animation with transform3d, throttle, requestAnimationFrame, will-change still laggy on Mobile and Desktop, Chrome and Safari
我目前正在尝试开发一个小型轻量级 vanilla-js parallax-library,它可以在没有任何背景位置的情况下在两个方向上水平和垂直地放置视差元素(图像、视频、滑块...) .
到目前为止,它运行良好,并且在期待一个重要细节方面做得很好:有时它仍然很慢。我已经在 Mac OS 和 iPhone、Safari 和 Chrome 浏览器中对其进行了测试。都一样。
我已经尝试使用节流、requestAnimationFrame 和 CSS 的 will-change 等常见做法,但没有成功。
我不确定,是否可以在 this video 中看到这里的滞后。
但我做了一个例子,你可以测试它:
https://codepen.io/flexplosion/pen/RwgQXxo?editors=1111
有人有想法,我怎样才能提高视差的滚动性能?
var parallaxes = document.getElementsByClassName('parallax');
const windowInnerHeight = window.innerHeight;
function throttle (callback, limit) {
var wait = false; // Initially, we're not waiting
return function () { // We return a throttled function
if (!wait) { // If we're not waiting
callback.call(); // Execute users function
wait = true; // Prevent future invocations
setTimeout(function () { // After a period of time
wait = false; // And allow future invocations
}, limit);
}
}
}
Array.from(parallaxes).forEach((parallax) => {
const movement = parseFloat(parallax.dataset.movement),
direction = parallax.dataset.direction,
element = parallax.children[0];
if( direction == 'horizontal' ) {
// Prepare for orzintal
element.style.height = '100%';
element.style.width = (100 + movement) + "%";
} else {
// Otherwise prepare for vertical
element.style.height = (100 + movement) + "%";
element.style.width = '100%';
}
});
const handleScroll = () => {
var parallaxes = document.getElementsByClassName('parallax');
Array.from(parallaxes).forEach((parallax) => {
const movement = parseFloat(parallax.dataset.movement),
direction = parallax.dataset.direction,
reverse = parallax.dataset.reverse === 'true',
element = parallax.children[0];
var containerReact = parallax.getBoundingClientRect();
var scrolledInContainer = 0 - (containerReact.top - windowInnerHeight);
var scrollArea = windowInnerHeight + containerReact.height;
var progress = scrolledInContainer / scrollArea;
var scrolledInContainer = window.pageYOffset - (containerReact.top - windowInnerHeight);
if(progress > 0 && progress < 1) {
requestAnimationFrame(() => {
var position = reverse
? movement * progress
: movement - movement * progress;
element.style[ direction == 'horizontal' ? 'left' : 'top'] = "-" + position + "%";
});
}
});
}
// Initialize
handleScroll();
// Hook into event
window.addEventListener('scroll', throttle(handleScroll, 10) );
更新 1:
我试图将脚本切换为使用 translate3d 而不是 top/left 属性。桌面 (Chrome) 上的动画现在非常流畅。究竟应该如何。在 Safari 和所有移动浏览器中,它并没有真正帮助...
https://codepen.io/flexplosion/pen/BaZrKYv
在 Safari 中(在移动 Chrome 和 Safari 中相同):https://jmp.sh/n4JG8J5
在 Chrome 中:https://jmp.sh/EMM7y1Z
对,所以,
您的视差实现非常基础,绝对应该能够每 10 毫秒 运行,即使在您能找到的最糟糕的移动设备上也是如此。为什么它与一些缺陷无关,其中两个缺陷很严重。
1。你的油门功能没有做任何事情
当您必须每一帧都重新计算内容时,动画的成本可能会很高。自然地,您希望通过将动画限制为每隔这么多滴答来帮助处理器。然而,在你的 throttle 函数中,你只是设置了一个 wait
变量并且没有对它做任何事情。节流函数最终会损害实现 — 对于 every 滚动事件(很多,每帧可能超过 1 个,请看图),您触发了重新计算,然后创建了一个无任何目的地更改 wait
变量的超时。
要使节流函数起作用,您必须跟踪其调用的所有 已知的状态。此更改需要对我在这里提到的所有内容进行最彻底的重新思考,如下所示。
2。 事件监听器应该是passive
编辑:
请参阅此答案下方的第一条评论。我记得通过添加此选项来优化滚动行为,但这些优化是通过将标志添加到 other eventListeners 来完成的,这可能会阻止浏览器继续处理其余事件。
原文:
eventListeners 可以接受一个 options
参数。其中,它可以包含一个 passive
标志,如果设置为 true
,则告诉浏览器您不会在回调中触发 event.preventDefault()
。这有助于浏览器,因为它不必 运行 整个函数来确定它是否应该继续处理事件。我不太清楚这种优化的来源,但我想这与事件传播的方式有关。
3。您在每次迭代时重新查询视差元素
我认为这是一个不需要太多解释的简单优化。您在脚本的开头查询视差元素,并且不需要在每次重新计算视差状态时都重新查询它们。这种优化实际上是从代码中删除 1 行的问题。
这是包含所有更改的 a Fiddle,这是 scrollHandler
部分,因为我不能 post 不显示代码:
const scrollHandler = (() => {
const ret = { active: false }
let timeout
ret.activate = function activate() {
if (ret.active) clearTimeout(timeout)
else {
ret.active = true
requestAnimationFrame(runParallax)
}
timeout = setTimeout(() => ret.active = false, 100)
}
return ret
})()
节流功能是其返回对象的一部分。它包含一个标志 active
和一个方法 activate
。如果您激活它,超时将在 20 毫秒后停用它。这意味着
- 一旦滚动
scrollHandler.activate()
,动画就会 运行。
- 然后运行每帧执行一次
if (scrollHandler.active) requestAnimationFrame(runParallax)
- 然后在用户停止滚动 20 毫秒后停止。
我还更改了一些小东西,比如添加了模板文字并进行了一些解构。您可能会发现一个有趣的变化是,您 不需要 在能够 forEach
之前将 HTMLCollection
转换为数组。只需将集合作为上下文直接调用 Array
的 forEach
。只要你给它传递一个可迭代的,它就会工作。
以下是其他一些不太重要的注意事项:
- 我添加了一个伪元素来指示
.visual
父元素的位置以及内部元素的移动方式。这是,您可以清楚地看到视差元素从一个边缘精确地移动到另一个边缘。
- 您还检查了
> 0
和 < 1
,但这些可能应该是 >= 0
和 <= 1
甚至更好 > -0.1
和 < 1.1
,以便在您进入屏幕之前将它们移动到正确的位置。这可以防止元素突然出现 flicker/movement。
-
.call()
部分是不必要的。如果你不打算给你的函数一个新的上下文,你可以只使用 ()
.
最后,如前所述,direction
和 reverse
属性现在已替换为映射到 X 和 Y 方向两个值的单个 angle
属性。这样,translate3d
就可以这样格式化:`translate3d(${position * multipliers[0]}px, ${position * multipliers[1]}px, 0)`
。这可能看起来很冗长,但简单地总是 运行 进行计算,结果有可能始终无用(0
,即)通常比执行单个 if
语句更快。
抱歉回答太长了
我目前正在尝试开发一个小型轻量级 vanilla-js parallax-library,它可以在没有任何背景位置的情况下在两个方向上水平和垂直地放置视差元素(图像、视频、滑块...) .
到目前为止,它运行良好,并且在期待一个重要细节方面做得很好:有时它仍然很慢。我已经在 Mac OS 和 iPhone、Safari 和 Chrome 浏览器中对其进行了测试。都一样。
我已经尝试使用节流、requestAnimationFrame 和 CSS 的 will-change 等常见做法,但没有成功。
我不确定,是否可以在 this video 中看到这里的滞后。
但我做了一个例子,你可以测试它: https://codepen.io/flexplosion/pen/RwgQXxo?editors=1111
有人有想法,我怎样才能提高视差的滚动性能?
var parallaxes = document.getElementsByClassName('parallax');
const windowInnerHeight = window.innerHeight;
function throttle (callback, limit) {
var wait = false; // Initially, we're not waiting
return function () { // We return a throttled function
if (!wait) { // If we're not waiting
callback.call(); // Execute users function
wait = true; // Prevent future invocations
setTimeout(function () { // After a period of time
wait = false; // And allow future invocations
}, limit);
}
}
}
Array.from(parallaxes).forEach((parallax) => {
const movement = parseFloat(parallax.dataset.movement),
direction = parallax.dataset.direction,
element = parallax.children[0];
if( direction == 'horizontal' ) {
// Prepare for orzintal
element.style.height = '100%';
element.style.width = (100 + movement) + "%";
} else {
// Otherwise prepare for vertical
element.style.height = (100 + movement) + "%";
element.style.width = '100%';
}
});
const handleScroll = () => {
var parallaxes = document.getElementsByClassName('parallax');
Array.from(parallaxes).forEach((parallax) => {
const movement = parseFloat(parallax.dataset.movement),
direction = parallax.dataset.direction,
reverse = parallax.dataset.reverse === 'true',
element = parallax.children[0];
var containerReact = parallax.getBoundingClientRect();
var scrolledInContainer = 0 - (containerReact.top - windowInnerHeight);
var scrollArea = windowInnerHeight + containerReact.height;
var progress = scrolledInContainer / scrollArea;
var scrolledInContainer = window.pageYOffset - (containerReact.top - windowInnerHeight);
if(progress > 0 && progress < 1) {
requestAnimationFrame(() => {
var position = reverse
? movement * progress
: movement - movement * progress;
element.style[ direction == 'horizontal' ? 'left' : 'top'] = "-" + position + "%";
});
}
});
}
// Initialize
handleScroll();
// Hook into event
window.addEventListener('scroll', throttle(handleScroll, 10) );
更新 1:
我试图将脚本切换为使用 translate3d 而不是 top/left 属性。桌面 (Chrome) 上的动画现在非常流畅。究竟应该如何。在 Safari 和所有移动浏览器中,它并没有真正帮助...
https://codepen.io/flexplosion/pen/BaZrKYv
在 Safari 中(在移动 Chrome 和 Safari 中相同):https://jmp.sh/n4JG8J5 在 Chrome 中:https://jmp.sh/EMM7y1Z
对,所以,
您的视差实现非常基础,绝对应该能够每 10 毫秒 运行,即使在您能找到的最糟糕的移动设备上也是如此。为什么它与一些缺陷无关,其中两个缺陷很严重。
1。你的油门功能没有做任何事情
当您必须每一帧都重新计算内容时,动画的成本可能会很高。自然地,您希望通过将动画限制为每隔这么多滴答来帮助处理器。然而,在你的 throttle 函数中,你只是设置了一个 wait
变量并且没有对它做任何事情。节流函数最终会损害实现 — 对于 every 滚动事件(很多,每帧可能超过 1 个,请看图),您触发了重新计算,然后创建了一个无任何目的地更改 wait
变量的超时。
要使节流函数起作用,您必须跟踪其调用的所有 已知的状态。此更改需要对我在这里提到的所有内容进行最彻底的重新思考,如下所示。
2。 事件监听器应该是passive
passive
编辑:
请参阅此答案下方的第一条评论。我记得通过添加此选项来优化滚动行为,但这些优化是通过将标志添加到 other eventListeners 来完成的,这可能会阻止浏览器继续处理其余事件。
原文:
eventListeners 可以接受一个 options
参数。其中,它可以包含一个 passive
标志,如果设置为 true
,则告诉浏览器您不会在回调中触发 event.preventDefault()
。这有助于浏览器,因为它不必 运行 整个函数来确定它是否应该继续处理事件。我不太清楚这种优化的来源,但我想这与事件传播的方式有关。
3。您在每次迭代时重新查询视差元素
我认为这是一个不需要太多解释的简单优化。您在脚本的开头查询视差元素,并且不需要在每次重新计算视差状态时都重新查询它们。这种优化实际上是从代码中删除 1 行的问题。
这是包含所有更改的 a Fiddle,这是 scrollHandler
部分,因为我不能 post 不显示代码:
const scrollHandler = (() => {
const ret = { active: false }
let timeout
ret.activate = function activate() {
if (ret.active) clearTimeout(timeout)
else {
ret.active = true
requestAnimationFrame(runParallax)
}
timeout = setTimeout(() => ret.active = false, 100)
}
return ret
})()
节流功能是其返回对象的一部分。它包含一个标志 active
和一个方法 activate
。如果您激活它,超时将在 20 毫秒后停用它。这意味着
- 一旦滚动
scrollHandler.activate()
,动画就会 运行。 - 然后运行每帧执行一次
if (scrollHandler.active) requestAnimationFrame(runParallax)
- 然后在用户停止滚动 20 毫秒后停止。
我还更改了一些小东西,比如添加了模板文字并进行了一些解构。您可能会发现一个有趣的变化是,您 不需要 在能够 forEach
之前将 HTMLCollection
转换为数组。只需将集合作为上下文直接调用 Array
的 forEach
。只要你给它传递一个可迭代的,它就会工作。
以下是其他一些不太重要的注意事项:
- 我添加了一个伪元素来指示
.visual
父元素的位置以及内部元素的移动方式。这是,您可以清楚地看到视差元素从一个边缘精确地移动到另一个边缘。 - 您还检查了
> 0
和< 1
,但这些可能应该是>= 0
和<= 1
甚至更好> -0.1
和< 1.1
,以便在您进入屏幕之前将它们移动到正确的位置。这可以防止元素突然出现 flicker/movement。 -
.call()
部分是不必要的。如果你不打算给你的函数一个新的上下文,你可以只使用()
.
最后,如前所述,direction
和 reverse
属性现在已替换为映射到 X 和 Y 方向两个值的单个 angle
属性。这样,translate3d
就可以这样格式化:`translate3d(${position * multipliers[0]}px, ${position * multipliers[1]}px, 0)`
。这可能看起来很冗长,但简单地总是 运行 进行计算,结果有可能始终无用(0
,即)通常比执行单个 if
语句更快。
抱歉回答太长了