Compare commits

...
Author SHA1 Message Date
jared d55a8ba8a5 Merge branch 'lotus' into fix/animated-bg-compositor
CI / Build & Quality Checks (pull_request) Successful in 1m52s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 9s
CI / Playwright smoke (e2e) (pull_request) Successful in 11m11s
2026-10-16 19:59:31 -04:00
Lotus CIandClaude Opus 5.5 2700aad209 perf(backgrounds): animated chat backgrounds cost ~5% CPU instead of 50 to 70% (#337)
CI / Build & Quality Checks (pull_request) Successful in 1m57s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 9s
CI / Playwright smoke (e2e) (pull_request) Failing after 9m56s
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-16 19:05:52 -04:00
6 changed files with 15 additions and 10 deletions
@@ -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 };
@@ -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`,
},
};
@@ -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`,
},
};
@@ -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`,
},
};
@@ -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`,
},
};
+5
View File
@@ -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' };
}