SVG 这个格式,我用了快十年,越用越觉得它被严重低估了。很多人一提 SVG,第一反应是“一种图片格式”,可它骨子里根本不是图片,而是一份用 XML 写的“几何说明书”。最近项目里要做一套能够自适应任意屏幕尺寸的图标系统,还要支持换肤、动效、甚至在 Android Compose 里复用同一套图形资产,位图方案简直寸步难行,SVG 几乎成了唯一解。
这篇东西不是科普文档,我就按自己做项目的思路来讲:先说清 SVG 为什么强、再说核心代码怎么读怎么写,然后用六个真实业务场景把“代码”和“用途”串起来,最后把我踩过的坑和排查方法一次性列给你。适合前端、客户端、UI 设计师、文档工程师,以及所有经常跟“图”打交道但还没认真碰过 SVG 的人。
1. SVG 凭什么这么强:一张不会糊的图背后的原理
1.1 位图和矢量图的本质差异:照片、马赛克与函数曲线
要理解 SVG 为什么强,得先理解它和 JPG、PNG 这类位图在底层逻辑上的区别。
位图记录的是一个个像素点的颜色值。一张 100x100 的图片,就是一万个色块组成的网格;放大到 1000x1000,像素不够了,软件只能插值“猜”出中间的颜色,猜得不好就发虚、出现锯齿、糊成一片。这跟我们看马赛克照片是一个道理,方块数量固定,放得越大越粗糙。
SVG 记录的是几何元素的数学描述:直线、圆、曲线、颜色渐变。它不保存任何一个像素点,只保存“这个圆的圆心是在坐标 (50, 50)、半径 30、填充红色”这样的公式。浏览器在渲染时,会根据你的屏幕尺寸现场“画”出来。屏幕是 1 倍缩放就画 1 倍,是 3 倍缩放就画 3 倍,每一根曲线都重新计算,所以怎么放大都锐利如初。
这就是为什么同一个 Logo,用在网页上的尺寸是 32px、印刷出来是 6 米宽,用 SVG 一份资产就全搞定,用位图你得准备七八个不同分辨率的版本。
1.2 SVG 的真正王牌:不止是“清晰”
很多人以为 SVG 的优势就是“放大不糊”,这只是起点。我实际用下来,真正的王牌是下面这几条:
- 它是文档而不是图片,天然能被大模型、代码和脚本理解和修改。JPG 打开只能改像素,SVG 用文本编辑器就能改,属性、颜色、结构一目了然。
- 节点能挂事件、能被 CSS 控制、能被 JS 操作。你给一个
<circle>补一个onclick,它就是一个会响应的按钮;你用 CSS 改它的fill,一套设计同时适配深浅主题。 - 文件体积极其小。一个复杂的线性图标可能只有几百字节到几 KB,几十上百个图标打包成一个 sprite 文件,也常常不到一张普通 PNG 的大小。
- 支持动画。CSS 动画、SMIL 动画、JS 驱动都可以作用于 SVG 内部节点,这是画布图片永远做不到的。
- 文本可检索。SVG 里的文字不是“图片上的印字”,而是可被搜索引擎收录、可被读屏软件朗读的真实文本。
- 跨端复用越来越成熟。浏览器、iOS、Android、Office、矢量设计工具对 SVG 支持都很完整,甚至能映射到 Compose 的 ImageVector、iOS 的 Core Graphics。
当然它也有短板:照片级图片、噪点纹理这类复杂内容,SVG 的渲染开销和文件体积会飙升,这时候位图反而是正确选择。所以判断标准很简单——凡是“图形、图标、图表、线稿”,优先考虑 SVG;凡是“照片、材质、重效果”,老老实实用位图。
1.3 哪些项目不用 SVG 会很难受
从我经手的项目看,下面几类场景几乎是 SVG 的主场,不用它,后面维护成本会高到你头疼:
- 图标系统:一个 App 里几百个图标,需要换尺寸、换颜色、做动效,用位图会疯掉。
- 数据可视化:图表要响应数据变化,SVG 的每个柱子、每个点都是可编程的 DOM 节点,悬停提示、联动高亮都顺手很多。
- 地图与导览:楼层平面图、展馆导览、车位图,需要热区点击和矢量缩放,用 SVG 比用 Canvas 容易得多。
- 品牌 Logo:要上网页、上名片、上 80 米广告牌,SVG 一份到位。
- 吉祥物与轻量角色动画:配合 CSS 做呼吸、眨眼、点头这些“假 Live2D”效果,比序列帧省太多流量。
- 文档图表:Mermaid、ECharts 这类工具导出的图表中,SVG 是默认主力,既能保证打印清晰,还能保留文字和交互。
2. 手写 SVG:核心代码和必须吃透的几个概念
2.1 SVG 骨架:xmlns、width、height 与 viewBox
一个最小的可显示 SVG 长这样:
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300" viewBox="0 0 100 100"> <rect x="10" y="10" width="80" height="80" rx="8" fill="#4A90E2"/> <circle cx="50" cy="50" r="20" fill="#FFF"/> </svg>xmlns是 SVG 的“身份证”,声明这个文档是 SVG 命名空间下的内容。浏览器靠它区分 XML 里的 SVG 和普通文本,没有它,很多浏览器会把里面的标签当成未知元素直接忽略。
width和height是 SVG 在页面里占的显示尺寸,而viewBox="0 0 100 100"定义的是内部坐标系:左上角是 (0, 0),右下角是 (100, 100)。有意思的地方来了——内部是 100 单位宽的图,要显示在 300 像素宽的容器里,那每个内部单位自动对应 3 个物理像素。这意味着你画图时只需要思考“内部逻辑尺寸”,不用关心实际显示尺寸。
关于缩放有一个常见误区:如果不写viewBox只写width/height,SVG 默认内部坐标系就是像素对像素,你画一个半径为 20 的圆在 20 寸显示器上就真只有 20 像素。所以绝大多数情况我都建议写上viewBox,用逻辑坐标画图。如果希望图片在容器变形时保持等比,还要配合preserveAspectRatio="xMidYMid meet",这个参数的意思是“等比缩放,居中显示,完整展示整个图”。实际项目里给<img>或给背景图用 SVG 时,忘记写 viewBox 是最容易出的低级错误,排查半天发现根本不是代码逻辑的问题。
2.2 常用图形元素:从最简单到最核心的 path
SVG 的基础图形元素可以直接当积木用,每个都对应真实世界的一个几何概念:
<rect>:矩形,四个核心参数是x、y、width、height,还能用rx、ry控制圆角。<circle>:圆,cx、cy定圆心,r定半径。<ellipse>:椭圆,rx、ry分别是横向、纵向半径。<line>:直线,x1/y1和x2/y2决定起点终点,适合画分割线、刻度线。<polyline>:多点连线,常用于折线图。<polygon>:闭合多边形,常用于三角形、箭头、地图的区域块。<path>:最强大也最复杂的元素,可以描述任意曲线、弧线和不规则形状,几乎所有复杂图标最终都会落到 path 上。
说一下 path。你可以把它想象成一支画笔的轨迹记录:M是“把笔移到某个点”,L是“画直线到某个点”,C是“画一条贝塞尔曲线到某个点”,Z是“闭合路径回到起点”。大写命令表示绝对坐标,小写命令表示相对上一个点的坐标。
一个简单心形可以用如下 path 表示:
<path d="M50 30 C50 30 30 20 18 32 C6 44 50 72 50 72 C50 72 94 44 82 32 C70 20 50 30 50 30 Z" fill="#E5484D"/>你不需要背这些,但看懂 path 能让你排查问题事半功倍。比如图标位置整体偏移了,多半是 M 的起始坐标不对;形状出现莫名其妙的尖角,很可能是 C 命令的控制点写错。用编辑器里的路径工具画完后再自己读一遍坐标,就像程序员 review 代码一样。
2.3 样式系统:fill、stroke、渐变与 CSS 集成
SVG 图形的外观由两大属性控制:fill是填充颜色,stroke是描边颜色。这两个属性既可以写在元素上,也可以用 CSS 类统一控制:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> <style> .icon { fill: none; stroke: #333; stroke-width: 4; stroke-linecap: round; } .red { fill: #E5484D; stroke: none; } </style> <circle class="icon" cx="30" cy="50" r="20"/> <rect class="icon red" x="55" y="30" width="40" height="40" rx="6"/> </svg>理解“SVG 节点可以被 CSS 选中”是很关键的一步。这意味着你在业务代码里可以像操作 DOM 那样,通过.icon--active { fill: #FF5A5F; }实现图标换色,通过媒体查询实现深浅主题适配,甚至给不同图标挂不同的悬停效果。
再进阶一点,<defs>是 SVG 的“资源仓库”,在里面定义渐变、滤镜、剪裁路径,然后用url()引用。举一个线性渐变的例子:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100"> <defs> <linearGradient id="grad" x1="0" y1="0" x2="1" y2="0"> <stop offset="0%" stop-color="#FC466B"/> <stop offset="100%" stop-color="#3F5EFB"/> </linearGradient> </defs> <rect x="10" y="10" width="180" height="80" rx="10" fill="url(#grad)"/> </svg>渐变看似简单,但有个容易忽略的点:x1/x2/y1/y2如果只写相对值而不指定gradientUnits,默认是基于对象边界框的百分比,也就是渐变会跟着图形大小走;如果希望渐变在整个 SVG 里固定,要设置gradientUnits="userSpaceOnUse"。这一条在跨端适配时特别容易踩中,Android 转换 SVG 时稍有偏差,渐变方向就反了。
2.4 一个完整案例:手写一只“鹈鹕骑自行车”的 SVG 草图
很多时候我们拿到一个“generate an svg of a pelican riding a bicycle”这类需求,第一反应是直接用 AI 生成。AI 确实能生成,但生成的代码往往冗余、标签乱、坐标飘,动手修复一次不如干脆自己搭结构。我手写了一个简笔版本,给你做个起点:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 160" width="600" height="400"> <!-- 后轮 --> <circle cx="60" cy="130" r="40" fill="none" stroke="#333" stroke-width="6"/> <line x1="60" y1="90" x2="60" y2="130" stroke="#333" stroke-width="4"/> <circle cx="60" cy="130" r="6" fill="#333"/> <!-- 前轮 --> <circle cx="180" cy="130" r="40" fill="none" stroke="#333" stroke-width="6"/> <line x1="180" y1="90" x2="180" y2="130" stroke="#333" stroke-width="4"/> <circle cx="180" cy="130" r="6" fill="#333"/> <!-- 车架三角 --> <polygon points="60,130 120,90 180,130" fill="none" stroke="#333" stroke-width="6"/> <!-- 车座 --> <line x1="100" y1="80" x2="120" y2="75" stroke="#333" stroke-width="8" stroke-linecap="round"/> <!-- 车把 --> <line x1="150" y1="80" x2="180" y2="90" stroke="#333" stroke-width="6"/> <line x1="150" y1="80" x2="165" y2="65" stroke="#333" stroke-width="6"/> <!-- 鹈鹕身体 --> <ellipse cx="120" cy="55" rx="28" ry="15" fill="#F7C948"/> <!-- 头 --> <circle cx="145" cy="40" r="12" fill="#F7C948"/> <!-- 喙 --> <polygon points="153,42 175,36 153,46" fill="#F78C6B"/> </svg>这当然不是精品插画,但作为结构演示已经足够了:轮子是圆、车架是折线、鹈鹕是椭圆和三角形。真正的手绘过程就是这样的,先用基础图形把画面“搭”出来,再逐步细化眼睛、羽毛、轮辐这些细节。这里也给你一个思路:AI 提示词不要写“给我一只骑自行车的鹈鹕”这种空泛描述,而要拆解成“画布 240x160、背景为空白、简笔卡通风格、暖黄色基调、两个大轮子构成自行车主体、鹈鹕位于车座上方、身体使用椭圆、嘴使用尖锐三角、保留动作夸张感”,给出的约束越接近 SVG 的几何语言,生成结果越好修。
3. 六个真实业务场景:从动画、地图到跨端复用
3.1 场景一:生成式 SVG 与提示词方法论
最近“svg图片”和“鹈鹕骑自行车动画svg提示词”这类搜索很多,说明越来越多人在尝试让大模型直接产出 SVG。我的经验是:AI 能产出结构正确的 SVG,但它对视觉审美的理解非常有限,而且经常生成包含滤镜、渐变、复杂 path 的臃肿代码。
所以,我不建议直接把 AI 输出贴到生产环境。正确姿势是:把 AI 当成“草图生成器”和“占位符生成器”,拿到后再做两层加工。第一层,删冗余:去掉 AI 重复定义的<defs>、没有使用的<clipPath>、多余的空<g>。第二层,改语义:把path1、path2这类无意义 id 改成wheel、pump、saddle,后面做动画、做测试、做无障碍标注都要靠这些名字。
提示词层面,我自己常用的模板是:
请生成一个 SVG,画布 viewBox 为 0 0 300 300,风格是扁平卡通。 主题:一只鹈鹕骑着自行车。 构图要求:自行车占画面下方三分之一,两个圆形车轮必须完整出现在画布内; 鹈鹕位于画面中心偏上,表情欢快。 配色:主色暖黄 #F7C948,辅色深灰 #333333,嘴和脚用橙红 #F78C6B。 图形组成:优先使用 circle、ellipse、polygon、path,避免使用滤镜和外部字体。 最后附上每个图形元素的语义化注释。这么一问,生成结果通常比“画一只鹈鹕骑自行车”好修十倍。这也是把生成式 AI 用在设计资产上的核心思路:你不是在问它“画个图”,而是在跟它“描述一份几何文档”。
3.2 场景二:SVG 也能做“假 Live2D”角色动效
很多人搜“svg live2d”,其实是想做那种角色轻微呼吸、眨眼、点头的看板娘效果。真正的 Live2D 有专用模型格式和运行时,但如果你想在网页里用轻量方式实现同款“活起来”的感觉,SVG 分层 + CSS 动画完全能凑合。
核心方法论是把角色拆成“层”。头部一层、身体一层、眼睛一层、手臂一层,每个层用一个<g>包裹。动画只作用于<g>,而不是作用于整个<svg>。以呼吸为例:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"> <style> .body-layer { animation: breathe 3s ease-in-out infinite; transform-origin: 100px 160px; } @keyframes breathe { 0% { transform: scale(1); } 50% { transform: scale(1.03) translateY(-3px); } 100% { transform: scale(1); } } </style> <g class="body-layer"> <ellipse cx="100" cy="150" rx="35" ry="50" fill="#FFD1DC"/> <circle cx="100" cy="60" r="28" fill="#FFD1DC"/> </g> </svg>这里最关键的属性是transform-origin。CSS 的 transform 默认旋转和缩放中心在元素自身的左上角也就是几何中心,但 SVG 里的坐标系和 CSS 盒子模型不完全一致,尤其是多个<g>嵌套时,不指定transform-origin的话动画会绕着一个莫名其妙的点转,角色像在漂移。我通常把transform-origin明确写成角色模型的盆骨或重心位置,动画一下子就稳了。
相比真实 Live2D,SVG 方案的局限在于表情混合和网格形变做不了,但胜在零依赖、几十行代码就有效果,流量开销小。适合吉祥物、弹窗角色、加载页动画。
3.3 场景三:SVG 室内导览系统:地图、热区与空间数据
“svg室内导览系统”也是高频搜索词之一。商场导航大屏、展馆导览页、地下车库找车这类需求,SVG 比 Canvas 好做太多,因为你需要的不只是“画一张平面图”,还需要“让平面图上的每个房间可点击、可悬停、可标注”。
实际项目里我会把平面图切成三层:
- 底图层:从 CAD 或设计稿导出的 SVG 轮廓,只负责展示墙体和房间边界。
- 数据层:一套坐标数据,记录每个房间的位置、名字、状态。这里的坐标直接复用 SVG 里的用户坐标系。只要底图定好
viewBox,房间的x/y就是空间定位。 - 交互层:用
<a>或挂事件的<polygon>实现点击区域,配title、desc做无障碍提示。
一个非常简化的示例是:把某个房间画成多边形,悬停时改变透明度,点击后触发详情。由于 SVG 本身就是 DOM,这些事情天然支持,不用像 Canvas 那样自己管理命中检测。真要做一个完整导览系统,还需要注意分级加载:楼层多时不要一次性渲染全部房间,而是切换楼层动态显示;缩放级别高时显示房间号,低时只显示区域名。用visibility切换图元,比反复重绘性能好得多。
3.4 场景四:SVG 转 Kotlin ImageVector:跨端资产复用
“svg → compose imagevector kotlin”这条热搜说明很多 Android 开发在找 SVG 的跨端方案。Jetpack Compose 里如果用 Bitmap 承载图标,App 安装包体积会变大,而且不同密度屏幕还要适配。Compose 官方推荐的轻量方案就是 ImageVector,它和 SVG 一样,记录的也是图形指令,而不是像素。
从 SVG 到 ImageVector,目前有两套常用路线:
第一,Android Studio 自带转换。在 res 目录右键 → New → Vector Asset,选择本地的 SVG 文件,Android Studio 会生成一个 XML 格式的 VectorDrawable,Compose 可以直接读取,或者再用工具生成 Kotlin 的 ImageVector 对象。
第二,手写 ImageVector 代码。如果你只是复用一两个简单图标,可以直接在 Kotlin 里定义:
val BikeIcon: ImageVector by lazy { ImageVector.Builder( name = "Bike", defaultWidth = 24.dp, defaultHeight = 24.dp, viewportWidth = 24f, viewportHeight = 24f ).apply { // path 对应 SVG 里的 path,fill 对应 fill 颜色 path( fill = SolidColor(Color.Black), pathData = PathParser().parsePathString("M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25z M20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z").toNodes() ).build() } }这里路径数据可以直接从 SVG 里拷贝,viewportWidth/Height对应 SVG 的viewBox,defaultWidth/defaultHeight对应显示尺寸。
要注意的是转换有兼容损失:SVG 里的滤镜(filter)、渐变(gradient)、剪裁路径(clipPath)有很大概率在 ImageVector 里失效或变形。我的建议是:用于跨端的 SVG 尽量用最简单的基础图形和纯色填充,越接近“几何线稿”,迁移成本越低。这也是设计资产规范化的价值——提前约定好约束,跨端复用时才能“一次导出,处处使用”。
3.5 场景五:Mermaid 图表导出 SVG:文档体系里的隐形功臣
“mermaid代码”这几年火得不行,它用纯文本定义图表,再渲染成漂亮的图形。很多人不知道的是,Mermaid 的默认图形输出主格式正是 SVG。技术文档里放一张可搜索、可交互、可自动适配暗色主题的图表,比截图放一张位图体验好太多。
Mermaid CLI 的导出命令大概是:
npx -y @mermaid-js/mermaid-cli -i flowchart.mmd -o flowchart.svg -t dark-t dark可以换主题,也可以用自定义 CSS 覆盖节点颜色和字体。我自己在组内文档系统里做图表时,几乎不用默认主题,而是给flowchart配一套和设计系统一致的品牌色变量,这样文档里的图和页面风格完全统一。
导出 SVG 后你可以把文件直接嵌到页面里,也可以再加工成 symbol 放进图标库。踩过的坑有两个:一是中文乱码,通常是系统缺少中文字体,渲染环境里必须安装中文字体并在配置里指定;二是导出的 SVG 里默认带了比较宽的 viewBox,嵌入后比例不对,需要手动调整preserveAspectRatio。记住:Mermaid 只是生产 SVG 的手段,真正要保证质量,最终都得落到“把 SVG 当成工程资产来管理”这件事上。
3.6 场景六:SVG 图标库与 figure 下载:团队设计系统的落地
“svg图例下载”这个关键词背后,其实是团队里常见的痛点:设计画好了图标,开发却找不到、格式不对、命名混乱。我推荐的做法是做一套 SVG Sprite,把所有图标收纳到一个文件里。
Sprite 的核心机制是<symbol>和<use>。symbol定义图标的“零件”,use在页面里的任何位置“实例化”它:
<svg style="display:none" xmlns="http://www.w3.org/2000/svg"> <symbol id="icon-home" viewBox="0 0 24 24"> <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/> </symbol> <symbol id="icon-search" viewBox="0 0 24 24"> <path d="M15.5 14h-.79l-.28-.27a6.5 6.5 0 1 0-.7.7l.27.28v.79l4.25 4.25 1.5-1.5-4.25-4.25z"/> </symbol> </svg> <svg class="icon"><use href="#icon-home"/></svg> <svg class="icon icon--large"><use href="#icon-search"/></svg>这套方案的好处是:所有图标只请求一次;CSS 能统一控制尺寸和颜色;新增图标只需要改 sprite 文件,不需要改业务页面。团队协作时我会定三个硬性规则:
- 每个图标必须是统一画布尺寸,推荐 24x24 的逻辑坐标。
- 图标命名必须语义化,比如
icon-user-add而不是icon-new-1。 - 外部引用的图标只允许纯色或单色填充,方便 CSS 控制颜色。
有了这套机制,所谓“svg图例下载”就变成一个内部 npm 包或静态资源的版本管理问题,而不是群里传来传去。
4. 工程实践中的高频坑:实测排查记录
4.1 图显示不出来,到底错在哪
SVG 不显示的原因非常多,按频率排序,我遇到过最多的是这几种:
- 缺少
xmlns="http://www.w3.org/2000/svg"。在 HTML 5 里很多时候写不写都能显示,但一旦放进 XML 环境、或者你把它当独立文件打开,缺了就是一片空白。 viewBox写错或漏写,导致图形跑到了可视区域之外。- path 的
d指令语法错误,比如 C 后面的坐标数量不对、Z 后多了空格或逗号。浏览器对 SVG 解析很宽容,错误的 path 不会报错,只是整个图形消失。 - 定义了
fill="none"却没有设置stroke,画出来的图形其实是透明的。 - 标签大小写错误,比如
viewbox写成了小写。SVG 属性名是区分大小写的。
排查流程我也固定下来了:先用浏览器直接打开 SVG 文件,确认文件本身有没有问题;再看页面的 SVG 外层容器尺寸,如果是 0 高度自然什么都看不见;最后逐步删掉元素,二分定位到底是哪一段代码导致渲染失败。最高效的方式是给它加一个最显眼的<rect>铺满画布,如果这个矩形能显示,说明画布和坐标系没问题,问题出在具体元素上。
4.2 viewBox 与尺寸:为什么图形偏到了角落
经常有同学把 SVG 嵌到页面里,发现图形只占据左上角一小块,或者被拉伸得变形。这通常是viewBox的宽高比和width/height的宽高比不一致导致的。
浏览器默认的preserveAspectRatio行为是等比缩放并居中显示,但在使用<img>时如果只设置宽度不设置高度,部分浏览器会拿viewBox的比例去计算高度,这时候表现还算正常;一旦你显式设置了和viewBox不成比例的宽高,就会出现裁切或留白。
解决方式有三个,按喜好选:第一,让外层width/height的宽高比始终和viewBox一致,这是最不容易出错的;第二,显式声明preserveAspectRatio="xMidYMid meet",让浏览器自动等比居中;第三,设置为preserveAspectRatio="none",这会强制拉伸以填满容器,适合背景图但会让图标变形,非特殊情况别用。
我做设计系统时,会把“viewBox宽高比就是图标标准比例”写进组件规范,从源头避免这问题。
4.3 path 指令的坑:坐标漂移与未闭合
path 是最容易藏问题的地方。我记几个必须形成肌肉记忆的点:
- 大写指令是绝对坐标,小写指令是相对坐标。混用没问题,但语义要清楚,否则改一个点时会牵连一片。
C需要三个坐标对:两个控制点加一个终点,少一个数直接整段失效。Z之后不能再画新线段,如果要继续画,得重新用 M 起笔。- 相对坐标的
m和绝对坐标的M后如果跟着多个坐标对,只有第一对是移动,后面的会被理解为直线。这也是“路径为什么莫名多了一条线”的常见原因。
真正排查路径问题时,最好的工具是矢量编辑器,把它导入 Figma 或 Adobe Illustrator,逐段路径检查,比肉眼读坐标快得多。路径这种东西,写出来容易,查错难,依赖可视化工具是正道。
4.4 文字变豆腐块:字体的锅怎么甩都甩不掉
SVG 支持<text>元素,但“支持”和“靠谱显示”是两回事。SVG 文本渲染依赖用户电脑上有没有对应的字体。你用 Figma 导出 SVG 时如果没做“转为轮廓”,那 SVG 里保存的是字体名称,别人电脑上没有这个字体,浏览器就会用默认字体替换,排版瞬间变丑,中文场景尤其严重,因为中文字体体积大,几乎不可能随 SVG 分发。
最可靠的方案是把文字转成 path 再导出。Figma 里选中文本按Shift+Ctrl+O转曲线,Illustrator 里叫“创建轮廓”。代价是文字失去可检索性,所以要根据场景取舍:标题、Logo 这类以精确呈现为优先的,转 path;正文、注释这类需要被搜索和朗读的,保留<text>。这个决策没有标准答案,但要提前想清楚。
4.5 内联、img 还是 background:引入方式的选择
同样是 SVG,引入方式不同,能力差别巨大:
<img src="xxx.svg">:简单可靠,能缩放、能缓存,但无法操作内部节点,CSS 改不了颜色,JS 挂不了事件。- 内联 SVG:把 SVG 代码直接写进 HTML 或组件里。代价是每次都要解析,但换来的是全部控制权,主题换色、点击交互、动画都随便玩。
- CSS
background-image: url(xxx.svg):适合纯展示、多个位置复用的场景,但同样没法交互,而且不能用 CSS 控制内部样式。
我的经验是:图标类、需要换色的,一律内联或放进 sprite 用<use>引用;Logo、装饰性图形、图表成品,用<img>或 background,简单省事。千万不要在一个页面上嵌入几百个内联 SVG 还不做合并,那样 DOM 节点数量会爆炸,性能直接掉到不忍直视。
4.6 动画卡顿:性能瓶颈到底在哪
SVG 动画卡顿,常见原因是动画属性选得不对。CSS 动画里最便宜的两个属性是transform和opacity,因为它们可以由合成器处理;一旦去动画fill、stroke、filter、width这些属性,浏览器就不得不反复重绘整个图层,性能差一个数量级。
做 SVG 动画时我给自己定了三条纪律:
- 能用 transform 改变的位移、缩放、旋转,绝不去改坐标系属性。
- 滤镜
<filter>能少用就少用,特别是模糊半径大的阴影,渲染开销非常高。一个页面放几个带大范围模糊的滤镜,低端手机上立刻掉帧。 - 使用
will-change: transform可以提示浏览器提前优化,但别到处乱加,否则反而吃内存。
这些经验来自真实项目:一个加载动画里我最初用了 CSS 动画改stroke-dashoffset来做描边效果,在低端安卓机上明显掉帧。后来改成经典的“旋转进度环 + opacity 呼吸”,代码更简单,动画反而顺滑很多。
5. SVG 工程化与团队协作清单
5.1 自动化压缩:不要手写,用工具链
手写 SVG 久了会发现,设计软件导出的 SVG 往往包含大量无用信息:编辑器的元数据、空的分组、多余的属性、大而全的 XML 声明。直接提交这些代码进项目,既拖慢解析速度,也给别人维护添堵。
我现在的标准动作是统一用 SVGO 压缩。命令行一行就能处理一个目录:
npx svgo -f src/svg -o dist/svgSVGO 的默认配置已经能去掉大部分垃圾,但我额外要求团队在配置里保留viewBox选项。默认预设里如果勾选了removeViewBox,会把viewBox删掉,导致 SVG 在外部容器里失去适配能力。这个坑我必须标记为“团队新人必修坑”。
压缩前后对比非常直观:一张从设计软件导出的“真实”图标可能有十几 KB,压缩后可能只剩一两 KB。经过压缩的 SVG 才能称为工程资产,原始导出文件只能叫草稿。
5.2 命名规范与代码评审:把 SVG 当代码来 review
我在团队里推行一个习惯:SVG 的代码评审标准和代码一致。文件名语义化、内部 id 语义化、关键结构写注释,这三条必须在提交时检查。
实际业务里很多开发会把图标从设计稿里导出,文件名默认是“未命名 2”“图标 副本 3”,然后直接塞进项目。等需求方说“把删除按钮的图标换个颜色”,你根本找不到那个文件是哪个。我的建议是:从设计稿导出时就按组件名定义文件名,比如button-delete、button-edit、menu-search,内部元素的 id 也尽量写成delete-icn-trash、delete-icn-body,连路径里的图形轨迹都能对应上,调试时会非常顺畅。
另一件容易被忽略的事是 XML 的“最小编辑原则”:SVG 毕竟是代码,交给 AI 生成或从设计稿导出后,动手改之前先弄清楚每一段是干什么的,一击到位,不要反复改。你大概率会被一个大大的好处打动——后续换肤需求只需要改fill一处,前提是代码结构保持整洁。
5.3 可访问性与 SEO:SVG 图标的另一面价值
SVG 的可访问性经常被忽视。默认情况下,读屏软件会把 SVG 当作一个整体图片处理,不读取内部文字。如果这张图表达的是关键信息,请加上<title>和<desc>:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-labelledby="title desc"> <title id="title">保存文件</title> <desc id="desc">一个磁盘图标,代表保存操作</desc> <path d="M6 2h9l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z"/> </svg>相反,如果这个图标是装饰性的,跟内容无关,就给它加aria-hidden="true",避免读屏软件读出一堆莫名其妙的节点。这一来一回,效果好很多,而且这些基本是零成本的。
SEO 方面值得注意:搜索引擎能检索 SVG 文件里的文本内容,因此做百科、说明书、官网的图文信息时,把流程图的文字保留在 SVG 里而不是全部转 path,能带来可观的搜索红利。
5.4 兼容性检查:新特性很美,旧环境很现实
最后聊兼容性。SVG 的老问题是 IE 和旧版 Android WebView 对滤镜、SMIL、部分 CSS 属性支持不全。现在主流浏览器已经没太大问题,但在一些内嵌浏览器的场景(比如 App 内 WebView、一些扫码后打开的轻应用)仍然容易出状况。
我个人的兼容性测试清单是:
- 必测点:
<img>方式、内联方式、background 方式三者的显示效果。 - 必测点:暗色模式下 CSS 变量替换
fill是否生效。 - 必测点:
preserveAspectRatio在不同容器宽高比下的表现。 - 谨慎使用:CSS 变量在 SVG 内部的兼容性、SMIL 动画在新版 Safari 的表现、filter 滤镜在部分安卓 WebView 中的崩溃问题。
如果目标是兼容老设备,最稳妥的路径是“降级”:检测到不支持 SVG 的环境,给<img>换 png fallback,或者直接放弃特效,用基础几何图形替代复杂滤镜。做决策前一定用 caniuse 查一遍再定,别凭感觉。
最后分享一个我自己的小习惯:每次从 Figma 导出一批 SVG 图标,我都会顺手开一个本地预览页面,把图标全部按 1x、2x、3x 三种规格列出来,再切一个暗色主题,用眼睛扫一遍。这一步花不了几分钟,但能提前抓住百分之八十的边框、颜色、可读性问题。SVG 是个越用越顺手的格式,希望这篇内容能帮你在项目里少踩几步坑,也多发现一些它真正好用的地方。