视差滚动效果实现与前端性能优化实战经验

Designer°子瑄 交互 阅读 2,282
赞 27 收藏
二维码
手机扫码查看
反馈

先看效果,再看代码

上周给一个 landing page 加了个视差滚动效果,客户看了直呼“高级”。其实核心逻辑就几行,但中间踩了几个坑,折腾了半天。今天直接上干货,把亲测有效的方案和容易翻车的地方都列出来。

视差滚动效果实现与前端性能优化实战经验

最简单的视差滚动,就是让背景图比内容慢一点动。比如页面往下滚 100px,背景只动 50px。这种效果用 CSS 就能搞定,根本不用 JS:

.parallax-section {
  background-image: url('bg.jpg');
  background-attachment: fixed;
  background-size: cover;
  height: 100vh;
}

但!别高兴太早。background-attachment: fixed 在移动端 Safari 上基本失效,而且性能很差,尤其在低端安卓机上会卡成 PPT。我一开始偷懒用了这个,结果 QA 一测就打回来了。所以,生产环境建议直接用 JS 方案,可控性高得多。

核心代码就这几行

下面是我现在主力用的纯 JS 实现,不依赖任何库,兼容性好,性能也稳:

<div class="parallax-container">
  <div class="parallax-layer" data-speed="0.5">
    <img src="bg.jpg" alt="">
  </div>
  <div class="content">
    <h1>这里是正文</h1>
  </div>
</div>
.parallax-container {
  position: relative;
  overflow: hidden;
  height: 100vh;
}

.parallax-layer {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 120%; /* 稍微高一点,避免滚动时露出底 */
  will-change: transform;
}

.parallax-layer img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.content {
  position: relative;
  z-index: 2;
  padding: 40px;
}
function initParallax() {
  const layers = document.querySelectorAll('.parallax-layer');
  
  window.addEventListener('scroll', () => {
    const scrollTop = window.pageYOffset;
    
    layers.forEach(layer => {
      const speed = parseFloat(layer.getAttribute('data-speed')) || 0.5;
      const yPos = -(scrollTop * speed);
      layer.style.transform = translateY(${yPos}px);
    });
  }, { passive: true });
}

// 记得在 DOM 加载完后调用
document.addEventListener('DOMContentLoaded', initParallax);

这里注意下,一定要加 passive: true,否则 Chrome 会警告你影响滚动性能。另外 will-change: transform 能让浏览器提前做优化,滚动更流畅。这些细节我一开始没加,后来 Lighthouse 评分掉到 60 多才补上。

踩坑提醒:这三点一定注意

视差滚动看着简单,但有几个坑特别容易踩,我全踩过:

  • 不要监听 scroll 事件不做节流:虽然上面代码没显式节流,但因为用了 transform 且开了 will-change,现代浏览器基本能 handle 住。但如果你要做更复杂的计算(比如根据滚动位置改变 opacity、scale),一定要用 requestAnimationFrame 包裹,否则低端机直接卡死。
  • 移动端 touchmove 会打断滚动:在 iOS 上,如果用户快速滑动,scroll 事件可能不会连续触发,导致视差层“跳帧”。解决办法是同时监听 touchmove 并手动触发更新,但实测发现反而更卡。我的妥协方案是:在移动端降低 data-speed 值(比如 0.2),让效果不那么明显,但至少流畅。
  • 别用百分比高度嵌套.parallax-container 的高度必须是固定值(比如 100vh 或具体 px),否则子元素的 transform 会计算错位。有一次我用 min-height: 100vh,结果在 Safari 里背景图位置乱飘,查了两小时才发现是 viewport 高度计算差异。

这个场景最好用

视差滚动最适合用在单页长滚动的 landing page,比如产品介绍页、活动页。但千万别在内容密集型页面(比如博客列表、后台管理)用,不仅没提升体验,反而增加滚动负担。

我最近试了个变种:让多个图层以不同速度滚动,营造景深感。比如前景文字 speed=0.1,中景插画 speed=0.3,远景背景 speed=0.7。代码几乎不用改,只要给每个 layer 加不同的 data-speed 就行:

<div class="scene">
  <div class="parallax-layer" data-speed="0.1"><img src="text.png"></div>
  <div class="parallax-layer" data-speed="0.3"><img src="illustration.png"></div>
  <div class="parallax-layer" data-speed="0.7"><img src="mountain.jpg"></div>
</div>

不过要注意图层顺序——data-speed 越小,越像“近景”(移动越慢),所以 HTML 里要按从近到远排,或者用 z-index 控制。我一开始顺序搞反了,做出个“隧道倒着走”的诡异效果,自己都没发现,直到设计师问我是不是故意的……

高级技巧:结合 Intersection Observer

有时候你只想在某个区块进入视口时才启动视差,避免全局监听 scroll 浪费性能。这时候可以结合 IntersectionObserver

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // 开始监听滚动
      window.addEventListener('scroll', parallaxHandler, { passive: true });
    } else {
      // 移出视口就停止
      window.removeEventListener('scroll', parallaxHandler);
    }
  });
}, { threshold: 0.1 });

observer.observe(document.querySelector('.parallax-section'));

但要注意,频繁添加/移除事件监听器本身也有开销。我的经验是:如果页面只有 1~2 个视差区块,直接全局监听更省事;超过 3 个才考虑用 Observer 优化。

最后说点实在的

视差滚动不是银弹,用不好反而显得花哨。我现在的原则是:效果要 subtle(微妙),速度别超过 0.7,移动端优先保证流畅度。另外,记得给 prefers-reduced-motion 用户关掉动画:

@media (prefers-reduced-motion: reduce) {
  .parallax-layer {
    transform: none !important;
  }
}

以上是我踩坑后的总结,希望对你有帮助。这个技术的拓展用法还有很多(比如结合 GSAP 做路径滚动、3D 视差),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流,尤其是移动端的优化方案,我还在摸索中。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论