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

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

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

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

先证明不是外层尺寸问题

遇到图标变扁,第一步不要猜。先打开 DevTools,看三件事:

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

如果外层按钮是 40 x 40img 也是 40 x 40,但里面的图形明显被压扁,那就不是 flex 把按钮挤扁,也不是 rem 把尺寸算错。问题在 SVG 内部:浏览器把 SVG 内容画进这个盒子时,采用了会拉伸的映射方式。

这一步很关键。否则很容易沿着错误方向改:把 size-10 换成 inline style,把 rem 换 px,给按钮加 min-width,甚至重写一套移动端样式。最后外壳越来越复杂,图标还是会在某些场景变形。

preserveAspectRatio 到底影响什么

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

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

preserveAspectRatio="none" 的意思是:不要保持比例,把 viewBox 直接拉满当前盒子。盒子是正方形,viewBox 是非正方形,或者真实图形所在区域和 viewBox 比例不一致时,图形就会变胖、变瘦、变扁。

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

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

同一个 viewBox 放进正方形外壳时,meet 会保持比例,none 会直接拉伸填满导致变形

Figma 的 frame 不等于真实图形

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

  • 外层 24 x 2440 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 模拟移动端没暴露,真机暴露了

Chrome DevTools 模拟移动端很有用,但它不等于真机。

真机浏览器会叠加更多变量:设备像素比、字体缩放、地址栏收缩、不同浏览器 SVG 渲染细节、WebView 内核版本、图片解码策略、以及真实触摸滚动带来的布局状态。某些 SVG 拉伸问题在桌面 Chrome 上看起来不明显,到了手机上就很刺眼。

但这不代表真机问题无法定位。还是回到证据:

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

这次的证据链属于第一类:外壳尺寸正确,内部图形被拉伸,所以不是“移动端布局把按钮挤坏”,而是“SVG preserve / viewBox / frame 处理不对”。

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

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

但这次图标变扁不能直接归因到 rem。原因很简单:如果 computed size 已经是正确的正方形,说明外层 CSS 尺寸链路没坏。图形仍然变扁,只能说明 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 策略”的回归测试。

这类测试守的是根因,不是某张截图里的偶然像素。

最后总结

图标变扁时,不要一上来改响应式。

先问三个问题:

  1. 外层 DOM 尺寸对不对?
  2. SVG 的 viewBox 和 preserve 策略对不对?
  3. Figma 的完整 frame 有没有被保留下来?

如果外层尺寸对,内部图形变形,问题大概率在资源,而不是 CSS。

最稳的做法是:Figma 里的交互 frame、透明内边距和内部图形位置都沉淀进 SVG 或 PNG 资源;业务组件只渲染这个完整 frame。这样不但 PC 和真机更一致,也能避免以后每加一个图标,就在 Vue 模板里再发明一张尺寸和偏移表。