From 2700aad20973f6c44c76cccaf850a95937f6fbe8 Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Fri, 16 Oct 2026 19:05:52 -0400 Subject: [PATCH] perf(backgrounds): animated chat backgrounds cost ~5% CPU instead of 50 to 70% (#337) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit background-position can't run on the compositor, so every frame of the animated chat backgrounds repainted the whole chat area: 51 to 72% main-thread busy while idle in a room at 60 fps. The drifts are slow (3 to 22 px/s, under a pixel per frame), so each animation now uses steps(): about 12 updates/s for fireflies, stars and aurora and about 24/s for rain and pulse. Measured headless (20 s idle in a room, CDP TaskDuration): none 0.7% → 0.9%; rain 50.7 → 5.9; stars 71.2 → 4.5; pulse 64.9 → 5.8; aurora 58.9 → 4.2; fireflies 71.7 → 3.8. Aurora and pulse used ease-in-out; with steps() their motion is now even-paced. Pause animations / reduced motion still strip the animation. Fixes #337 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5 --- src/app/features/lotus/backgrounds/animAurora.ts | 4 ++-- src/app/features/lotus/backgrounds/animFireflies.ts | 4 ++-- src/app/features/lotus/backgrounds/animPulse.ts | 4 ++-- src/app/features/lotus/backgrounds/animRain.ts | 4 ++-- src/app/features/lotus/backgrounds/animStars.ts | 4 ++-- src/app/features/lotus/chatBackground.ts | 5 +++++ 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/app/features/lotus/backgrounds/animAurora.ts b/src/app/features/lotus/backgrounds/animAurora.ts index 06385cdbd..98b77da9d 100644 --- a/src/app/features/lotus/backgrounds/animAurora.ts +++ b/src/app/features/lotus/backgrounds/animAurora.ts @@ -51,7 +51,7 @@ const dark: CSSProperties = { backgroundSize: '260% 240%, 300% 260%, 240% 280%, 220% 200%, 100% 100%, 100% 100%', backgroundPosition: '0% 30%, 100% 70%, 50% 0%, 20% 80%, 50% 50%, 50% 50%', backgroundRepeat: 'no-repeat', - animation: `${auroraFlow} 60s ease-in-out infinite`, + animation: `${auroraFlow} 60s steps(180) infinite`, }; const light: CSSProperties = { @@ -75,7 +75,7 @@ const light: CSSProperties = { backgroundSize: '260% 240%, 300% 260%, 240% 280%, 220% 200%, 100% 100%, 100% 100%', backgroundPosition: '0% 30%, 100% 70%, 50% 0%, 20% 80%, 50% 50%, 50% 50%', backgroundRepeat: 'no-repeat', - animation: `${auroraFlow} 60s ease-in-out infinite`, + animation: `${auroraFlow} 60s steps(180) infinite`, }; export const animAurora: ChatBgVariants = { dark, light }; diff --git a/src/app/features/lotus/backgrounds/animFireflies.ts b/src/app/features/lotus/backgrounds/animFireflies.ts index 87d05b171..86e868c2d 100644 --- a/src/app/features/lotus/backgrounds/animFireflies.ts +++ b/src/app/features/lotus/backgrounds/animFireflies.ts @@ -55,7 +55,7 @@ export const animFireflies: ChatBgVariants = { '0 0', // wash A (static) '0 0', // wash B (static) ].join(','), - animation: `${firefliesDrift} 44s linear infinite`, + animation: `${firefliesDrift} 44s steps(528) infinite`, }, // Light: a cozy warm dim-dusk. No harsh dots on white — soft amber motes with @@ -93,6 +93,6 @@ export const animFireflies: ChatBgVariants = { '0 0', // wash A (static) '0 0', // wash B (static) ].join(','), - animation: `${firefliesDrift} 44s linear infinite`, + animation: `${firefliesDrift} 44s steps(528) infinite`, }, }; diff --git a/src/app/features/lotus/backgrounds/animPulse.ts b/src/app/features/lotus/backgrounds/animPulse.ts index fb7d1adf6..f849c9bea 100644 --- a/src/app/features/lotus/backgrounds/animPulse.ts +++ b/src/app/features/lotus/backgrounds/animPulse.ts @@ -76,7 +76,7 @@ export const animPulse: ChatBgVariants = { '0 0', // base wash '0 0', // vignette ].join(','), - animation: `${gridPulse} 22s ease-in-out infinite`, + animation: `${gridPulse} 22s steps(528) infinite`, }, // Light: soft slate-blue grid on pale cool-white, a gentle luminance breathe. @@ -116,6 +116,6 @@ export const animPulse: ChatBgVariants = { '0 0', // base wash '0 0', // vignette ].join(','), - animation: `${gridPulse} 22s ease-in-out infinite`, + animation: `${gridPulse} 22s steps(528) infinite`, }, }; diff --git a/src/app/features/lotus/backgrounds/animRain.ts b/src/app/features/lotus/backgrounds/animRain.ts index d4b742b1f..8dc1525c5 100644 --- a/src/app/features/lotus/backgrounds/animRain.ts +++ b/src/app/features/lotus/backgrounds/animRain.ts @@ -97,7 +97,7 @@ export const animRain: ChatBgVariants = { ].join(','), backgroundSize: ['260px 200px', '100% 100%', '100% 100%', '100% 100%'].join(','), backgroundPosition: ['0 0', '0 0', '0 0', '0 0'].join(','), - animation: `${rainFall} 12s linear infinite`, + animation: `${rainFall} 12s steps(288) infinite`, }, // Light: soft teal-grey streaks on a pale cool base — elegant, never neon. @@ -118,6 +118,6 @@ export const animRain: ChatBgVariants = { ].join(','), backgroundSize: ['260px 200px', '100% 100%', '100% 100%', '100% 100%'].join(','), backgroundPosition: ['0 0', '0 0', '0 0', '0 0'].join(','), - animation: `${rainFall} 12s linear infinite`, + animation: `${rainFall} 12s steps(288) infinite`, }, }; diff --git a/src/app/features/lotus/backgrounds/animStars.ts b/src/app/features/lotus/backgrounds/animStars.ts index cc3085d05..d184f4fdc 100644 --- a/src/app/features/lotus/backgrounds/animStars.ts +++ b/src/app/features/lotus/backgrounds/animStars.ts @@ -68,7 +68,7 @@ export const animStars: ChatBgVariants = { '0 0', // nebula A '0 0', // nebula B ].join(','), - animation: `${starDrift} 90s linear infinite`, + animation: `${starDrift} 90s steps(1080) infinite`, }, // Light: an airy pre-dawn sky. No literal white-on-white stars — instead very @@ -112,6 +112,6 @@ export const animStars: ChatBgVariants = { '0 0', // wash A '0 0', // wash B ].join(','), - animation: `${starDrift} 100s linear infinite`, + animation: `${starDrift} 100s steps(1200) infinite`, }, }; diff --git a/src/app/features/lotus/chatBackground.ts b/src/app/features/lotus/chatBackground.ts index 8309a0d24..1777b036a 100644 --- a/src/app/features/lotus/chatBackground.ts +++ b/src/app/features/lotus/chatBackground.ts @@ -149,6 +149,11 @@ export const getChatBg = ( } // For animated backgrounds, promote the element to its own compositor layer so // background-position keyframes don't trigger repaints on descendant elements. + // [Gitea #337] background-position can't run on the compositor, so every + // animation frame repaints the whole chat area (45 to 70% main-thread busy + // at 60 fps). The drifts are slow (3–22 px/s), so each animation uses steps(): + // ~12 updates/s for the slow ones, ~24/s for rain and pulse: visually the + // same, a fraction of the repaints. if (style.animation) { return { ...style, willChange: 'background-position', contain: 'paint' }; }