深色网站上放一片星空很常见,但把它做得「不像贴图」需要处理三件事:粒子分布不能均匀、闪烁不能同步、滚动要有纵深。更麻烦的是,它是一个全屏常驻动画,性能稍不注意就会在手机上拖垮整页。
先想清楚它是什么
星空是背景,不是主体。这意味着:
- 它的对比度必须低到不影响正文阅读;
- 它不能抢走任何一次点击(
pointer-events: none); - 它必须在低端设备上可降级,甚至完全消失也不影响信息。
明确这三点后,很多炫技的想法会自动被排除——比如流星雨、鼠标跟随光晕,它们会让背景变成主角。
分层:CSS 打底,Canvas 增强
第一层用 CSS 的径向渐变画出几十个星点,成本为零,而且没有 JS 也能看到:
.starfield::before {
content: '';
position: absolute;
inset: 0;
background-image:
radial-gradient(1px 1px at 20% 30%, #fff, transparent),
radial-gradient(1px 1px at 70% 60%, #cfe0ff, transparent),
radial-gradient(1.5px 1.5px at 40% 80%, #fff, transparent);
background-size: 320px 320px;
opacity: 0.5;
}
第二层才是 Canvas。它在首帧画好后把 CSS 层压暗,两套星点视觉上融在一起,用户感知不到切换。
粒子预算比粒子数量重要
新手最容易写 for (let i = 0; i < 1000; i++)。桌面也许扛得住,手机上就是灾难。改成按屏幕面积算:
const area = window.innerWidth * window.innerHeight;
const count = Math.min(
window.innerWidth < 768 ? 140 : 260,
Math.max(80, Math.round(area / 9000)),
);
再配合 DPR 缩放,高分屏才不糊:
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = width * dpr;
canvas.height = height * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
注意 Math.min(dpr, 2):3x 屏按 3 倍绘制只会增加像素填充成本,肉眼看不出差别。
闪烁:让每颗星有自己的周期
同一时间闪烁所有星星,看起来像屏幕在闪。正确做法是给每颗星一个随机相位和周期:
star.alpha = star.baseAlpha * (0.7 + 0.3 * Math.sin(t / star.period + star.phase));
周期取 3–8 秒,且只让约 15% 的星参与闪烁。剩下的星星保持静止,画面才有「深空」的稳定感。
视差:滚动时星层要慢半拍
视差的关键是速度差。远景位移系数 0.02,近景 0.05,滚动 1000px 时两者差 30px,刚好能感觉到纵深但不会晕。
实现上不要重新计算每颗星的位置,而是在绘制时做取模,避免滚动到底后出现空白:
const y = mod(star.y - scrollY * star.factor, height);
mod 保证结果始终在 [0, height) 内,星点从底部滚出后会从顶部回来,因为分布是随机的,看不出接缝。
省电比流畅更难
页面切到后台时,requestAnimationFrame 会被浏览器降频,但并没有完全停止。加上可见性判断更稳妥:
document.addEventListener('visibilitychange', () => {
if (document.hidden) cancelAnimationFrame(rafId);
else rafId = requestAnimationFrame(loop);
});
另外把帧率限制在 30fps:星点移动极慢,30fps 完全看不出卡顿,却能把 CPU 占用砍掉一半。
降级是设计的一部分
@media (prefers-reduced-motion: reduce) {
/* 只渲染一帧静态星空,不启动循环 */
}
有前庭功能障碍的用户会因为持续运动感到不适,这不只是「体验优化」,而是可访问性要求。同理,prefers-reduced-data 或省电模式下也可以直接关掉 Canvas 层。
最后的检查清单
- 星空脚本 gzip 后小于 6KB
- 页面隐藏时停止绘制
- 手机连续停留 3 分钟不烫手
- 关闭 JS 仍能看到星点
- 正文对比度不受影响
做到这几条,星空才算是「背景」,而不是一个拖累页面的特效。