根本原因在于html根字号未对齐设备真实渲染逻辑,受系统字体缩放、DPR、viewport缩放及横竖屏切换多重干扰;window.innerWidth返回视口宽度(含滚动条),非设备逻辑像素宽,也不反映系统字体放大后的实际基准。

根本原因不是 rem 本身不可靠,而是你设置的 html 根字号没对齐设备真实的渲染逻辑 —— 它被系统字体缩放、DPR、viewport 缩放、横竖屏切换同时干扰,而多数人只用 window.innerWidth 算一次就完事。
为什么 window.innerWidth 算出来的 rem 总是不准
这个值返回的是当前视口宽度(含滚动条),但它既不是设备逻辑像素宽,也不反映系统字体放大后的实际渲染基准。在 iPhone 12 上它可能接近 375,但在开启「更大字体」的华为 Mate50 上,getComputedStyle(document.documentElement).fontSize 可能是 24px,而你 JS 算出的却是 20px,差的这 4px 就会让按钮高度偏差 20%,文字行高错位。
更麻烦的是:安卓 WebView 常在首帧按系统默认字号渲染,JS 后设会闪一下;iOS Safari 对 resize 事件响应滞后,横屏时来不及重算。
- 优先用
window.screen.width / window.devicePixelRatio得到 CSS 像素宽(即设计稿所指的“设备逻辑宽度”) - 必须监听
resize和orientationchange两个事件,后者专治横竖屏突变 - 首次执行不能等 DOMContentLoaded,要在
document.documentElement可访问后立刻设初值(比如内联 script) - 加节流(如 100ms 内最多执行一次),避免双指缩放时疯狂触发
系统字体缩放如何悄悄破坏 rem 布局
你写了 html { font-size: 62.5% },以为 1rem = 10px,但 iOS 把系统字体设为「最大动态类型」后,浏览器直接把根字号渲染成 28px —— 那 62.5% 就成了 17.5px,不是你预期的 10px。这不是 bug,是浏览器在尊重可访问性。
立即学习“前端免费学习笔记(深入)”;
更隐蔽的问题是:媒体查询里写 @media (min-width: 37.5rem),断点计算发生在根字号最终确定前,此时 rem 还按默认 16px 算,结果断点永远不生效。
- 开头必须写
html { font-size: 100% },不覆盖系统默认值 - JS 初始化时要二次校准:先设一个初值(如 100px),再用
getComputedStyle读真实值,按比例反推修正 - 所有
line-height改用无单位值(如1.4),它天然随字号缩放;别用1.4rem,那是二次放大 -
border-width别用 rem(如0.01rem),小屏下可能缩到 0.1px 不可见,坚持用1px或0.5px
第三方组件和盒模型怎么让 rem “突然断裂”
你用 rem 设置了按钮高度 2.75rem,但 Vant 的 van-button 内部写死 height: 44px、padding: 0 16px,又没重置 box-sizing,结果按钮总高变成 44px + 两边 padding,和你的 rem 容器完全不对齐。更糟的是,textarea 没设固定 font-size,系统放大后行高拉伸,但容器高度仍按 rem 计算,内容就被裁掉。
- 全局加
* { box-sizing: border-box },包括::before和::after - 第三方库组件需单独重置:
.van-button, .nut-button { box-sizing: border-box; font-size: inherit; } - 所有固定像素语义的尺寸(图标、分割线、1px 边框)放弃 rem,回归
px或vw - 测试不能只看「较大」字体档位,必须开到 iOS 「最大动态类型」或安卓「超大字体」,观察
document.documentElement实际计算值
真正难的不是写几行 JS 算根字号,而是接受 rem 从来不是“锁死尺寸”的工具 —— 它依赖浏览器渲染链每个环节都稳定输出,而移动端恰恰在 viewport、DPR、系统字体、横竖屏、WebView 实现上处处有坑。稳住 rem 的唯一办法,是让它可测量、可校准、可降级,而不是试图用一行 calc() 一劳永逸。


















