手机上把网页小游戏跑顺,只需四件事:viewport 写对、dvh替代vh、touch-action关掉误触手势、用deviceMemory+Save-Data做低配与省流降级。按本文 5 段配置落地,一款 Canvas 小游戏在 iPhone 与安卓机上的首屏可交互时间可稳定压到 2.5 秒以内。
一、viewport 是所有移动端问题的源头
「手机打开网页游戏比例不对、上方被地址栏切掉」,几乎都出在这一行:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
width=device-width 让 1 个 CSS 像素等于 1 个设备像素;initial-scale=1 锁定初始缩放比;viewport-fit=cover 让页面延伸到刘海/圆角区域,配合下一节的 env(safe-area-inset-*) 才能真正贴边。
同一条 meta 还有两个易漏的关键字:interactive-widget 控制软键盘弹出时的视口行为(resizes-visual 默认、resizes-content、overlays-content),做输入框时用 resizes-content 可避免键盘顶掉游戏画面;user-scalable 在 iOS 10 起被浏览器默认忽略,MDN 警告禁用缩放会挡住低视力用户阅读,WCAG 要求至少支持 2 倍缩放、最佳实践 5 倍——做小游戏请不要写 user-scalable=no。Apple 的 Safari HTML Reference 也确认:只写 initial-scale=1.0 时,Safari 竖屏假设宽度为 device-width、横屏假设为 device-height,所以两者建议成对出现。
二、100vh 被地址栏截断:改用 dvh 与安全区
vh 的历史问题是移动端地址栏会动态收缩,而 100vh 按「最大视口高度」计算,页面底部被盖住一截。CSS 长度规范给出的解法是三组视口单位 svh/lvh/dvh,且 vh 现在等价于 lvh。做游戏画布直接用 dvh,地址栏收起时自动铺满,展开时自动回缩:
#stage { width: 100%; height: 100dvh; background: #111; }
.hud {
padding-top: env(safe-area-inset-top, 0px);
padding-bottom: env(safe-area-inset-bottom, 0px);
}
env() 的四个动态变量 safe-area-inset-top/right/bottom/left 在矩形屏幕上全为 0,只有刘海、圆角或动态键盘占位时才大于 0;第二个参数是兜底值。
三、横屏:全屏 API + orientation 媒体查询
Element.requestFullscreen() 返回 Promise:授权成功则 resolve 并触发 fullscreenchange,被拒则 reject 并触发 fullscreenerror。调用有两个硬前提——元素必须是标准 HTML 元素或 <svg>/<math>(不能是 <dialog>),且必须位于顶层文档或带 allowfullscreen 的 iframe 中;同时 Permissions-Policy 须允许 fullscreen 特性,其默认 allowlist 为 self。
btn.addEventListener('click', async () => {
if (document.fullscreenElement) await document.exitFullscreen();
else await document.getElementById('stage').requestFullscreen();
});
document.addEventListener('fullscreenchange', () => {
layout(document.fullscreenElement ? 'landscape' : 'portrait');
});
两个坑:全屏必须由用户手势触发,否则被拒;部分移动浏览器在全屏下会忽略 viewport 设置并屏蔽用户缩放。布局切换用媒体查询更可靠:@media (orientation: landscape) 自 2015 年 7 月起全浏览器可用。但它有真实陷阱——竖屏弹出软键盘会让视口变宽,浏览器于是套用 landscape 样式(MDN 原文即以「竖屏打开软键盘使视口比它更高」描述)。判断方向别只看 orientation,要结合 window.innerWidth > window.innerHeight 与 visualViewport 的 offsetTop/offsetLeft。
Safari 16.4(2023 年 3 月 27 日发布)取消 Fullscreen API 前缀,:fullscreen 伪类同时去前缀;Screen Orientation API 的 lock()/unlock() 至今仍是实验特性,需在 iOS 设置 → Safari → 高级 → 实验功能手动开启。
四、触屏:用 Pointer Events,别用 touch 事件
PointerEvent 继承自 MouseEvent,一套代码覆盖鼠标、触摸、触控笔:pointerId 标识哪根手指,width/height 是接触面尺寸(CSS 像素),pressure 是 0 到 1 的归一化压力,setPointerCapture() 可把后续事件强制重定向到指定元素,手指滑出画布也不丢事件。
滑动类游戏(《2048》《贪吃蛇》)正确做法:记录 pointerdown 起点,在 pointermove 中判断位移,超过 24–30 CSS 像素阈值即判定一次滑动并锁定方向,忽略同一次手势内的后续方向变更。同时用 CSS 关掉浏览器手势接管:
#stage { touch-action: none; user-select: none; }
touch-action: none 禁用浏览器处理所有平移与缩放手势;manipulation 允许平移与双指缩放但禁双击缩放,能省掉浏览器等待 click 的延迟。
五、性能、音频与省流:低配机也能跑
按内存降级。 navigator.deviceMemory 返回近似内存(GiB),W3C Device Memory 规范要求该值向上取整到 2 的幂并施加实现自定义上下界(Chrome 实现区间 0.25–8 GiB),因此你拿到的是 0.25/0.5/1/2/4/8 这样的粗档位而非精确值——这是为降低指纹识别风险刻意做的。
const mem = navigator.deviceMemory ?? 4; // 缺失时按中端机处理
const lite = mem <= 2; // 低档:关粒子、降分辨率、关阴影
按网络省流。 navigator.connection.effectiveType 给出 slow-2g/2g/3g/4g 并可监听 change;更直接的是 HTTP Save-Data 请求头,用户显式开启省流时值为 On,服务端可据此下发更小图片与更轻样式、关闭轮询。但 Save-Data 并非全浏览器支持、属实验特性,须自行降级。
按可见性省电。 Page Visibility API 的 visibilitychange 在窗口最小化、切标签页或被完全遮挡时触发,此时暂停游戏主循环,切回再恢复——直接解决「后台回来发现已经死了」。
防熄屏与解锁音频。 Screen Wake Lock API 自 2025 年 3 月起成为 Baseline 新特性,仅在安全上下文(HTTPS)可用。音频方面,Chrome 自动播放策略是:静音自动播放始终允许;带声音仅在用户已与该域名交互过、桌面端 Media Engagement Index 达标、或站点已添加到主屏/装成 PWA 时允许。MEI 计算要求媒体消费超过 7 秒、音频存在且未静音、标签页激活、媒体尺寸大于 200×140 像素。策略不满足时无手势调用 play() 会以 NotAllowedError 拒绝 Promise——所以「开始游戏」按钮第一件事就是 audioContext.resume()(被策略挂起时状态为 suspended)。
Canvas 要省。 MDN 优化指南第一条即离屏预渲染:把每帧重复的绘制画到 offscreen canvas,主画布每帧只做一次 drawImage,配合 requestAnimationFrame 单循环驱动。
用指标验收。 Core Web Vitals 三条阈值:LCP 不超过 2.5 秒、INP 不超过 200 毫秒、CLS 不超过 0.1,并在第 75 百分位评估。移动端小游戏最容易踩 INP——把重逻辑挪出主线程、输入事件里只更新状态是最直接的改善。