vh塌陷:一个被低估的移动端布局“幽灵问题”与真正落地的工程解法
你有没有遇到过这样的场景?
- 页面刚打开时,一个
.hero-section { height: 100vh; }的全屏横幅完美撑满屏幕; - 你轻轻向上滑动——地址栏收起,视口“变高”了;
- 但那个横幅纹丝不动,底部却突然多出一片刺眼的白空;
- 更糟的是,下面的按钮被地址栏盖住,用户点不到、看不见、甚至以为页面卡死了。
这不是 bug,也不是你 CSS 写错了。这是现代移动浏览器里一个静默运行多年、却极少被正确认知的底层机制——vh单位在视觉视口变化时不会重算。它不是失效,而是「太守旧」:它只认页面加载那一刻的window.innerHeight,之后无论用户怎么滚动、缩放、横竖屏切换,它都固执地站在原地。
这个现象被笼统叫作“vh塌陷”,但它背后没有魔法,只有浏览器对「布局稳定性」的权衡取舍。而真正的工程难点从来不是“为什么塌”,而是:如何让 UI 在所有真实用户操作下,始终严丝合缝地贴合他们此刻真正看到的那块屏幕?
它为什么塌?先看清浏览器到底在想什么
别急着写 JS 或查 polyfill。我们得先掀开浏览器的盖子,看看vh是怎么被“冻住”的。
vh不是“当前高度”,而是“快照高度”
1vh = 初始布局视口高度的 1%—— 这句话里的关键词是初始和布局视口。
- 初始:指
document完成解析、CSSOM 构建完毕、首次 layout 触发前的那个瞬间。此时window.innerHeight被读取并固化为vh的计算基准。 - 布局视口(Layout Viewport):这是 CSS 排版所依赖的抽象画布。它的尺寸由
<meta>定义(如width=device-width),通常固定不变。vh、vw、vmin、vmax全部绑定于此。
而用户滚动时变化的,是另一个东西:
- 视觉视口(Visual Viewport):这才是你手指划过屏幕时,眼睛真正聚焦的那一小块区域。它会随着地址栏显隐、双指缩放、页面滚动实时伸缩。它的高度可通过
window.visualViewport.height实时读取。
✅ 简单记:
vh→ 绑定「画布大小」;visualViewport.height→ 反映「镜头位置」。
❌ 当镜头拉远(地址栏收起),画布没变,但你期望内容跟着镜头走——这就产生了错位。
真实数据,比理论更有力(iPhone 14 Pro Max 实测)
| 行为 | window.innerHeight | visualViewport.height | 100vh实际像素值 | 用户 |
|---|