移动端 URL 栏伸缩导致 ScrollTrigger refresh:为什么桌面模拟复现不了,以及 height-only resize 防护

真机回滚时出现“蓝色背景突然闪现、雪花层缺失”的根因:地址栏伸缩触发 height-only resize,我们在 resize 中主动 ScrollTrigger.refresh() 导致进度跳变。本文记录定位过程与通用防护场景,并说明 About 页是否需要同类处理。

January 23, 2026 (1mo ago)
2 min read
961 views
199 likes
Category
Beginner
#GSAP#ScrollTrigger#Mobile#Viewport#Resize#Debugging

在手机端(Edge/Chrome 真机)测试时,我遇到一个很诡异的现象:

  • 向下滚动进入博客展示阶段时一切正常
  • 但当我从 goals 区域向上回滚时,博客阶段的“蓝色背景”会突然出现一下(像是进度跳了一帧)
  • 更奇怪的是:背景出现时,雪花/粒子层并没有同步出现,看起来像渲染顺序被打乱了
  • 同样的操作在桌面浏览器的设备模拟器里基本复现不了

这类“真机才有、桌面模拟没有”的 ScrollTrigger 问题,通常都离不开一个关键词:移动端动态视口(dynamic viewport)

根因:手机浏览器地址栏伸缩会触发“只有高度变化的 resize”

移动端浏览器的 URL/地址栏(以及底部工具栏)在滚动时会自动收起/展开。这会带来两个后果:

  1. window.innerHeight 会变化(可视高度变大/变小)
  2. 浏览器会触发 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:

  • 页面高度变化本身就需要立刻重新布局(例如:软键盘弹出导致布局必须即时适配)
  • 以表单/输入为主的页面,宁可接受一点动画偏差也要保证布局即时适配
CleanLove

Written by CleanLove

Full-stack developer passionate about modern web technologies