优化聊天室特效加载与移动端性能

This commit is contained in:
2026-04-25 03:34:19 +08:00
parent 128b52d0aa
commit e3cba255f9
6 changed files with 158 additions and 25 deletions
+1 -12
View File
@@ -1,18 +1,7 @@
/**
* 文件功能:聊天室全屏特效 Vite 入口
*
* 按原 public script 顺序加载所有全屏特效模块,让生产环境由 Vite 统一压缩、加 hash 并减少请求数
* 首屏仅加载特效管理器,具体特效模块和音效模块由管理器在首次播放时按需加载
*/
import "./effects/effect-sounds.js";
import "./effects/effect-manager.js";
import "./effects/fireworks.js";
import "./effects/rain.js";
import "./effects/lightning.js";
import "./effects/snow.js";
import "./effects/sakura.js";
import "./effects/meteors.js";
import "./effects/gold-rain.js";
import "./effects/hearts.js";
import "./effects/confetti.js";
import "./effects/fireflies.js";
+5 -2
View File
@@ -92,7 +92,10 @@ const ConfettiEffect = (() => {
const w = canvas.width;
const h = canvas.height;
const DURATION = 7800;
let pieces = Array.from({ length: 90 }, () => new Piece(w, h));
const isMobile = window.matchMedia?.("(max-width: 640px)")?.matches || window.innerWidth <= 640;
const initialPieceCount = isMobile ? 56 : 90;
const spawnPieceCount = isMobile ? 6 : 10;
let pieces = Array.from({ length: initialPieceCount }, () => new Piece(w, h));
const startTime = performance.now();
let lastSpawnAt = startTime;
let animId = null;
@@ -129,7 +132,7 @@ const ConfettiEffect = (() => {
});
if (now - startTime < DURATION * 0.9 && now - lastSpawnAt >= 120) {
pieces.push(...Array.from({ length: 10 }, () => new Piece(w, h)));
pieces.push(...Array.from({ length: spawnPieceCount }, () => new Piece(w, h)));
lastSpawnAt = now;
}
+140 -6
View File
@@ -7,6 +7,24 @@
*/
const EffectManager = (() => {
// 音效模块只在首次播放特效时加载,缓存 Promise 避免重复 import
let _soundModulePromise = null;
// 各特效模块加载器,使用静态路径让 Vite 拆分为可按需加载的 chunk
const EFFECT_MODULES = {
fireworks: { key: "fireworks", load: () => import("./fireworks.js") },
"wedding-fireworks": { key: "fireworks", load: () => import("./fireworks.js") },
rain: { key: "rain", load: () => import("./rain.js") },
lightning: { key: "lightning", load: () => import("./lightning.js") },
snow: { key: "snow", load: () => import("./snow.js") },
sakura: { key: "sakura", load: () => import("./sakura.js") },
meteors: { key: "meteors", load: () => import("./meteors.js") },
"gold-rain": { key: "gold-rain", load: () => import("./gold-rain.js") },
hearts: { key: "hearts", load: () => import("./hearts.js") },
confetti: { key: "confetti", load: () => import("./confetti.js") },
fireflies: { key: "fireflies", load: () => import("./fireflies.js") },
};
// 特效模块 Promise 缓存,同类型重复触发时复用同一次加载
const _effectModulePromises = new Map();
// 当前正在播放的特效名称(防止同时播放两个特效)
let _current = null;
// 当前特效返回的取消函数,用于手动停止时真正停掉内部 RAF / 定时器
@@ -17,6 +35,8 @@ const EffectManager = (() => {
const _queue = [];
// 队列最多保留 3 个待播特效,避免高频触发后播放过期动画
const MAX_QUEUE_LENGTH = 3;
// 普通特效排队超过 10 秒后丢弃,避免播放过期动画
const QUEUED_EFFECT_TTL = 10000;
// 当前特效播放批次,用于忽略手动停止后的旧回调
let _playToken = 0;
// 是否已经绑定本轮点击停止监听
@@ -154,6 +174,16 @@ const EffectManager = (() => {
stop();
}
/**
* 页面进入后台时停止特效,避免隐藏标签页继续运行 Canvas 和音效。
*/
function _handleVisibilityChange() {
if (document.hidden) {
stop();
_queue.length = 0;
}
}
/**
* 特效结束后清理 Canvas,重置状态,并停止音效。
*
@@ -186,8 +216,8 @@ const EffectManager = (() => {
window.EffectSounds.stop();
}
if (playNext && _queue.length > 0) {
const nextType = _queue.shift();
if (playNext) {
const nextType = _dequeueNextType();
if (nextType) {
play(nextType);
}
@@ -200,17 +230,84 @@ const EffectManager = (() => {
* @param {string} type 待播放特效类型
*/
function _enqueue(type) {
const existingIndex = _queue.indexOf(type);
const existingIndex = _queue.findIndex((item) => item.type === type);
if (existingIndex !== -1) {
_queue.splice(existingIndex, 1);
}
_queue.push(type);
_queue.push({
type,
queuedAt: Date.now(),
keepUntilPlayed: type === "wedding-fireworks",
});
while (_queue.length > MAX_QUEUE_LENGTH) {
_queue.shift();
}
}
/**
* 取出下一个仍然有效的排队特效。
*
* @returns {string|null}
*/
function _dequeueNextType() {
const now = Date.now();
while (_queue.length > 0) {
const next = _queue.shift();
if (next.keepUntilPlayed || now - next.queuedAt <= QUEUED_EFFECT_TTL) {
return next.type;
}
}
return null;
}
/**
* 确保音效模块已经加载。
*
* @returns {Promise<void>}
*/
function _loadSoundModule() {
if (!_soundModulePromise) {
_soundModulePromise = import("./effect-sounds.js");
}
return _soundModulePromise;
}
/**
* 确保指定特效模块已经加载。
*
* @param {string} type 特效类型
* @returns {Promise<void>}
*/
function _loadEffectModule(type) {
const moduleConfig = EFFECT_MODULES[type];
if (!moduleConfig) {
return Promise.reject(new Error(`未知特效类型:${type}`));
}
if (!_effectModulePromises.has(moduleConfig.key)) {
_effectModulePromises.set(moduleConfig.key, moduleConfig.load());
}
return _effectModulePromises.get(moduleConfig.key);
}
/**
* 播放前加载对应特效模块和音效模块。
*
* @param {string} type 特效类型
* @returns {Promise<void>}
*/
function _loadModulesFor(type) {
return Promise.all([
_loadEffectModule(type),
_loadSoundModule(),
]).then(() => {});
}
/**
* 记录具体特效返回的取消句柄。
*
@@ -246,16 +343,51 @@ const EffectManager = (() => {
* @param {string} type 特效类型:fireworks / rain / lightning / snow / sakura / meteors / gold-rain / hearts / confetti / fireflies
*/
function play(type) {
if (document.hidden) {
return;
}
if (!EFFECT_MODULES[type]) {
console.warn(`[EffectManager] 未知特效类型:${type}`);
return;
}
// 防重入:同时只允许一个特效
if (_current) {
console.log(`[EffectManager] 特效 ${_current} 正在播放,加入队列 ${type}`);
_enqueue(type);
return;
}
const canvas = _getCanvas();
_play(type);
}
/**
* 加载模块后播放指定特效。
*
* @param {string} type 特效类型
*/
async function _play(type) {
_current = type;
const token = _playToken;
try {
await _loadModulesFor(type);
} catch (error) {
console.error(`[EffectManager] 加载特效模块失败:${type}`, error);
_cleanup({ token });
return;
}
if (token !== _playToken || _current !== type) {
return;
}
if (document.hidden) {
_cleanup({ playNext: false, token });
return;
}
const canvas = _getCanvas();
const finishCurrent = () => _cleanup({ token });
_bindClickStop();
@@ -328,6 +460,8 @@ const EffectManager = (() => {
_cleanup({ playNext: false, cancelCurrent: true });
}
document.addEventListener("visibilitychange", _handleVisibilityChange);
return { play, stop };
})();
+3 -1
View File
@@ -102,7 +102,9 @@ const GoldRainEffect = (() => {
const w = canvas.width;
const h = canvas.height;
const DURATION = 8600;
const COIN_COUNT = Math.min(58, Math.max(28, Math.floor(w / 28)));
const isMobile = window.matchMedia?.("(max-width: 640px)")?.matches || window.innerWidth <= 640;
const densityScale = isMobile ? 0.72 : 1;
const COIN_COUNT = Math.round(Math.min(58, Math.max(28, Math.floor(w / 28))) * densityScale);
const coins = Array.from({ length: COIN_COUNT }, () => new Coin(w, h));
const startTime = performance.now();
let animId = null;
+2 -1
View File
@@ -66,7 +66,8 @@ const RainEffect = (() => {
const w = canvas.width;
const h = canvas.height;
const DURATION = 8000;
const DROP_COUNT = 200; // 增加雨滴数量(原 180
const isMobile = window.matchMedia?.("(max-width: 640px)")?.matches || window.innerWidth <= 640;
const DROP_COUNT = isMobile ? 130 : 200; // 移动端降低雨线数量,避免满屏线段拖慢滚动和输入。
const drops = Array.from({ length: DROP_COUNT }, () => {
const d = new Drop(w, h);
+7 -3
View File
@@ -161,18 +161,22 @@ const SnowEffect = (() => {
const w = canvas.width;
const h = canvas.height;
const DURATION = 10000;
const isMobile = window.matchMedia?.("(max-width: 640px)")?.matches || window.innerWidth <= 640;
const densityScale = isMobile ? 0.68 : 1;
const backFlakeCount = Math.round(Math.min(120, Math.max(70, Math.floor(w / 18))) * densityScale);
const frontFlakeCount = Math.round(Math.min(64, Math.max(34, Math.floor(w / 42))) * densityScale);
const flakes = [
...Array.from(
{ length: Math.min(120, Math.max(70, Math.floor(w / 18))) },
{ length: backFlakeCount },
() => new Flake(w, h, "back"),
),
...Array.from(
{ length: Math.min(64, Math.max(34, Math.floor(w / 42))) },
{ length: frontFlakeCount },
() => new Flake(w, h, "front"),
),
];
const breezeBands = Array.from({ length: 2 }, () => ({
const breezeBands = Array.from({ length: isMobile ? 1 : 2 }, () => ({
x: Math.random() * w,
y: Math.random() * h,
radius: 180 + Math.random() * 140,