在手机端(Edge/Chrome 真机)测试时,我遇到一个很诡异的现象:
- 向下滚动进入博客展示阶段时一切正常
- 但当我从 goals 区域向上回滚时,博客阶段的“蓝色背景”会突然出现一下(像是进度跳了一帧)
- 更奇怪的是:背景出现时,雪花/粒子层并没有同步出现,看起来像渲染顺序被打乱了
- 同样的操作在桌面浏览器的设备模拟器里基本复现不了
这类“真机才有、桌面模拟没有”的 ScrollTrigger 问题,通常都离不开一个关键词:移动端动态视口(dynamic viewport)。
根因:手机浏览器地址栏伸缩会触发“只有高度变化的 resize”
移动端浏览器的 URL/地址栏(以及底部工具栏)在滚动时会自动收起/展开。这会带来两个后果:
window.innerHeight会变化(可视高度变大/变小)- 浏览器会触发
resize事件(但往往只有 height 变了,width 没变)
而桌面 DevTools 的“设备模拟”多数情况下并不会真实模拟地址栏动态伸缩,因此你会看到:真机有 bug,桌面模拟没有。
为什么 height-only resize 会“破坏滚动动画”
问题不在于浏览器触发 resize 本身,而在于我们对 resize 的处理方式。
在 TextScrollSection 中,我们为了在窗口变化时重新计算布局并重建触发器,写了 resize -> calculateDynamicScale() -> ScrollTrigger.refresh() 的逻辑。
这在桌面/正常 resize(比如改变窗口宽度、旋转屏幕)时是合理的;但在移动端,当用户正在滚动手势中,地址栏来回伸缩会导致:
- 频繁触发
resize - 我们频繁调用
ScrollTrigger.refresh() pin / pinSpacing / start / end相关几何重新计算- 进度
progress发生跳变(肉眼可见的“闪回/插帧”)
当某个阶段的状态(例如雪花层 opacity)是由 ScrollTrigger 的阶段逻辑驱动时,这种进度跳变就可能造成短暂的“背景出现了但粒子没跟上”的错位。
修复:忽略移动端“仅高度变化”的 resize(height-only resize guard)
核心策略:
在移动端,如果 resize 事件只有
innerHeight变化、innerWidth不变,就不要做ScrollTrigger.refresh()。
伪代码如下(与项目内实现一致):
let lastW = window.innerWidth;
let lastH = window.innerHeight;
window.addEventListener("resize", () => {
const w = window.innerWidth;
const h = window.innerHeight;
const isMobileLike = w <= 768;
// URL 栏伸缩:通常只有高度变化
if (isMobileLike && w === lastW && h !== lastH) {
lastH = h;
return;
}
lastW = w;
lastH = h;
calculateDynamicScale();
ScrollTrigger.refresh();
});当我禁用这条 guard 时,真机 bug 可以稳定回归;反之开启后问题消失,因此可以确认:根因就是 height-only resize 触发的 refresh。
额外建议:ScrollTrigger 自带的移动端配置
除了避免“我们自己主动 refresh”,ScrollTrigger 自己也会在 resize 时做 refresh。GSAP 提供了:
ScrollTrigger.config({ ignoreMobileResize: true });它会尽量忽略移动端地址栏伸缩带来的 resize,从而减少内部 refresh 的频率。
经验上:
ignoreMobileResize更适合作为全局默认(例如放在 GSAP 初始化的 Provider 里)- height-only guard 更像是你在“自己写了 resize 逻辑”时的补丁
About 页面也需要吗?
结论(以当前项目为准):
- About 页确实使用了 ScrollTrigger(pin + scrub 的动画)
- 但 About 页没有额外绑定
resize -> ScrollTrigger.refresh()的逻辑 - 同时它的关键 pin 段使用了
end: () => "+=" + window.innerHeight * 4并开启了invalidateOnRefresh
因此:
- About 页不太需要“height-only resize guard”(因为它没有手动 refresh)
- 但建议保留/启用
ignoreMobileResize这种全局策略,降低移动端地址栏伸缩导致的抖动风险
使用场景总结:什么时候该用这套防护
适合启用 height-only resize guard:
- 使用 ScrollTrigger 的
pin/scrub做长滚动叙事动画(尤其有阶段状态机/React state 联动) - 你在
resize里调用ScrollTrigger.refresh()或重算 start/end 距离 - 你发现“真机闪、桌面模拟不闪”,并且问题与地址栏伸缩强相关
不建议直接忽略 height-only resize:
- 页面高度变化本身就需要立刻重新布局(例如:软键盘弹出导致布局必须即时适配)
- 以表单/输入为主的页面,宁可接受一点动画偏差也要保证布局即时适配