perf(immersive): fix critical CPU usage issue in background blur

Performance problem:
- WebKitWebProcess consuming 97% CPU on one core
- Cause: CSS filter: blur(120px) on large image is extremely expensive
  in WebKit/GTK which cannot GPU-accelerate this operation

Solution:
- Replace CSS blur with Canvas-based tiny thumbnail approach
- Draw image to 8x8 canvas, apply color adjustments, scale up via CSS
- Natural pixelation creates blur-like effect with ZERO CPU cost
- Remove backdrop-filter: blur() from header elements (use solid bg)

Also fixed:
- Change Mic icon to Mic2 to match rest of app
- Add missing i18n translations for player.credits and player.suggestions
This commit is contained in:
vicrodh committed 2026-01-31 11:01:34 -06:00
1 parent 4d5a78ff24
commit f012ba33f4
4 files changed
+85 -37

No files matched your search

@@ -5,32 +5,81 @@
let { artwork }: Props = $props();
let canvasRef: HTMLCanvasElement | undefined = $state();
let isLoading = $state(true);
let currentArtwork = $state('');
// Track artwork changes for fade transition
// Generate a tiny blurred version using Canvas (GPU-free approach)
// Instead of CSS blur(120px) which kills WebKit performance,
// we resize the image to 8x8 pixels and scale it up - natural blur effect
async function generateBlurredBackground(imageUrl: string): Promise<void> {
if (!canvasRef || !imageUrl) return;
const ctx = canvasRef.getContext('2d');
if (!ctx) return;
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
// Draw to tiny size (8x8) - this creates natural blur when scaled
const tinySize = 8;
canvasRef!.width = tinySize;
canvasRef!.height = tinySize;
// Draw image scaled down to 8x8
ctx.drawImage(img, 0, 0, tinySize, tinySize);
// Slightly boost saturation by adjusting colors
const imageData = ctx.getImageData(0, 0, tinySize, tinySize);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
// Simple saturation boost
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const avg = (r + g + b) / 3;
// Saturation factor (1.3x)
const satFactor = 1.3;
data[i] = Math.min(255, avg + (r - avg) * satFactor);
data[i + 1] = Math.min(255, avg + (g - avg) * satFactor);
data[i + 2] = Math.min(255, avg + (b - avg) * satFactor);
// Brightness reduction (0.6x)
data[i] = data[i] * 0.6;
data[i + 1] = data[i + 1] * 0.6;
data[i + 2] = data[i + 2] * 0.6;
}
ctx.putImageData(imageData, 0, 0);
isLoading = false;
};
img.onerror = () => {
isLoading = false;
};
img.src = imageUrl;
}
// Track artwork changes
$effect(() => {
if (!artwork || artwork === currentArtwork) return;
currentArtwork = artwork;
isLoading = true;
generateBlurredBackground(artwork);
});
function handleImageLoad() {
isLoading = false;
}
</script>
<div class="immersive-background" class:loading={isLoading}>
<!-- Heavily blurred artwork - just color blobs -->
{#if artwork}
<img
src={artwork}
alt=""
class="background-image"
aria-hidden="true"
onload={handleImageLoad}
/>
{/if}
<!-- Canvas-generated tiny image, scaled up via CSS -->
<canvas
bind:this={canvasRef}
class="background-canvas"
aria-hidden="true"
></canvas>
<!-- Dark overlay for better contrast -->
<div class="dark-overlay"></div>
@@ -45,21 +94,22 @@
background-color: #0a0a0b;
}
.background-image {
.background-canvas {
position: absolute;
/* Extend beyond viewport to hide blur edges */
inset: -100px;
width: calc(100% + 200px);
height: calc(100% + 200px);
object-fit: cover;
object-position: center;
/* Heavy blur - should be unrecognizable, just color blobs */
filter: blur(120px) saturate(1.3) brightness(0.6);
transform: scale(1.2);
/* Extend beyond viewport for seamless edges */
inset: -50px;
width: calc(100% + 100px);
height: calc(100% + 100px);
/* Scale up tiny canvas - creates natural blur effect */
/* Using image-rendering: auto for smooth interpolation */
image-rendering: auto;
/* GPU layer for smooth transitions */
transform: scale(1.1) translateZ(0);
will-change: opacity;
transition: opacity 500ms ease-out;
}
.loading .background-image {
.loading .background-canvas {
opacity: 0;
}
@@ -1,5 +1,5 @@
<script lang="ts">
import { X, Disc3, LayoutGrid, Mic, ListMusic, Music2, Info, Radio } from 'lucide-svelte';
import { X, Disc3, LayoutGrid, Mic2, ListMusic, Music2, Info, Radio } from 'lucide-svelte';
import { t } from '$lib/i18n';
export type ImmersiveTab = 'lyrics' | 'credits' | 'suggestions' | 'queue';
@@ -39,7 +39,7 @@
const displayModes: { id: DisplayMode; icon: typeof Disc3; title: string }[] = [
{ id: 'coverflow', icon: Disc3, title: 'Coverflow (1)' },
{ id: 'split', icon: LayoutGrid, title: 'Split View (2)' },
{ id: 'lyrics-focus', icon: Mic, title: 'Lyrics Focus (3)' },
{ id: 'lyrics-focus', icon: Mic2, title: 'Lyrics Focus (3)' },
{ id: 'queue-focus', icon: ListMusic, title: 'Queue Focus (4)' },
];
@@ -123,9 +123,7 @@
align-items: center;
gap: 2px;
padding: 4px;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
background: rgba(0, 0, 0, 0.6);
border: 1px solid var(--alpha-10, rgba(255, 255, 255, 0.1));
border-radius: 10px;
}
@@ -163,9 +161,7 @@
align-items: center;
gap: 4px;
padding: 4px;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
background: rgba(0, 0, 0, 0.6);
border: 1px solid var(--alpha-10, rgba(255, 255, 255, 0.1));
border-radius: 12px;
}
@@ -209,9 +205,7 @@
justify-content: center;
width: 40px;
height: 40px;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
background: rgba(0, 0, 0, 0.6);
border: 1px solid var(--alpha-10, rgba(255, 255, 255, 0.1));
border-radius: 50%;
color: var(--alpha-70, rgba(255, 255, 255, 0.7));
+2
View File
@@ -253,6 +253,8 @@
"lyrics": "Lyrics",
"noLyrics": "No lyrics available",
"fetchingLyrics": "Fetching lyrics...",
"credits": "Credits",
"suggestions": "Suggestions",
"shuffle": "Shuffle",
"repeat": "Repeat",
"repeatAll": "Repeat All",
+2
View File
@@ -253,6 +253,8 @@
"lyrics": "Letras",
"noLyrics": "No hay letras disponibles",
"fetchingLyrics": "Buscando letras...",
"credits": "Créditos",
"suggestions": "Sugerencias",
"shuffle": "Aleatorio",
"repeat": "Repetir",
"repeatAll": "Repetir Todo",