DiceBear Blobs 是我这两年做匿名头像时用得最多的一个预设。它不像那些插画型风格需要精心摆姿势、挑五官,Blobs 只给你几坨圆滚滚的、带着呼吸感的色块,放在列表页、评论区、协作文档的协作者标识里都不突兀。上个月给一个社区类产品做匿名身份体系,需求方原话是"不要照片、不要卡通脸,要那种看起来像一滴正在融化的墨"。我试了五六个风格,最后落到 Blobs 上,原因很简单:它对配色的容忍度极高,对内容信息量几乎零要求,而且尺寸压到 32px 还能看出形状差异。
但这套东西的文档写得偏"参数字典",真正上手时会卡在几个地方:backgroundColor传了没生效、同一个页面放十几个渐变头像颜色串成一锅粥、想让它动起来却不知道从哪下手、升级一个小版本后发现老朋友们的头像全变了。这篇就把 Blobs 的配色方案、动画开关和实战用法一次讲透,从参数含义到封装落地,适合刚接手的同学直接抄,也适合已经在用但被坑过的同学对一遍账。
1. Blobs 预设到底在生成什么
1.1 从 seed 到一段路径:生成链路拆解
先把底层逻辑说清楚,后面调参才不是瞎试。Blobs 属于"随机几何"型风格,和那些"眼睛、嘴巴、发型"拼装出来的插画型完全是两条路。它在一个方形画布(通常按 100x100 的 viewBox 组织)里,用若干控制点围出一个闭合轮廓,控制点的角度和半径由伪随机数驱动,再用曲线把点连成平滑边界,最后填充颜色,得到一个圆润的不规则形状。有的实现会叠两到三坨,形成大小墨滴互相遮挡的层次感。
关键在于这个伪随机数不是真随机,而是由seed经过哈希后喂给一个确定性的发生器。也就是说,只要 seed 相同、风格版本相同、参数相同,出来的 SVG 逐字节一致。这一条是整个方案能落地的前提——你可以把用户 ID 当 seed 存进数据库,任何时候都能重算出同一张头像,不需要真的存图片文件。我一般直接把用户的稳定标识(比如业务侧的用户编号)拼一个前缀当 seed,好处是可读、可调试,出问题时能一眼看出这张头像是谁。
需要提醒的是,这种确定性只在"同一个风格版本"内成立。风格实现里的控制点数量、曲线算法、默认调色板都可能在小版本里微调,一旦变了,同一个 seed 出来的形状就跟着变。这是后面第 6 章要重点说的坑,先记一笔。
1.2 为什么我优先选 Blobs 而不是别的风格
选风格这件事,我一般看三个维度:信息量、尺寸鲁棒性、和界面的冲突程度。信息量指的是头像本身带多少可辨识特征——插画型风格特征多,好看,但在"匿名"场景里反而危险,因为用户会去记"那个戴眼镜的",形成非预期的身份感。Blobs 信息量低,只有形状和颜色,天然适合匿名。
尺寸鲁棒性是第二个关键点。很多细节丰富的风格压到 24px 或 32px 就糊成一团噪点,而 Blobs 是大色块加平滑边界,缩到 24px 依然能靠轮廓和色相区分。我在一个评论列表里做过实测,同样 32px 尺寸下,Blobs 的"可区分度"明显好于细节型风格,滚动时不会让人眼睛累。
第三个是和界面的冲突。Blobs 默认色彩偏柔和,形状没有尖锐外角,放在浅色卡片、深色侧栏、甚至渐变背景上都不打架。你要是做过那种"头像把整个 UI 的视觉重心抢走"的页面,就知道这点有多重要。所以我的默认推荐是:匿名、协作者、占位、系统生成类场景一律 Blobs;只有需要表达"这个人是谁"的正式资料页,才考虑换更具体的风格。
2. 参数全景:把每个旋钮拆开看
2.1 公共参数速查表
Blobs 复用了核心库的一大票公共参数,加上自己的少量专属项。我按"改了有明显视觉变化"和"改了只是微调"分成两组,整理成下面这张表,方便你对着调。
| 参数 | 取值范围 | 默认 | 实际作用 | 我的使用频率 |
|---|---|---|---|---|
seed | 任意字符串 | 随机 | 决定形状与配色抽样 | 必用 |
size | 1–256 | 依调用方式 | 输出画布像素宽高 | 常用 |
backgroundColor | 6 位十六进制数组 | 内置调色板 | 背景色池,按 seed 抽一个 | 常用 |
backgroundType | solid/gradientLinear/gradientRadial | solid | 背景填充方式 | 常用 |
backgroundRotation | 0–360 | 0 | 渐变角度 | 偶尔 |
radius | 0–50 | 0 | 画布圆角百分比 | 常用 |
scale | 0–100 | 100 | 图形主体相对画布的缩放 | 常用 |
rotate | -360–360 | 0 | 整体旋转角度 | 偶尔 |
flip | 布尔 | false | 水平镜像 | 少用 |
translateX/translateY | -100–100 | 0 | 主体平移 | 少用 |
randomizeIds | 布尔 | false | 随机化 SVG 内部 id | 多图时必开 |
有一点必须强调:backgroundColor传的是不带井号的六位十六进制字符串数组,不是单个字符串,也不是带#的写法。我见过太多次写成['#b6e3f4']结果整个背景被忽略的情况。数组的含义不是"渐变停靠点",而是"候选池"——每次生成会从里面抽一个,具体抽哪个同样由 seed 决定。想固定颜色就只放一个元素。
2.2 scale 与几何感:最容易调过头的一个参数
scale是我认为 Blobs 里最值得单独拿出来讲的参数,因为它直接决定了头像"松"还是"满"。取 100 时,图形基本铺满画布,边界可能贴着边缘;取 60 到 80 时,四周留出呼吸空间,视觉上更干净;低于 50 就开始显得小而空,除非你就是要那种极简的小点感。
我的经验配比是这样:列表页 32px 小头像用scale=100,因为尺寸小,留白会让形状显得更碎;详情页或资料卡上 80px 以上的头像用scale=85左右,留白能凸显轮廓的圆润感;如果外面还套了一圈边框或背景容器,就再往下压到 75,避免主体和边框贴太紧。这个不是玄学,你拿同一个 seed 分别导 100、85、75 三张,摆在同一屏里对比,差距一眼就能看出来。
顺带说radius。它是给整个方形画布切圆角的,0 到 50 对应 0% 到 50% 圆角,取 50 就是正圆裁切。如果你的设计里头像一律是圆形,别在 CSS 里加border-radius: 50%,直接在生成时给radius=50更稳——因为 CSS 裁切是在渲染后做的,而生成时裁切意味着 SVG 自身就是圆的,导出 PNG 也不会带方角。这一条我在导出场景里踩过,用 CSS 裁切导出的图片四角是白的。
2.3 三套可以直接抄的参数配方
配方一,评论区小头像:seed用用户标识,size=64,radius=50,scale=100,backgroundColor给一组同色系的浅色,backgroundType=solid。这套的特点是对比度高、辨识快,滚动浏览时不刺眼。
配方二,资料页大头像:size=240,radius=50,scale=85,backgroundType=gradientLinear,backgroundRotation=45,backgroundColor给两到三个相近色。渐变在这里的作用不是炫技,而是让大尺寸头像不显得太平——纯色铺满 240px 会有轻微的"塑料感",加一层低对比渐变就活了。
配方三,占位骨架头像:size=48,radius=50,scale=70,backgroundType=solid,backgroundColor只给一个接近界面底色的灰。这个用在新数据加载、被删除用户、系统消息这类场景,重点是"有但不抢"。
提示:三套配方里的
scale差异不是随手写的。尺寸越大,主体越应该往里收,否则大图上色块的边界会显得廉价;尺寸越小,越要铺满,否则小图上全是留白。
3. 配色方案:从撞色到可读
3.1 backgroundColor 与 backgroundType 的组合逻辑
配色这块,很多人以为backgroundColor是控制"图形颜色"的,其实它控制的是背景。图形主体的颜色跟随 seed 从风格内置的调色板里取,你没法直接指定某一个 blob 的填充色。理解这一点很关键,因为它决定了你的配色策略不是"我要让这个头像变蓝",而是"我要让这堆头像整体落在什么色调里"。
所以正确姿势是:用backgroundColor给背景设一个低饱和、低对比的底色池,让主体色块跳出来;而不是把背景设成和主体相近的高饱和色,那样会糊成一片。我一般把背景色的明度控制在 90% 以上(偏白)或 15% 以下(偏黑),饱和度压在 30% 以内。这样无论主体抽到什么颜色,都不会出现"背景吃掉图形"的情况。
backgroundType的三种取值各有适用面。solid最稳,任何场景都不会出错,也最好压缩(纯色填充的 SVG 体积更小)。gradientLinear适合大尺寸和强调位,backgroundRotation控制渐变方向,我常用的值是 45 和 135,斜向渐变比水平垂直更有动感。gradientRadial我用得最少,它适合做"聚光"效果,比如让头像看起来像被一束光打亮,但小尺寸下几乎看不出来,性价比低。
3.2 用 HSL 搭一套可控调色板
直接凭感觉写十六进制很容易翻车,我的做法是先在 HSL 空间里定规则,再转成十六进制。具体是三步。
第一步定色相。挑 6 到 8 个色相,均匀分布在色环上,比如 0、40、80、160、200、260、320。均匀分布能保证任意两个 seed 抽到的背景色有明显差异,降低"两个用户看起来一样"的概率。
第二步定饱和度和明度。浅色主题下固定S=55%, L=88%,深色主题下固定S=40%, L=22%。固定 S 和 L 的好处是整组颜色的"重量感"一致,不会出现某个背景特别扎眼。
第三步转十六进制并去掉井号。这一步手算太痛苦,写个小脚本一次生成,存成常量数组,后面所有地方复用。
配色定完之后还要验一遍对比度,因为头像里的图形色是随机抽的,你控制不了,但至少背景和页面文字、背景和头像边框的对比要能算。相对亮度的算法是先把每个通道归一化到 0–1,做伽马校正(小于等于 0.03928 的除以 12.92,否则取((c+0.055)/1.055)^2.4),再按 0.2126、0.7152、0.0722 加权求和。对比度就是两个亮度加 0.05 后相除。图形元素和界面元素的目标是 3:1,文字是 4.5:1。我写过一个二十行的小函数跑一遍全部背景色对,把不达标的剔掉,比肉眼靠谱得多。
3.3 深色模式与主题切换的落地方式
主题切换这件事,最容易犯的错是"用同一组背景色硬套两套主题"。浅色底上好看的浅粉,放到深色底上就是一块发光的疤。正确做法是维护两套backgroundColor数组,浅色主题用高明度低饱和,深色主题用低明度中等饱和,通过主题状态切换传入。
这里有个性能细节:如果头像组件是服务端渲染的 SVG 字符串,主题切换时不能只改 CSS 变量,因为背景色已经写死在 SVG 内部了。要么在客户端根据主题重新生成一遍(成本可接受,一次生成耗时在毫秒级),要么生成两份缓存起来切换。我选前者,因为切换频率低,且不用管缓存失效。另外深色模式下我一般会把scale再压 5 个单位,深色背景会让色块显得更"膨胀",收一点更舒服。
还有个小技巧,如果产品有品牌主色,可以在backgroundColor池里塞一到两个品牌色的极浅版本(比如品牌色明度提到 92%),这样一部分头像会天然带一点品牌调性,整体看起来不像完全随机的。比例别太高,控制在八分之一左右,否则会显得刻意。
4. 动画开关:静态 SVG 怎么动起来
4.1 先明确一件事:Blobs 本身没有动画开关
我把话说死一点:Blobs 的选项定义里没有animated这类字段,它产出的就是一段静态 SVG。如果你在别处看到"给 Blobs 打开动画"的说法,先确认版本和风格,至少在我核对过的版本里,Blobs 是纯静态的。所以"动画开关"这件事,本质是在应用层自己做一层控制,而不是调一个参数。
这一点想清楚之后,方案就很多了。核心思路是:把 SVG 里需要动的元素包一层<g>,用 CSS 或 SMIL 驱动它的transform,再加一个布尔开关控制动画类名是否挂载。为什么强调用transform而不是路径变形?因为 Blobs 每次生成的路径点数不一定相同,直接对d属性做形变动画会因为点数不匹配而失败或者跳帧。用整体的缩放、旋转、轻微位移,任何形状都能安全地动起来,这是最省事也最稳的路子。
4.2 CSS 方案:给路径加一层呼吸
我常用的是下面这段。先把 SVG 内联到 DOM 里(不是<img>,<img>里的 SVG 会被隔离,外部 CSS 打不进去),然后给目标元素挂类名。
.blob-avatar svg { overflow: visible; transform-box: fill-box; transform-origin: center; } .blob-avatar.is-animated svg > path, .blob-avatar.is-animated svg > g { transform-box: fill-box; transform-origin: center; animation: blob-breathe 7s ease-in-out infinite alternate; } @keyframes blob-breathe { 0% { transform: scale(1) rotate(0deg); } 55% { transform: scale(1.04) rotate(2.5deg); } 100% { transform: scale(1.015) rotate(-1.5deg); } }这里有个必须写的属性:transform-box: fill-box。不写它,SVG 元素的transform-origin会按整个 SVG 视口的坐标系算,center就变成了画布中心而不是图形中心,结果就是图形绕着某个奇怪的支点甩动。我第一次做这个的时候排查了半小时,最后发现就差这一行。同理overflow: visible也很重要,缩放后超出 viewBox 的部分默认会被裁掉,看起来就像图形被"啃"了一口。
时长和幅度也要克制。7 到 10 秒、缩放幅度 3% 到 5%、旋转 2 到 3 度,这个区间内动效是"呼吸感";低于 4 秒会变成烦躁的抖动,幅度超过 8% 会让人分心。我试过 3 秒加 10% 缩放的激进版本,放在列表页里,整屏都在晃,看起来像出了渲染 bug。
4.3 开关设计:把"动不动"做成可配置项
既然是自己加的动画,开关就得自己设计。我的做法是在组件配置里放一个animation字段,支持三种取值:none(不动)、breathe(呼吸)、hover(悬停才动)。none用在批量列表里,几十个头像同时呼吸会让人难受;breathe只用在页面上的唯一主角位,比如个人资料卡顶部;hover是折中方案,默认静止,鼠标移上去才开始,兼顾了活跃感和安静。
还有一层系统级的开关必须尊重:prefers-reduced-motion。有些用户在前端环境里关闭了动效,这时候你硬上动画就是打扰。
@media (prefers-reduced-motion: reduce) { .blob-avatar.is-animated svg > path, .blob-avatar.is-animated svg > g { animation: none; } }配合hover模式还有个小细节:悬停动画要用transition而不是无限animation,不然鼠标移开动画还在跑。我的写法是默认状态给一个transform: scale(1),:hover里给scale(1.05),加上transition: transform .35s ease。便宜、够用、不失控。
4.4 性能与体积:什么规模下会翻车
SVG 动画的渲染成本主要在重绘。如果动画只是transform和opacity,现代浏览器能走合成层,基本不触发重排,性能很好。但你要是不小心动了filter、d、fill这类属性,每帧都要重算光栅,几十个一起跑,中低端设备立刻掉帧。
我做过一个粗略测试:同一屏放 40 个带动画的 Blobs 头像,纯transform版本滚动帧率稳定;把动画换成filter: blur()的发光效果后,帧率明显下滑。结论很明确——动transform,别动filter,也别用drop-shadow做呼吸光晕。
另一个容易被忽略的是will-change。很多人为了"优化"给每个头像都加will-change: transform,结果几十个合成层全驻留内存,页面反而更卡。我的原则是只在悬停前一刻加,或者干脆不加,靠浏览器自己判断。
5. 实战:封装一个能长期用的头像组件
5.1 配置对象设计:把散落的参数收拢
直接在业务代码里到处写createAvatar(blobs, {...})是灾难的开始,因为参数会散、默认值会不一致、升级时会到处改。我一般先定一个配置对象,把"业务语义"映射成"生成参数"。
// avatar-config.js export const AVATAR_PRESETS = { list: { size: 64, radius: 50, scale: 100, backgroundType: 'solid', palette: 'light', }, profile: { size: 240, radius: 50, scale: 85, backgroundType: 'gradientLinear', backgroundRotation: 45, palette: 'light', }, placeholder: { size: 48, radius: 50, scale: 70, backgroundType: 'solid', palette: 'neutral', }, }; export const PALETTES = { light: ['f3e8ff', 'e0f2fe', 'dcfce7', 'fef3c7', 'ffe4e6', 'ede9fe'], neutral: ['e5e7eb'], dark: ['1f2937', '1e293b', '27272a', '312e81', '1e3a5f'], };这样业务侧只需要说"我要 list 预设、浅色主题",不用关心scale是 85 还是 100。换主题、换配方都在一处改。这套结构我用了好几个项目,最大的价值不是省代码,而是让"视觉决策"和"业务调用"分开了,视觉调整不需要动业务逻辑。
5.2 生成与缓存:优先用 Data URI 而不是内联字符串
组件里怎么渲染,有两个选择:把 SVG 字符串内联进 DOM,或者转成 Data URI 塞进<img>。我默认选后者,原因有三。
第一是 id 冲突。带渐变的 SVG 内部会定义linearGradient,每个都带 id。同页面放多个内联 SVG,如果 id 重复,浏览器会按"第一个出现的"来解析引用,结果就是所有头像的渐变全变成同一个颜色。这个问题极为隐蔽,表现是"颜色看起来串了"但代码完全正确。解法有两个:生成时开randomizeIds让 id 随机化,或者干脆用 Data URI 隔离每个实例。我两个都用,双保险。
第二是浏览器缓存。Data URI 放进<img src>后,同一 URL 会被缓存,重复渲染几乎零成本。内联字符串每次都要走一遍 DOM 解析。
第三是减少 DOM 节点。一个 Blobs SVG 里有几十个节点,一百个头像就是几千个节点,内联会明显拖慢首屏。
代码大概长这样:
import { createAvatar } from '@dicebear/core'; import { blobs } from '@dicebear/collection'; const cache = new Map(); export function getAvatarDataUri(seed, preset) { const key = `${seed}::${preset.name}`; if (cache.has(key)) return cache.get(key); const avatar = createAvatar(blobs, { seed, size: preset.size, radius: preset.radius, scale: preset.scale, backgroundColor: preset.colors, backgroundType: preset.backgroundType, backgroundRotation: preset.backgroundRotation ?? 0, randomizeIds: true, }); const uri = avatar.toDataUri(); cache.set(key, uri); return uri; }内存缓存的 key 一定要把预设名带上,否则同一个 seed 在不同预设下会互相覆盖,表现出来就是"列表页头像跑到资料页还是小的"。这个 bug 我实打实写过一次。
5.3 批量预生成与服务端分流
如果头像量大(比如一次要渲染几百条列表),逐个在客户端生成还是会卡。这时候两种分流方案。
一是构建时预生成。把已知的固定头像(系统账号、默认占位、运营位)在构建阶段跑一遍,产出静态文件或写进常量表,运行时零计算。
二是服务端预生成加长缓存。每次生成后把 SVG 存起来(对象存储或者构建产物都行),用seed + 参数哈希当文件名,配上很长的缓存头。这样重复请求基本命中缓存,只有新用户才真算一次。注意这里要锁定风格版本,否则升级后新旧不一致——后面第 6 章会说。
三是 HTTP 接口直连。如果不想自己部署,直接按路径拼参数走公开接口即可,形如/9.x/blobs/svg?seed=yourKey&size=128&backgroundColor=f3e8ff,e0f2fe&backgroundType=gradientLinear&backgroundRotation=45&radius=50&scale=85。参数名和库里的选项名基本一一对应,多值用逗号分隔。这条路径适合原型和内部工具,正式产品还是建议自己生成,可控性更好。
6. 常见问题与排查实录
6.1 症状速查表
下面这张表是我和同事这两年攒下来的,基本覆盖了九成以上的"看起来不对"。
| 症状 | 最可能的原因 | 处理方式 |
|---|---|---|
| 背景色完全没生效 | 颜色带了#,或传了字符串而非数组 | 去掉井号,改成六位十六进制数组 |
| 同页多图颜色串成一锅 | SVG 内部 id 重复 | 开randomizeIds,或改用 Data URI 渲染 |
| 图形边缘被切掉 | scale太大,或容器overflow: hidden | 调低scale,给 SVG 加overflow: visible |
| 圆角不生效 | 只在 CSS 里加了border-radius | 改用生成参数radius=50 |
| 动画绕着奇怪支点甩 | 缺transform-box: fill-box | 补上该属性 |
| 动画完全不跑 | SVG 用<img>引入,外部 CSS 打不进去 | 改内联,或用 SMIL 写在 SVG 内部 |
| 升级后头像全变了 | 风格实现调整,同 seed 输出不同 | 锁定版本,或落地时存快照 |
| 中文 seed 生成异常 | URL 未编码 | 用encodeURIComponent处理 |
| 打包体积暴涨 | 整包引入全部风格集合 | 只引入blobs单个风格 |
6.2 几个我实打实踩过的坑
第一个是版本锁定。有一回升级依赖后,测试同学跑来说"所有用户头像都变了,像换了一批人"。查下来就是风格实现微调导致同 seed 输出不同。这个问题的严重性在于它是静默的,接口不报错、页面不崩,只是视觉变了,如果头像承担了部分身份识别功能,用户会明显感到困惑。我的解法是三点:生产环境把版本号写死在依赖里,不用范围符;关键位置的头像在服务端存一份 SVG 快照,作为兜底;升级前先跑一次"同 seed 对比",把差异截图出来评审。这三条现在成了我们的标准动作。
第二个是seed的选择。别用会变的东西当 seed,比如昵称、邮箱、时间戳。用户改个名字头像就换一张,体验很怪。用稳定不变的业务标识,并且给不同业务域加前缀(比如user:123、team:456),避免不同实体的编号撞车出现同一张脸。
第三个是采样分布。Blobs 的形状差异其实不算特别大,如果你有几千个用户,一定会有"看起来很像"的。想提升区分度,最有效的手段不是调形状参数,而是把背景色的候选池做大——颜色差异比形状差异更容易被人眼捕捉。我从 4 色扩到 8 色之后,误认率明显下降。另外可以在同一个 seed 上做二次偏移,比如用seed + '-v2'生成第二张,让系统账号和普通用户落在不同分布里。
第四个是无障碍。头像这种东西很容易被读屏软件念成一串无意义的字符,或者干脆把 SVG 当内容朗读。我的处理是:装饰性头像一律加空alt或者aria-hidden="true",让读屏跳过;真正承载身份信息的头像,用外层容器加aria-label写清楚是谁,别指望 SVG 自己表达。
7. 最后一点个人体会
这套方案我从最早的直接拼链接,到后来自己封装组件、做预生成、加动画开关,前后迭代了三四轮。最大的感受是:Blobs 这种"低信息量"头像的价值,不在于它多好看,而在于它把"这个人是谁"的表达成本压到了最低——你不用挑图、不用审核、不用考虑版权,一行 seed 就有一张稳定的脸。
真正需要花心思的地方全在周边:配色池要设计得能收敛整页视觉,缓存策略要撑得住列表页的并发量,动画要克制到不打扰人,版本要锁死到不会自己变脸。这几件事做扎实了,它就是一个几乎零维护的基础设施;做得糙,它就会变成"颜色老是串""头像怎么又变了"这类反复冒出来的问题单。
如果只让我留一条经验,那就是:先把seed的稳定性和版本锁定想清楚,再谈配色和动画。前者决定了这套东西能不能当基础设施用,后者只决定它好不好看。顺序搞反了,返工成本会大得多。