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
|
// Other handlers
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|
||||||
private handleRemoveTrack(e: Event, position: number) {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.queue.removeFromQueue(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getDisplayTitle(track: {
|
private getDisplayTitle(track: {
|
||||||
title: string;
|
title: string;
|
||||||
filePath: string;
|
filePath: string;
|
||||||
@@ -1328,6 +1323,8 @@ export class QueuePanel
|
|||||||
dropIdx === trackCount &&
|
dropIdx === trackCount &&
|
||||||
index === trackCount - 1;
|
index === trackCount - 1;
|
||||||
|
|
||||||
|
// No inline closures — all events delegated via data-index
|
||||||
|
// on the virtualizer element (see firstUpdated).
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
@@ -1339,15 +1336,6 @@ export class QueuePanel
|
|||||||
})}
|
})}
|
||||||
data-index=${index}
|
data-index=${index}
|
||||||
draggable="true"
|
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">
|
<span class="track-position">
|
||||||
${index + 1}
|
${index + 1}
|
||||||
@@ -1362,8 +1350,6 @@ export class QueuePanel
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="remove-button"
|
class="remove-button"
|
||||||
@click=${(e: Event) =>
|
|
||||||
this.handleRemoveTrack(e, index)}
|
|
||||||
title="Remove from queue"
|
title="Remove from queue"
|
||||||
>
|
>
|
||||||
<wa-icon name="xmark"></wa-icon>
|
<wa-icon name="xmark"></wa-icon>
|
||||||
|
|||||||
@@ -1051,6 +1051,16 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
}
|
}
|
||||||
|
|
||||||
override disconnectedCallback() {
|
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(
|
this.virtualizer?.removeEventListener(
|
||||||
'visibilityChanged',
|
'visibilityChanged',
|
||||||
this.onVisibilityChanged,
|
this.onVisibilityChanged,
|
||||||
@@ -1087,6 +1097,17 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
|
|
||||||
override firstUpdated() {
|
override firstUpdated() {
|
||||||
this.initColumnWidths();
|
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>) {
|
override updated(changed: Map<string, unknown>) {
|
||||||
@@ -1209,6 +1230,75 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
this.libraryCtrl.setScrollPosition('tracks', first);
|
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(
|
private onTrackRowClick(
|
||||||
e: MouseEvent,
|
e: MouseEvent,
|
||||||
track: library.Track,
|
track: library.Track,
|
||||||
@@ -1548,6 +1638,8 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
? 'solid'
|
? 'solid'
|
||||||
: 'regular';
|
: 'regular';
|
||||||
|
|
||||||
|
// No inline closures — all events delegated via data-index
|
||||||
|
// on the virtualizer element (see firstUpdated).
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
@@ -1556,27 +1648,13 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
|
|||||||
selected,
|
selected,
|
||||||
})}
|
})}
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@click=${(e: MouseEvent) =>
|
data-index=${index}
|
||||||
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}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
'fav-icon': true,
|
'fav-icon': true,
|
||||||
favorited: isFav,
|
favorited: isFav,
|
||||||
})}
|
})}
|
||||||
@click=${(e: MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
void this.favCtrl.toggleFavorite(
|
|
||||||
track.FilePath,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<wa-icon
|
<wa-icon
|
||||||
name=${this.favCtrl.iconName}
|
name=${this.favCtrl.iconName}
|
||||||
|
|||||||
Reference in New Issue
Block a user