1

我目前正在尝试开发一个小型轻量级 vanilla-js 视差库,它可以在两个方向上水平和垂直地设置视差元素(图像、视频、滑块等),而无需任何背景位置的东西。

到目前为止,它运行良好,并且在期待一个重要细节方面做得很好:有时它仍然滞后。我已经在 Mac OS 和 iPhone、Safari 和 Chrome 浏览器上对其进行了测试。都一样。

我尝试使用节流、requestAnimationFrame 和 CSS 的 will-change 等常见做法,但没有成功。

我不确定,是否可以在此视频中看到滞后。

但我做了一个例子,你可以测试它: 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 而不是顶部/左侧属性。桌面 (Chrome) 上的动画现在非常流畅。它应该是怎样的。在 Safari 和所有移动浏览器中,它并没有真正帮助......

https://codepen.io/flexplosion/pen/BaZrKYv

在 Safari 中(在移动 Chrome 和 Safari 上相同):https ://jmp.sh/n4JG8J5 在 Chrome 中:https ://jmp.sh/EMM7y1Z

4

1 回答 1

2

是的,所以,
你的视差实现是非常基本的,并且绝对应该能够每 10 毫秒运行一次,即使在你能找到的最糟糕的移动设备上也是如此。为什么它与一些缺陷无关,其中两个是至关重要的。

1.你的油门功能没有做任何事情

当您必须重新计算每一帧的内容时,动画可能会变得昂贵。自然地,您希望通过将动画限制在每这么多滴答声中来帮助处理器。然而,在你的节流函数中,你只是设置了一个wait变量而不用它做任何事情。油门函数最终会损害实现 - 对于每个滚动事件(很多,可能每帧超过 1 个,如图所示),您触发重新计算,然后创建一个超时来更改wait变量而没有任何目的。

要使节流阀功能起作用,您必须跟踪其所有调用都知道的状态。这种变化需要对我在这里提到的所有内容进行重新思考,如下所示。

2.事件监听器应该是passive

编辑:
请参阅此答案下方的第一条评论。我记得通过添加此选项来优化滚动行为,但这些优化是通过将标志添加到其他eventListeners 来完成的,这可能会阻止浏览器继续处理其余事件。

ORIGINAL:
eventListeners 可以接受一个options参数。除其他外,它可以包含一个passive标志,如果设置为true,则告诉浏览器您不会event.preventDefault()在回调内部触发。这有助于浏览器,因为它不必运行整个函数来确定它是否应该继续进行事件。这种优化的来源对我来说并不完全清楚,但我想这与事件的传播方式有关。

3. 每次迭代都重新查询视差元素

我认为,一个不需要太多解释的简单优化。您在脚本开始时查询视差元素,无需在每次重新计算视差状态时重新查询它们。这种优化实际上是从代码中删除 1 行。

这是一个包含所有更改的小提琴scrollHandler,这是其中的一部分,因为我无法在不显示代码的情况下发布:

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 毫秒后将其停用。这意味着

  • 只要通过 do 滚动,动画就会运行scrollHandler.activate()
  • 然后通过做每帧运行一次if (scrollHandler.active) requestAnimationFrame(runParallax)
  • 然后在用户停止滚动 20 毫秒后停止。

我还更改了一些小东西,例如添加了模板文字并进行了一些解构。您可能会发现有趣的一个变化是,您无需HTMLCollections 转换为数组即可forEach。只需直接调用Array'sforEach并将集合作为其上下文。只要你传递一个可迭代的,它就会工作。

以下是其他一些不太重要的注意事项:

  • 我添加了一个伪元素来指示.visual父元素的位置以及内部元素如何移动。这是,您可以清楚地看到视差元素精确地从一个边缘移动到另一个边缘。
  • 您还检查了> 0and < 1,但这些应该是>= 0and<= 1甚至更好的> -0.1and < 1.1,以便在您进入屏幕之前将它们移动到正确的位置。这可以防止元素进入视野时突然闪烁/移动。
  • 说的那部分.call()是不必要的。如果你不打算给你的函数一个新的上下文,你可以使用().

最后,如前所述,directionreverse属性现在被替换为单个angle属性,该属性映射到 X 和 Y 方向的两个值。这样,translate3d可以像这样格式化:`translate3d(${position * multipliers[0]}px, ${position * multipliers[1]}px, 0)`. 它可能看起来很冗长,但总是运行一个计算,结果有可能总是无用(0即)通常比执行单个if语句更快。

抱歉,答案太长了

于 2021-09-23T00:26:45.707 回答