perf: reduce software rendering overhead for NVIDIA+Wayland
Targeted optimizations for the DMABuf-disabled rendering path where every frame is software-composited: - Replace infinite CSS scroll-text animation with transition-based cycle that only repaints during active scroll, not during pauses - Remove CSS mask-image on scrolling text (mask + animation was the single most expensive continuous repaint) - Replace wa-icon in track rows with inline SVG — eliminates 30-50 shadow DOM trees (each with SVG fetch/parse) during scroll - Remove hover transitions on album cards, artist cards, genre cards, fav icons, queue remove buttons — each transition was causing per-frame software repaints - Use visibility:hidden instead of opacity:0 for queue remove button (binary switch vs per-frame alpha blend) - Add decoding=async to now-playing cover art images (prevents main-thread blocking during image decode on track change) - Add contain:strict to fixed-height track rows (33px) and queue items (49px) — browser skips size contribution calculations
This commit is contained in:
@@ -245,9 +245,7 @@ export class ArtistsView
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
/* transitions removed — software rendering repaints per frame */
|
||||||
background-color 0.15s ease,
|
|
||||||
transform 0.15s ease;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -143,9 +143,7 @@ const gridStyles = css`
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
transition:
|
/* transitions removed — software rendering repaints per frame */
|
||||||
background-color 0.2s ease,
|
|
||||||
transform 0.15s ease;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: var(--card-width, 176px);
|
width: var(--card-width, 176px);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,9 +249,7 @@ export class GenresView
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
/* transitions removed — software rendering repaints per frame */
|
||||||
background-color 0.15s ease,
|
|
||||||
transform 0.15s ease;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,18 @@ export class NowPlaying extends LitElement {
|
|||||||
@state()
|
@state()
|
||||||
private artistHovered = false;
|
private artistHovered = false;
|
||||||
|
|
||||||
|
/** Whether each field is actively mid-scroll (class toggle). */
|
||||||
|
@state()
|
||||||
|
private titleScrolling = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private artistScrolling = false;
|
||||||
|
|
||||||
|
private scrollTimers: Record<string, ReturnType<typeof setTimeout> | null> = {
|
||||||
|
title: null,
|
||||||
|
artist: null,
|
||||||
|
};
|
||||||
|
|
||||||
private resizeObserver?: ResizeObserver;
|
private resizeObserver?: ResizeObserver;
|
||||||
|
|
||||||
static override styles = [designTokens, css`
|
static override styles = [designTokens, css`
|
||||||
@@ -185,35 +197,17 @@ export class NowPlaying extends LitElement {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fade-out masks on both edges when scrolling */
|
/* Scrolling text: use a single transition instead of an infinite
|
||||||
.track-title.will-scroll,
|
CSS animation. The infinite animation + mask-image was repainting
|
||||||
.track-artist.will-scroll {
|
every frame in software rendering mode (no DMABuf). A transition
|
||||||
mask-image: linear-gradient(
|
only repaints during the active scroll, and pauses are free. */
|
||||||
to right,
|
|
||||||
transparent 0%,
|
|
||||||
black 8%,
|
|
||||||
black 92%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
-webkit-mask-image: linear-gradient(
|
|
||||||
to right,
|
|
||||||
transparent 0%,
|
|
||||||
black 8%,
|
|
||||||
black 92%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.will-scroll .scroll-content {
|
.will-scroll .scroll-content {
|
||||||
animation: scroll-text var(--scroll-duration, 5s) linear infinite;
|
transition: transform var(--scroll-duration, 5s) linear;
|
||||||
padding-right: 2em; /* gap before the text repeats visually */
|
padding-right: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes scroll-text {
|
.will-scroll.scrolling .scroll-content {
|
||||||
0% { transform: translateX(0); }
|
transform: translateX(var(--scroll-distance, -100%));
|
||||||
5% { transform: translateX(0); }
|
|
||||||
95% { transform: translateX(var(--scroll-distance, -100%)); }
|
|
||||||
100% { transform: translateX(var(--scroll-distance, -100%)); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.resize-handle {
|
.resize-handle {
|
||||||
@@ -253,12 +247,15 @@ export class NowPlaying extends LitElement {
|
|||||||
document.removeEventListener('mouseup', this.handleMouseUp);
|
document.removeEventListener('mouseup', this.handleMouseUp);
|
||||||
window.removeEventListener(SCROLL_CHANGE_EVENT, this.handleScrollModeEvent);
|
window.removeEventListener(SCROLL_CHANGE_EVENT, this.handleScrollModeEvent);
|
||||||
this.resizeObserver?.disconnect();
|
this.resizeObserver?.disconnect();
|
||||||
|
this.stopScrollCycle('title');
|
||||||
|
this.stopScrollCycle('artist');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override updated(): void {
|
protected override updated(): void {
|
||||||
this.checkOverflows();
|
this.checkOverflows();
|
||||||
this.observeTextContainers();
|
this.observeTextContainers();
|
||||||
this.applyScrollDistances();
|
this.applyScrollDistances();
|
||||||
|
this.syncScrollCycles();
|
||||||
}
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
@@ -298,6 +295,7 @@ export class NowPlaying extends LitElement {
|
|||||||
? html`<img
|
? html`<img
|
||||||
src="${track.coverArtSmall || track.coverArt}"
|
src="${track.coverArtSmall || track.coverArt}"
|
||||||
alt="Album cover"
|
alt="Album cover"
|
||||||
|
decoding="async"
|
||||||
@error=${(e: Event) => {
|
@error=${(e: Event) => {
|
||||||
const img = e.target as HTMLImageElement;
|
const img = e.target as HTMLImageElement;
|
||||||
if (
|
if (
|
||||||
@@ -325,6 +323,7 @@ export class NowPlaying extends LitElement {
|
|||||||
<img
|
<img
|
||||||
src="${track.coverArt}"
|
src="${track.coverArt}"
|
||||||
alt="Album cover full size"
|
alt="Album cover full size"
|
||||||
|
decoding="async"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@@ -334,16 +333,18 @@ export class NowPlaying extends LitElement {
|
|||||||
<div class="track-info-wrapper">
|
<div class="track-info-wrapper">
|
||||||
<div class="track-info">
|
<div class="track-info">
|
||||||
<span
|
<span
|
||||||
class="track-title ${titleScrolling ? 'will-scroll' : ''}"
|
class="track-title ${titleScrolling ? 'will-scroll' : ''} ${this.titleScrolling ? 'scrolling' : ''}"
|
||||||
@mouseenter=${this.handleTitleMouseEnter}
|
@mouseenter=${this.handleTitleMouseEnter}
|
||||||
@mouseleave=${this.handleTitleMouseLeave}
|
@mouseleave=${this.handleTitleMouseLeave}
|
||||||
|
@transitionend=${() => this.onScrollCycleEnd('title')}
|
||||||
>
|
>
|
||||||
<span class="scroll-content">${track.title}</span>
|
<span class="scroll-content">${track.title}</span>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="track-artist ${artistScrolling ? 'will-scroll' : ''}"
|
class="track-artist ${artistScrolling ? 'will-scroll' : ''} ${this.artistScrolling ? 'scrolling' : ''}"
|
||||||
@mouseenter=${this.handleArtistMouseEnter}
|
@mouseenter=${this.handleArtistMouseEnter}
|
||||||
@mouseleave=${this.handleArtistMouseLeave}
|
@mouseleave=${this.handleArtistMouseLeave}
|
||||||
|
@transitionend=${() => this.onScrollCycleEnd('artist')}
|
||||||
>
|
>
|
||||||
<span class="scroll-content">${track.artist || 'Unknown Artist'}</span>
|
<span class="scroll-content">${track.artist || 'Unknown Artist'}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -456,6 +457,62 @@ export class NowPlaying extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// TRANSITION-BASED SCROLL CYCLE
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a scroll cycle for a field. Adds the `scrolling` class which
|
||||||
|
* triggers a CSS transition. When the transition ends, we pause then
|
||||||
|
* snap back and repeat. This replaces the old infinite CSS animation
|
||||||
|
* which repainted every frame even during the pause phases.
|
||||||
|
*/
|
||||||
|
private startScrollCycle(field: 'title' | 'artist'): void {
|
||||||
|
if (!this.shouldScroll(field)) return;
|
||||||
|
|
||||||
|
// Small delay before starting the scroll
|
||||||
|
this.scrollTimers[field] = setTimeout(() => {
|
||||||
|
if (field === 'title') this.titleScrolling = true;
|
||||||
|
else this.artistScrolling = true;
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopScrollCycle(field: 'title' | 'artist'): void {
|
||||||
|
if (this.scrollTimers[field] !== null) {
|
||||||
|
clearTimeout(this.scrollTimers[field]!);
|
||||||
|
this.scrollTimers[field] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === 'title') this.titleScrolling = false;
|
||||||
|
else this.artistScrolling = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onScrollCycleEnd(field: 'title' | 'artist'): void {
|
||||||
|
// Transition finished → snap back after a pause, then repeat
|
||||||
|
if (field === 'title') this.titleScrolling = false;
|
||||||
|
else this.artistScrolling = false;
|
||||||
|
|
||||||
|
// Restart after a pause (2s at the scrolled-to position)
|
||||||
|
this.scrollTimers[field] = setTimeout(() => {
|
||||||
|
this.startScrollCycle(field);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called from updated() when shouldScroll state changes. */
|
||||||
|
private syncScrollCycles(): void {
|
||||||
|
for (const field of ['title', 'artist'] as const) {
|
||||||
|
const should = this.shouldScroll(field);
|
||||||
|
const active = this.scrollTimers[field] !== null ||
|
||||||
|
(field === 'title' ? this.titleScrolling : this.artistScrolling);
|
||||||
|
|
||||||
|
if (should && !active) {
|
||||||
|
this.startScrollCycle(field);
|
||||||
|
} else if (!should && active) {
|
||||||
|
this.stopScrollCycle(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
// HOVER HANDLERS
|
// HOVER HANDLERS
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
|
|||||||
@@ -319,6 +319,7 @@ export class QueuePanel
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 49px;
|
height: 49px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
contain: strict;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-item:hover {
|
.track-item:hover {
|
||||||
@@ -384,12 +385,11 @@ export class QueuePanel
|
|||||||
padding: 4px;
|
padding: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
opacity: 0;
|
visibility: hidden;
|
||||||
transition: opacity 0.15s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-item:hover .remove-button {
|
.track-item:hover .remove-button {
|
||||||
opacity: 1;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remove-button:hover {
|
.remove-button:hover {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { library } from '@go/models';
|
import { library } from '@go/models';
|
||||||
import { LitElement, html, css, nothing } from 'lit';
|
import { LitElement, html, svg, css, nothing } from 'lit';
|
||||||
import { designTokens } from '../../styles/tokens.css';
|
import { designTokens } from '../../styles/tokens.css';
|
||||||
import {
|
import {
|
||||||
customElement,
|
customElement,
|
||||||
@@ -61,6 +61,21 @@ const SORT_DIR_KEY = 'track-list-sort-direction';
|
|||||||
const MIN_COLUMN_WIDTH = 50;
|
const MIN_COLUMN_WIDTH = 50;
|
||||||
const DEFAULT_FIXED_WIDTH = 80;
|
const DEFAULT_FIXED_WIDTH = 80;
|
||||||
|
|
||||||
|
// Inline SVG paths for favorite icons — eliminates wa-icon shadow DOM
|
||||||
|
// overhead (30-50 shadow roots during scroll). Font Awesome 6 paths.
|
||||||
|
const FAV_ICONS = {
|
||||||
|
heart: {
|
||||||
|
viewBox: '0 0 512 512',
|
||||||
|
regular: 'M225.8 468.2l-2.5-2.3L48.1 303.2C17.4 274.7 0 234.7 0 192.8v-3.3c0-70.4 50-130.8 119.2-144C158.6 37.9 198.9 47 231 69.6c9 6.3 17.3 13.5 25 21.5c7.7-8 16-15.2 25-21.5c32.1-22.6 72.4-31.7 111.8-24.2C461.5 59.6 512 124.2 512 192.8v3.3c0 41.9-17.4 81.9-48.1 110.4L288.7 465.9l-2.5 2.3c-8.2 7.6-19 11.9-30.2 11.9s-22-4.2-30.2-11.9z',
|
||||||
|
solid: 'M47.6 300.4L228.3 469.1c7.5 7 17.4 10.9 27.7 10.9s20.2-3.9 27.7-10.9L464.4 300.4c30.4-28.3 47.6-68 47.6-109.5v-5.8c0-69.9-50.5-129.5-119.4-141C347 36.5 300.6 51.4 268 84L256 96 244 84c-32.6-32.6-79-47.5-124.6-39.9C50.5 55.6 0 115.2 0 185.1v5.8c0 41.5 17.2 81.2 47.6 109.5z',
|
||||||
|
},
|
||||||
|
star: {
|
||||||
|
viewBox: '0 0 576 512',
|
||||||
|
regular: 'M287.9 0c9.2 0 17.6 5.2 21.6 13.5l68.6 141.3 153.2 22.6c9 1.3 16.5 7.6 19.3 16.3s.5 18.1-5.9 24.5L434.8 326.7l26.2 155.6c1.5 9-2.2 18.1-9.7 23.5s-17.3 6-25.3 1.7L288 439.6 149.7 507.5c-8 4.3-17.8 3.7-25.3-1.7s-11.2-14.5-9.7-23.5l26.2-155.6L31.1 218.2c-6.5-6.4-8.7-15.9-5.9-24.5s10.3-14.9 19.3-16.3l153.2-22.6L266.3 13.5C270.4 5.2 278.7 0 287.9 0z',
|
||||||
|
solid: 'M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 108.4 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3L288.1 439.8 420.9 508.3c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L437.7 329 542 225.9c8.6-8.4 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L380.7 150.3 316.9 18z',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
type SortDirection = 'asc' | 'desc';
|
type SortDirection = 'asc' | 'desc';
|
||||||
|
|
||||||
@customElement('track-list')
|
@customElement('track-list')
|
||||||
@@ -956,6 +971,7 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 33px;
|
height: 33px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
contain: strict;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-row > * {
|
.track-row > * {
|
||||||
@@ -1012,7 +1028,6 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--yj-text-tertiary, #666);
|
color: var(--yj-text-tertiary, #666);
|
||||||
font-size: var(--yj-text-sm);
|
font-size: var(--yj-text-sm);
|
||||||
transition: color 0.1s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fav-icon:hover {
|
.fav-icon:hover {
|
||||||
@@ -1652,9 +1667,11 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
const isFav = this.favCtrl.isFavorited(
|
const isFav = this.favCtrl.isFavorited(
|
||||||
track.FilePath,
|
track.FilePath,
|
||||||
);
|
);
|
||||||
const favVariant = isFav
|
|
||||||
? 'solid'
|
// Inline SVG instead of wa-icon — eliminates a shadow DOM tree
|
||||||
: 'regular';
|
// per visible row (~30-50 during scroll).
|
||||||
|
const iconDef = FAV_ICONS[this.favCtrl.iconStyle === 'star' ? 'star' : 'heart'];
|
||||||
|
const iconPath = isFav ? iconDef.solid : iconDef.regular;
|
||||||
|
|
||||||
// No inline closures — all events delegated via data-index
|
// No inline closures — all events delegated via data-index
|
||||||
// on the virtualizer element (see firstUpdated).
|
// on the virtualizer element (see firstUpdated).
|
||||||
@@ -1675,10 +1692,9 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
favorited: isFav,
|
favorited: isFav,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<wa-icon
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${iconDef.viewBox.split(' ').slice(2).join(' ')}" width="14" height="14">
|
||||||
name=${this.favCtrl.iconName}
|
${svg`<path fill="currentColor" d="${iconPath}"/>`}
|
||||||
variant=${favVariant}
|
</svg>
|
||||||
></wa-icon>
|
|
||||||
</div>
|
</div>
|
||||||
${cols.map((col) => {
|
${cols.map((col) => {
|
||||||
const val = col.accessor(track);
|
const val = col.accessor(track);
|
||||||
|
|||||||
Reference in New Issue
Block a user