视差滚动效果实现与前端性能优化实战经验
先看效果,再看代码
上周给一个 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 视差),后续会继续分享这类博客。有更优的实现方式欢迎评论区交流,尤其是移动端的优化方案,我还在摸索中。

暂无评论