perf(14-03): eliminate per-item closure allocation in scroll render paths
- Replace inline arrow closures in renderTrackRow with event delegation via data-index
- Replace inline arrow closures in renderTrackItem with event delegation via data-index
- Add delegated click/dblclick/contextmenu/dragstart handlers on virtualizer elements
- Remove button click in queue panel also delegated via closest('.remove-button')
- Zero new function objects created per renderItem call during scroll
This commit is contained in:
@@ -1264,11 +1264,6 @@ export class QueuePanel
|
||||
// Other handlers
|
||||
// =================================================================
|
||||
|
||||
private handleRemoveTrack(e: Event, position: number) {
|
||||
e.stopPropagation();
|
||||
this.queue.removeFromQueue(position);
|
||||
}
|
||||
|
||||
private getDisplayTitle(track: {
|
||||
title: string;
|
||||
filePath: string;
|
||||
@@ -1328,6 +1323,8 @@ export class QueuePanel
|
||||
dropIdx === trackCount &&
|
||||
index === trackCount - 1;
|
||||
|
||||
// No inline closures — all events delegated via data-index
|
||||
// on the virtualizer element (see firstUpdated).
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
@@ -1339,15 +1336,6 @@ export class QueuePanel
|
||||
})}
|
||||
data-index=${index}
|
||||
draggable="true"
|
||||
@click=${(e: MouseEvent) =>
|
||||
this.handleTrackClick(e, track, index)}
|
||||
@dblclick=${() =>
|
||||
this.handleTrackDblClick(index)}
|
||||
@contextmenu=${(e: MouseEvent) =>
|
||||
this.handleTrackContextMenu(e, index)}
|
||||
@dragstart=${(e: DragEvent) =>
|
||||
this.onTrackDragStart(e, index)}
|
||||
@dragend=${this.onTrackDragEnd}
|
||||
>
|
||||
<span class="track-position">
|
||||
${index + 1}
|
||||
@@ -1362,8 +1350,6 @@ export class QueuePanel
|
||||
</div>
|
||||
<button
|
||||
class="remove-button"
|
||||
@click=${(e: Event) =>
|
||||
this.handleRemoveTrack(e, index)}
|
||||
title="Remove from queue"
|
||||
>
|
||||
<wa-icon name="xmark"></wa-icon>
|
||||
|
||||
@@ -1051,6 +1051,16 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
// Remove delegated event handlers from virtualizer.
|
||||
const virt = this.virtualizer;
|
||||
if (virt) {
|
||||
virt.removeEventListener('click', this.onDelegatedClick);
|
||||
virt.removeEventListener('dblclick', this.onDelegatedDblClick);
|
||||
virt.removeEventListener('contextmenu', this.onDelegatedContextMenu);
|
||||
virt.removeEventListener('dragstart', this.onDelegatedDragStart);
|
||||
virt.removeEventListener('dragend', this.onTrackDragEnd);
|
||||
}
|
||||
|
||||
this.virtualizer?.removeEventListener(
|
||||
'visibilityChanged',
|
||||
this.onVisibilityChanged,
|
||||
@@ -1087,6 +1097,17 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
||||
|
||||
override firstUpdated() {
|
||||
this.initColumnWidths();
|
||||
|
||||
// Event delegation: attach stable handlers to the virtualizer
|
||||
// so renderTrackRow creates zero per-item closures.
|
||||
const virt = this.virtualizer;
|
||||
if (virt) {
|
||||
virt.addEventListener('click', this.onDelegatedClick);
|
||||
virt.addEventListener('dblclick', this.onDelegatedDblClick);
|
||||
virt.addEventListener('contextmenu', this.onDelegatedContextMenu);
|
||||
virt.addEventListener('dragstart', this.onDelegatedDragStart);
|
||||
virt.addEventListener('dragend', this.onTrackDragEnd);
|
||||
}
|
||||
}
|
||||
|
||||
override updated(changed: Map<string, unknown>) {
|
||||
@@ -1209,6 +1230,75 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
||||
this.libraryCtrl.setScrollPosition('tracks', first);
|
||||
};
|
||||
|
||||
// =================================================================
|
||||
// Delegated event handlers (stable references, zero per-item closures)
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* Walk up from the event target to find the nearest
|
||||
* `.track-row` and extract track + index via `data-index`.
|
||||
*/
|
||||
private resolveTrackFromEvent(
|
||||
e: Event,
|
||||
): { track: library.Track; index: number } | null {
|
||||
const row = (e.target as HTMLElement).closest(
|
||||
'.track-row',
|
||||
) as HTMLElement | null;
|
||||
|
||||
if (!row) return null;
|
||||
|
||||
const idx = Number(row.dataset.index);
|
||||
const track = this.cachedSortedTracks[idx];
|
||||
|
||||
if (!track) return null;
|
||||
|
||||
return { track, index: idx };
|
||||
}
|
||||
|
||||
private onDelegatedClick = (e: MouseEvent) => {
|
||||
const hit = this.resolveTrackFromEvent(e);
|
||||
|
||||
if (!hit) return;
|
||||
|
||||
// Check if click was on fav-icon
|
||||
const favEl = (e.target as HTMLElement).closest(
|
||||
'.fav-icon',
|
||||
);
|
||||
|
||||
if (favEl) {
|
||||
e.stopPropagation();
|
||||
void this.favCtrl.toggleFavorite(
|
||||
hit.track.FilePath,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.onTrackRowClick(e, hit.track, hit.index);
|
||||
};
|
||||
|
||||
private onDelegatedDblClick = (e: MouseEvent) => {
|
||||
const hit = this.resolveTrackFromEvent(e);
|
||||
|
||||
if (hit) this.onTrackRowDblClick(hit.track);
|
||||
};
|
||||
|
||||
private onDelegatedContextMenu = (e: MouseEvent) => {
|
||||
const hit = this.resolveTrackFromEvent(e);
|
||||
|
||||
if (hit) this.onTrackContextMenu(e, hit.track);
|
||||
};
|
||||
|
||||
private onDelegatedDragStart = (e: DragEvent) => {
|
||||
const hit = this.resolveTrackFromEvent(e);
|
||||
|
||||
if (hit) this.onTrackDragStart(e, hit.track);
|
||||
};
|
||||
|
||||
// =================================================================
|
||||
// Original handlers (still used internally)
|
||||
// =================================================================
|
||||
|
||||
private onTrackRowClick(
|
||||
e: MouseEvent,
|
||||
track: library.Track,
|
||||
@@ -1548,6 +1638,8 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
||||
? 'solid'
|
||||
: 'regular';
|
||||
|
||||
// No inline closures — all events delegated via data-index
|
||||
// on the virtualizer element (see firstUpdated).
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
@@ -1556,27 +1648,13 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
||||
selected,
|
||||
})}
|
||||
draggable="true"
|
||||
@click=${(e: MouseEvent) =>
|
||||
this.onTrackRowClick(e, track, index)}
|
||||
@dblclick=${() =>
|
||||
this.onTrackRowDblClick(track)}
|
||||
@contextmenu=${(e: MouseEvent) =>
|
||||
this.onTrackContextMenu(e, track)}
|
||||
@dragstart=${(e: DragEvent) =>
|
||||
this.onTrackDragStart(e, track)}
|
||||
@dragend=${this.onTrackDragEnd}
|
||||
data-index=${index}
|
||||
>
|
||||
<div
|
||||
class=${classMap({
|
||||
'fav-icon': true,
|
||||
favorited: isFav,
|
||||
})}
|
||||
@click=${(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
void this.favCtrl.toggleFavorite(
|
||||
track.FilePath,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<wa-icon
|
||||
name=${this.favCtrl.iconName}
|
||||
|
||||
Reference in New Issue
Block a user