跳到正文

SVG 图标为什么在真机上被挤扁

有一类 UI bug 很迷惑:PC 正常,Chrome 模拟移动端也正常,到了真机上图标就扁了。

这次具体表现是:个人页左上角返回按钮在手机上像被横向压扁;基本信息页的行尾箭头也有类似问题;后来又发现「充值」入口图标被拉伸,其他图标却正常。第一眼看,很容易怀疑是响应式、rem、字体缩放或移动端 viewport 造成的。

但排查到最后,真正的问题在 SVG 资源本身:外层 DOM 尺寸是对的,内部 SVG 被按错误的比例拉伸了。

先证明不是外层尺寸问题

遇到图标变扁,先用 DevTools 核对外壳尺寸和资源映射:

  1. 外层元素的 computed width / height。
  2. 图片或 SVG 元素自己的 DOMRect。
  3. SVG 文件里的 viewBox 和 preserveAspectRatio。

如果外层按钮是 40 x 40,img 也是 40 x 40,但里面的图形明显被压扁,可以先排除外壳被 flex 或 rem 改成非正方形。接着还要检查资源内容、嵌套 transform、裁剪和视口映射;外壳尺寸正确,本身并不能证明一定是 preserveAspectRatio。

如果不先区分外壳与内部图形,就容易把 size-10 换成 inline style、把 rem 换 px、给按钮加 min-width,甚至重写一套移动端样式。外壳越来越复杂,内部图形的映射问题却可能仍然存在。

preserveAspectRatio 到底影响什么

SVG 有一个 viewBox,表示内部坐标系;外层 CSS 又会给它一个实际显示尺寸。浏览器需要决定:怎么把 viewBox 里的内容映射到这个 CSS 盒子里?

默认情况下,SVG 的 preserveAspectRatio 相当于 xMidYMid meet:保持比例,居中放进盒子。如果盒子比例和 viewBox 比例不同,就留空边,不强行拉伸。

而 preserveAspectRatio="none" 会把 viewBox 非等比映射到当前视口。当两者宽高比不同,图形才会因此变胖、变瘦或变扁;两者比例相同,即使图形没有铺满 viewBox,也不会仅因 none 变形。图形边界影响留白,不能和坐标映射比例混为一谈。

所以不能简单说「没写 preserveAspectRatio 就一定错」。SVG 没写时,默认通常是等比的。真正危险的是:

  • 资源里显式写了 preserveAspectRatio="none"。
  • 资源的 viewBox 和真实图形边界比例不一致,但组件又用错误的外壳比例渲染。
  • Figma 导出了内部某一层图形,组件再把它当完整 frame 拉满。

正方形 viewBox 放进 200×100 视口:meet 保持比例,none 非等比拉伸

Figma 的 frame 不等于真实图形

Figma 里一个图标通常不是单个路径。它常常是:

  • 外层 24 x 24 或 40 x 40 的交互 frame。
  • 内部一个更小的 group、vector 或 image。
  • 透明内边距、旋转、偏移、mask、clip。

设计稿上你看到一个 24 x 24 的节点,不代表里面的图形应该铺满 24px。比如一个箭头可能实际只有 8 x 14,居中放在 24 x 24 frame 里;一个编辑图标可能外层是 24px,内部真实图形只有 17px 左右。

如果导出时只拿了内部 vector,再在代码里写:

.icon {
  width: 24px;
  height: 24px;
  object-fit: contain;
}

它不一定能还原设计稿。因为设计稿需要的是「24px frame 里放一个带偏移的小图形」,而你保存的资源可能已经丢了 frame。

更稳的方式是:资源文件保留完整 frame,组件只负责摆放 frame。

SVG 自己应该长这样:

<svg
  width="24"
  height="24"
  viewBox="0 0 24 24"
  preserveAspectRatio="xMidYMid meet"
  xmlns="http://www.w3.org/2000/svg"
>
  <!-- 内部图形按 Figma 的真实位置放在 24x24 frame 里 -->
</svg>

组件侧只写:

<img class="menu-item__icon" src="./assets/ic-recharge.svg" alt="" />
.menu-item__icon {
  @apply size-6 shrink-0 object-contain;
}

不要在 Vue 里维护一张 ICON_STYLE 表,给每个图标手写不同的 width、height、left、top。那些偏移关系本来应该属于 SVG 资源,而不是业务组件。

Figma 的交互 frame、透明内边距和内部真实图形最好保留在 SVG 资源里,组件只控制外壳

为什么 PC 模拟移动端没暴露,真机暴露了

真机和模拟环境可能使用不同的布局状态、资源版本或 WebView 内核,需要分别取证。这次确认了资源映射问题,但没有逐项隔离「为什么只有真机明显」的所有变量;不能仅凭设备像素比或图片解码策略,就解释等比映射为何改变。先对照两边实际加载的 SVG、视口尺寸和 transform,再判断差异来自哪一层。

定位时,用外壳尺寸区分检查方向:

  • 如果 DOMRect 对,图形扁,查 SVG。
  • 如果 DOMRect 不对,查 CSS。
  • 如果真机和模拟器 DOMRect 不一样,查响应式、flex shrink、viewport、safe area。

这次的证据链属于第一类:外壳尺寸正确,内部图形被拉伸。移动端布局没有把按钮挤坏,真正需要修的是 SVG preserve / viewBox / frame 处理。

不要把 SVG 变形误判成 rem 问题

有些项目会用 postcss-pxtorem,CSS 里的 px 会统一转成 rem。这确实会带来尺寸策略问题:哪些尺寸要跟随字号,哪些尺寸应该固定,哪些应该靠响应式布局。

但这次图标变扁不能直接归因到 rem。computed size 和 DOMRect 都是正确的正方形,能排除外壳被横向压缩,却还不能判断内部 path、嵌套 transform、裁剪或图片适配方式是否有问题。本次是继续检查 SVG 资源后,才确认内容映射有误。

所以排查顺序应该是:

  1. 先看外层 DOMRect。
  2. DOMRect 错,再谈 rem、flex、断点、min-width。
  3. DOMRect 对,优先查 SVG 源码。

这一点也能避免过度修复。否则为了一个资源问题改掉全局尺寸策略,很容易把其它图标、头像、按钮热区也带出新问题。

哪些 SVG 可以 none

preserveAspectRatio="none" 不是永远错误。它适合那些本来就应该拉伸的资源:

  • 背景光效。
  • 渐变色块。
  • 分割线或可变宽装饰。
  • 卡片背景里明确要求铺满容器的纹理。

但交互图标一般不属于这一类。返回、关闭、更多、行尾箭头、充值、钱包、语言、设置这些图标,用户预期它们保持固定比例。它们如果需要更大的点击区域,就应该加外层 frame 或透明 padding,而不是拉伸内部 path。

所以项目规则可以写成:

交互图标默认显式保持等比;装饰背景需要拉伸时才允许 preserveAspectRatio="none",并写清楚它是可拉伸资产。

资源验收要看「可见边界」

导出 SVG 后,需要核对资源边界与设计稿、组件显示尺寸的对应关系:

  • SVG 根节点有没有合理的 width / height / viewBox。
  • preserveAspectRatio 是否符合图标语义。
  • 可见图形是不是居中在 frame 里。
  • 透明边距是否和 Figma 一致。
  • 组件侧有没有把内部图形当外层 frame 二次拉伸。
  • 真机上局部截图是否和 PC 一致。

如果是 PNG,也要检查透明像素边界。比如设计稿是 36 x 36 frame,内部真实图形 27 x 27,入库资源就应该保留 36 x 36 透明外壳,而不是裁成 27 x 27 后再让组件猜怎么居中。

测试怎么守住

这类问题不一定适合整页截图快照。截图快照成本高,波动也多。更低成本的测试可以这样设计:

  • 单测或 source test 检查关键 SVG 不含 preserveAspectRatio="none"。
  • 对交互图标做资源扫描,确保根节点有 viewBox,且 preserve 策略符合预期。
  • Playwright focused E2E 进入目标页面,断言图标 DOMRect 是正方形,必要时裁局部图做人工证据。
  • 对已经出过问题的图标,补一条「资源根节点 preserve 策略」的回归测试。

资源检查约束 viewBox 和等比策略,DOMRect 断言检查外壳尺寸;内部图形是否仍有变形,再用局部截图核对。

总结

这几个图标的外壳尺寸是对的,修复落在 SVG 的坐标映射和 frame 处理上。完整 frame、透明内边距和图形位置留在资源里,组件就不必再维护一张尺寸和偏移表。不过 DOMRect 正确只能排除一部分布局问题;图形仍然变形时,实际加载的资源、内部坐标映射、嵌套 transform 和 CSS 变换都还要继续核对。