CSS 渐变的工作原理,从线性到锥形

2026-02-14设计工具
分享到

CSS 渐变看起来很简单,写几行代码浏览器就能渲染出平滑的过渡效果。但我在实际项目中踩过不少坑——渐变条带、颜色发灰、不同浏览器颜色不一致、大面积渐变掉帧。这些问题的根源都在渐变的工作原理上。这篇文章把 CSS 渐变的渲染机制拆开来讲。

浏览器如何渲染渐变

CSS 渐变本质上是一套数学描述。你写一个 linear-gradient,浏览器不会去加载图片,而是在 GPU 上实时计算每个像素的颜色值。

渲染流程大致分三步:

  1. 解析 gradient 函数,构建颜色停止点(color-stop)列表
  2. 根据渐变类型和方向参数,确定每个像素在颜色停止点之间的位置
  3. 在颜色空间中对 RGB 通道做线性插值,写出最终像素值

理解了这个流程,那些诡异的问题就都能解释了。

三种渐变的渲染差异

CSS 支持三种核心渐变类型,它们的渲染差异在于第二步——如何把像素坐标映射到渐变进度。

linear-gradient 的方向系统

linear-gradient 的方向参数有两种写法,我以前一直混着用,直到出现 bug 才发现角度理解错了。

关键词写法:

linear-gradient(to bottom, red, blue)
linear-gradient(to top right, red, blue)

to bottom 表示渐变从顶部到底部,也就是 180deg 方向。to top right 指向右上角,角度取决于元素宽高比——不是固定的 45deg。

角度写法:

linear-gradient(45deg, red, blue)
linear-gradient(0.25turn, red, blue)

deg 的单位是度,0deg 指向正上方(相当于 to top),顺时针递增。turn 是圈数,0.25turn 等于 90deg。

角度的计算起点是正上方,不是数学坐标系里的正右方。这是我犯过的错——写 0deg 想表示水平方向,结果浏览器给我一个上下渐变。CSS 的角度起点沿用了钟表习惯,12 点钟方向为 0,顺时针走。

角度映射关系:

  • 0deg / 360deg → to top
  • 45deg → to top right
  • 90deg → to right
  • 135deg → to bottom right
  • 180deg → to bottom
  • 225deg → to bottom left
  • 270deg → to left
  • 315deg → to top left

linear-gradient 角度示意图

理解这个之后,写对角线渐变心里就有底了。

radial-gradient 的形状与位置

radial-gradient 以某一点为中心向外扩散。它的核心参数是形状(shape)和范围(size)。

radial-gradient(circle, red, blue)
radial-gradient(ellipse at center, red, blue)
radial-gradient(circle at 30% 40%, red, blue)

默认形状是 ellipse——如果容器是矩形,渐变会拉伸成椭圆。指定 circle 则强制为正圆。

at 关键词控制中心点位置,默认是 center。可以写百分比、px 或者关键词组合。

size 参数的坑:

radial-gradient(circle closest-side, red, blue)
radial-gradient(circle farthest-corner, red, blue)

closest-side 让渐变范围刚好覆盖到离中心最近的边。farthest-corner 覆盖到最远的角。默认值是 farthest-corner

我在一个圆形容器里用了 radial-gradient,默认 farthest-corner 导致颜色还没扩散到边缘就结束了,整个渐变看起来像被裁切。改成 closest-side 就正常了。

conic-gradient 的旋转过渡

conic-gradient 是较晚加入的渐变类型。它不是沿直线或径向扩散,而是绕中心点旋转,颜色从 0 度到 360 度逐度过渡。

conic-gradient(from 0deg, red, blue, red)
conic-gradient(from 90deg at 30% 40%, #4F46E5, #F97316, #10B981, #4F46E5)

from 指定起始角度,at 指定中心点位置。

锥形渐变最常见的用途是制作色相环、饼图和环形进度条。我之前用它做了一个转盘抽奖的配色,代码只有 20 行,视觉效果很好。

.conic-demo {
  width: 200px;
  height: 200px;
  border-radius: 50%;
  background: conic-gradient(
    from 0deg,
    #4f46e5 0deg 90deg,
    #f97316 90deg 180deg,
    #10b981 180deg 270deg,
    #6b7280 270deg 360deg
  );
}

这种写法利用了 color-stop 可以指定两个位置(起点和终点),效果是四个颜色各占四分之一圆,没有过渡。

三种 CSS 渐变类型对比:线性、径向、锥形

Color Stop 的语法和间距计算

Color stop 是渐变的核心。每个停止点由颜色和位置组成。浏览器根据这些停止点计算中间色值。

linear-gradient(red 0%, blue 100%)

如果只写颜色不写位置,浏览器会自动分配。两个停止点就是 0% 和 100%,三个停止点就是 0%、50%、100%,以此类推。

linear-gradient(red, blue)           /* red 在 0%, blue 在 100% */
linear-gradient(red, yellow, blue)   /* red 0%, yellow 50%, blue 100% */

多位置停止点:

CSS 允许一个颜色写多个位置,实现硬边过渡:

linear-gradient(
  red 0%, red 30%,
  blue 30%, blue 70%,
  green 70%, green 100%
)

这个渐变在 30% 和 70% 位置是突变,不是平滑过渡。用在进度条、分段指示器上很合适。

间距的插值计算:

当停止点位置不连续时,浏览器按比例插值。比如:

linear-gradient(red 0%, blue 40%, green 100%)

红色到蓝色的过渡占 0%-40% 这段,蓝色到绿色占 40%-100% 这段。如果在中间某个位置(比如 20%),它落在红色到蓝色区间内,颜色值按 20%/40% = 50% 的比例插值。

Color Stop 插值过程示意图

颜色插值空间:sRGB 与 oklch

这是最容易被忽视的部分,也是导致渐变"发灰"的根本原因。

默认情况下,CSS 渐变在 sRGB 颜色空间 做线性插值。sRGB 的 Gamma 曲线不是线性的,导致中间色在视觉上偏暗、偏灰。红到蓝的渐变中间会出现一块灰紫色。

/* 默认 sRGB 插值 */
linear-gradient(red, blue)
/* 中间点是 rgb(128, 0, 128) 灰紫色 */

CSS Color Level 4 引入了 color-interpolation 控制插值空间。在支持的浏览器里可以显式指定:

linear-gradient(in oklch, red, blue)
linear-gradient(in srgb, red, blue)
linear-gradient(in hsl, red, blue)

oklch 空间的优势:

oklch 是专门为感知一致性设计的颜色空间。在 oklch 中插值,中间色的亮度变化更均匀,不会出现 sRGB 那种发灰的问题。红到蓝的渐变在 oklch 中会经过更饱满的紫色,视觉效果好很多。

我对比过这两者的渲染结果,差别非常明显——尤其在渐变跨度大或者涉及亮度差异大的颜色时。Safari 对 oklch 的支持目前最好,Chrome 紧跟其后。

各浏览器的插值差异:

实际工作中我发现不同浏览器渲染同一个渐变,颜色可能不太一样。主要原因:

  1. 浏览器在 sRGB 空间中做插值,但 Gamma 校正的精度不同
  2. 字体渲染抗锯齿算法差异,影响边缘颜色混合
  3. color-interpolation 的支持程度不同

Chrome 和 Firefox 在 sRGB 下的结果基本一致。Safari 有时会做额外的色彩管理,导致颜色略有偏差。最稳妥的做法是手动指定关键停止点,减少浏览器自行插值的区间。

可以在 CSS 渐变工具 中实时对比不同颜色空间下的渐变效果,也可以用 颜色转换工具 把颜色值在不同格式间互转,方便调试。

重复渐变

CSS 还提供了三种重复渐变:repeating-linear-gradientrepeating-radial-gradientrepeating-conic-gradient

repeating-linear-gradient(
  45deg,
  #4F46E5 0px, #4F46E5 10px,
  #F97316 10px, #F97316 20px
)

这段代码生成 45 度的条纹图案,每个条纹 10px 宽。浏览器识别到最后一个停止点位置是 20px,就自动以 20px 为周期向后平铺。

重复渐变的原理和普通渐变一样,区别在于浏览器会计算周期长度,然后沿渐变方向无限重复。周期长度由最后一个 color-stop 的位置决定。

一个容易忽视的细节:

如果最后一个停止点没有显式指定位置,浏览器无法确定周期长度,重复渐变不会生效。必须保证最后一个停止点有明确的位置值。

重复渐变非常适合制作棋盘格、条纹、波点等背景图案。纯 CSS 实现,不依赖图片加载。

浏览器渲染的硬件加速

CSS 渐变的渲染依赖浏览器的栅格化引擎。现代浏览器会把渐变渲染任务交给 GPU 来完成,这就是硬件加速。

什么时候触发硬件加速:

  • will-change: transformtransform: translateZ(0) 提升为合成层
  • 渐变用于 background-image 时,大多数浏览器会自动合成
  • 渐变配合 CSS 动画使用时,如果动画属性是 transform 或 opacity,走合成线程

什么时候会掉帧:

大面积渐变加上位置变化动画,导致浏览器需要不断重新栅格化。渐变本身没有位图缓存,每次变化都要重新计算所有像素。

之前做了一个全屏渐变背景的页面,加了淡入淡出动画,在移动设备上帧率掉到 20fps。后来发现是渐变面积太大,每次变化都要重新渲染整个视口。改成静态渐变加上内容层动画,帧率就正常了。

渐变与图片的性能对比

CSS 渐变和对应的图片实现相比,有几个重要差异:

文件体积:

渐变是 CSS 代码,通常不到 1KB。图片即使压缩也要几 KB 到几十 KB。对于图标、按钮背景这类小面积场景,CSS 渐变优势明显。

加载时机:

渐变在 CSS 解析后立即渲染,没有网络请求。图片需要 HTTP 请求,即使在 HTTP/2 下也有延迟。渐变更适合首屏内容的背景。

渲染开销:

纯色和简单渐变(两个停止点)的 GPU 渲染开销很小。复杂渐变(大量停止点、多重重叠渐变)会增加渲染时间。图片的渲染开销取决于尺寸和压缩格式,固定后就是常数。

抗锯齿:

渐变的边缘抗锯齿由浏览器的渲染引擎处理,质量普遍不错。图片缩放时可能出现锯齿或模糊。

内存占用:

渐变不占用额外内存,渲染时实时计算。图片需要解码后占用显存,一张 1920x1080 的 32 位 PNG 约 8MB。

总的来说,小面积、简单的渐变用 CSS 实现更高效。大面积复杂纹理用图片更合适——CSS 渐变的渲染时间随面积线性增长,而图片的解码时间是固定的。

CSS 渐变背景的实用技巧

积累了一些实际项目中的用法:

多重渐变叠加:

一个 background 可以叠多个渐变,用逗号分隔。前面的渐变覆盖后面的:

.multi-bg {
  background:
    linear-gradient(45deg, rgba(79, 70, 229, 0.3), transparent),
    radial-gradient(
      circle at right bottom,
      rgba(249, 115, 22, 0.2),
      transparent
    ),
    linear-gradient(180deg, #f9fafb, #e5e7eb);
}

这种叠加可以实现丰富的纹理效果,不用额外 DOM 元素。

硬边条纹:

.stripes {
  background: repeating-linear-gradient(
    90deg,
    #4f46e5 0px,
    #4f46e5 2px,
    transparent 2px,
    transparent 8px
  );
}

2px 宽的紫色竖线,间隔 6px 透明。调整颜色、宽度和方向可以做出各种条纹。

渐变模拟阴影:

box-shadow 做不出复杂的多层阴影效果,但渐变可以模拟。比如用径向渐变做一个光晕效果:

.glow {
  background: radial-gradient(
    ellipse at 50% 100%,
    rgba(79, 70, 229, 0.15),
    transparent 70%
  );
}

这个技巧可以用在卡片底部制造发光效果,比 box-shadow 更可控。船长工具箱的 CSS 阴影工具 可以帮你调试这类效果。

渐变做遮罩:

.fade-edge {
  mask-image: linear-gradient(
    to right,
    transparent,
    black 20%,
    black 80%,
    transparent
  );
  -webkit-mask-image: linear-gradient(
    to right,
    transparent,
    black 20%,
    black 80%,
    transparent
  );
}

让内容在左右边缘渐变消失,常用于长文本容器的两端 fade-out 效果。

用渐变生成占位图:

在图片加载完成前,用渐变作为低质量占位符(类似 LQIP 的思路)。复杂度不高,但能显著改善感知加载速度。

.placeholder {
  background: linear-gradient(135deg, #e5e7eb 0%, #d1d5db 50%, #e5e7eb 100%);
  background-size: 200% 200%;
  animation: shimmer 1.5s ease-in-out infinite;
}

@keyframes shimmer {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}

这就是常见的 skeleton screen 闪烁效果。

实际项目中的坑

渐变色带(banding):

当渐变在较窄的范围内跨度很大时,浏览器色深不够(8bit/通道),相邻像素的色值差异肉眼可见,形成条纹。

解决办法:在渐变中增加中间停止点,给浏览器更多参考色。或者改用 oklch 插值空间,色值分布更均匀。

颜色发灰:

我在做一个紫色到橙色的渐变背景时,中间区域出现了一块难看的灰褐色。这是 sRGB 插值导致的。色相在色环上的路径经过了低饱和度区域。改成 in oklch 之后问题消失。

渲染不一致:

同一个渐变色值,在 Chrome 和 Firefox 里表现一致,到了 Safari 会偏亮或偏暗。因为 Safari 的系统色彩管理更严格。

我的做法:在渐变的 25%、50%、75% 位置手动添加中间色,减少浏览器自行插值的自由度。三层渐变改为五层,渲染一致性明显提升。

总结

CSS 渐变的核心是颜色停止点的管理和颜色空间的插值计算。角度参数从 12 点方向顺时针计数,径向渐变的形状和范围决定扩散方式,锥形渐变适合色相环和饼图。重复渐变是周期性平铺,需要最后一个停止点有明确位置。

性能方面,简单渐变依赖 GPU 硬件加速,比图片加载快但面积大时渲染开销高。颜色插值默认用 sRGB,跨光谱大渐变建议用 in oklch 避免发灰。

理解这些原理后,调试渐变问题会快很多,写代码时也能预判浏览器行为。