perf(downloads): start column moves immediately
Commit the local target order before persistence, limit FLIP measurements to columns that actually move, and remove the redundant row-wide grid preparation pass. Keep the animation-disabled path free of geometry reads and preserve serialized settings writes.
This commit is contained in:
@@ -95,7 +95,7 @@ import {
|
|||||||
type StatisticsViewActions
|
type StatisticsViewActions
|
||||||
} from "./views/statistics/StatisticsView";
|
} from "./views/statistics/StatisticsView";
|
||||||
import { buildDownloadsViewModel, formatRemainingDownloadBytes, getDownloadQueueTotalBytes, getDownloadSpeedBps, getPendingDownloadItemCount, getRemainingDownloadBytes, type DownloadDisplayMode, type DownloadSidebarFilter } from "./views/downloads/downloads-model";
|
import { buildDownloadsViewModel, formatRemainingDownloadBytes, getDownloadQueueTotalBytes, getDownloadSpeedBps, getPendingDownloadItemCount, getRemainingDownloadBytes, type DownloadDisplayMode, type DownloadSidebarFilter } from "./views/downloads/downloads-model";
|
||||||
import { applyDownloadGridTemplate, downloadColumnDefinitions, type DownloadSortColumn } from "./views/downloads/DownloadsTable";
|
import { downloadColumnDefinitions, type DownloadSortColumn } from "./views/downloads/DownloadsTable";
|
||||||
import { DeleteConfirmationDialog } from "./views/downloads/DeleteConfirmationDialog";
|
import { DeleteConfirmationDialog } from "./views/downloads/DeleteConfirmationDialog";
|
||||||
import { beginDownloadColumnDrag, clearDownloadColumnDrag, commitDownloadColumnDrag, createDownloadColumnOrderPersistence, DOWNLOAD_COLUMN_MOVE_DURATION_MS, updateDownloadColumnDrag, type DownloadColumnDragSession, type DownloadColumnOrderPersistence } from "./views/downloads/column-drag";
|
import { beginDownloadColumnDrag, clearDownloadColumnDrag, commitDownloadColumnDrag, createDownloadColumnOrderPersistence, DOWNLOAD_COLUMN_MOVE_DURATION_MS, updateDownloadColumnDrag, type DownloadColumnDragSession, type DownloadColumnOrderPersistence } from "./views/downloads/column-drag";
|
||||||
import {
|
import {
|
||||||
@@ -4917,11 +4917,11 @@ export function App(): ReactElement {
|
|||||||
suppressColumnSortRef.current = true;
|
suppressColumnSortRef.current = true;
|
||||||
window.setTimeout(() => { suppressColumnSortRef.current = false; }, 0);
|
window.setTimeout(() => { suppressColumnSortRef.current = false; }, 0);
|
||||||
const changed = next.join("|") !== session.measurements.map((measurement) => measurement.id).join("|");
|
const changed = next.join("|") !== session.measurements.map((measurement) => measurement.id).join("|");
|
||||||
if (changed) persistColumnOrder(next);
|
|
||||||
const animationsEnabled = snapshot.settings.animatePackageDisclosure;
|
const animationsEnabled = snapshot.settings.animatePackageDisclosure;
|
||||||
columnDragAnimationsRef.current = commitDownloadColumnDrag(session, next, (order) => {
|
columnDragAnimationsRef.current = commitDownloadColumnDrag(session, next, (order) => {
|
||||||
if (changed) flushSync(() => setColumnOrder(order));
|
if (changed) flushSync(() => setColumnOrder(order));
|
||||||
}, () => applyDownloadGridTemplate(session.root, next), animationsEnabled);
|
}, () => {}, animationsEnabled);
|
||||||
|
if (changed) persistColumnOrder(next);
|
||||||
if (!animationsEnabled) {
|
if (!animationsEnabled) {
|
||||||
if (columnDragSessionRef.current === session) columnDragSessionRef.current = null;
|
if (columnDragSessionRef.current === session) columnDragSessionRef.current = null;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -44,13 +44,6 @@ export function downloadGridTemplateForOrder(columnOrder: readonly string[]): st
|
|||||||
return downloadGridTemplate(columnOrder.map((column) => downloadColumnDefinitions[column]?.width ?? "100px").join(" "));
|
return downloadGridTemplate(columnOrder.map((column) => downloadColumnDefinitions[column]?.width ?? "100px").join(" "));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyDownloadGridTemplate(root: HTMLElement, columnOrder: readonly string[]): void {
|
|
||||||
const value = downloadGridTemplateForOrder(columnOrder);
|
|
||||||
root.querySelectorAll<HTMLElement>(".downloads-table-header, .downloads-package-row, .downloads-item-row").forEach((row) => {
|
|
||||||
row.style.gridTemplateColumns = value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPackageRowDisclosureExcluded(target: EventTarget | null): boolean {
|
function isPackageRowDisclosureExcluded(target: EventTarget | null): boolean {
|
||||||
const closest = (target as { closest?: (selector: string) => Element | null } | null)?.closest;
|
const closest = (target as { closest?: (selector: string) => Element | null } | null)?.closest;
|
||||||
return typeof closest === "function" && closest.call(target, PACKAGE_ROW_DISCLOSURE_EXCLUSION_SELECTOR) !== null;
|
return typeof closest === "function" && closest.call(target, PACKAGE_ROW_DISCLOSURE_EXCLUSION_SELECTOR) !== null;
|
||||||
|
|||||||
@@ -124,12 +124,22 @@ export function commitDownloadColumnDrag(
|
|||||||
prepare: () => void = () => {},
|
prepare: () => void = () => {},
|
||||||
animationsEnabled = true
|
animationsEnabled = true
|
||||||
): Animation[] {
|
): Animation[] {
|
||||||
const elements = Array.from(session.root.querySelectorAll<HTMLElement>("[data-download-column]"));
|
if (!animationsEnabled) {
|
||||||
|
clearDownloadColumnDrag(session);
|
||||||
|
prepare();
|
||||||
|
commit(order);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const originalOrder = session.measurements.map((measurement) => measurement.id);
|
||||||
|
const movedIds = originalOrder.filter((id, index) => (
|
||||||
|
order[index] !== id || Math.abs(session.preview.settleOffsets[id] ?? 0) >= 0.5
|
||||||
|
));
|
||||||
|
const selector = movedIds.map((id) => `[data-download-column="${id}"]`).join(", ");
|
||||||
|
const elements = selector ? Array.from(session.root.querySelectorAll<HTMLElement>(selector)) : [];
|
||||||
const before = new Map(elements.map((element) => [element, element.getBoundingClientRect()]));
|
const before = new Map(elements.map((element) => [element, element.getBoundingClientRect()]));
|
||||||
clearDownloadColumnDrag(session);
|
clearDownloadColumnDrag(session);
|
||||||
prepare();
|
prepare();
|
||||||
commit(order);
|
commit(order);
|
||||||
if (!animationsEnabled) return [];
|
|
||||||
session.root.classList.add("is-column-drag-settling");
|
session.root.classList.add("is-column-drag-settling");
|
||||||
const animations: Animation[] = [];
|
const animations: Animation[] = [];
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ describe("desktop shell", () => {
|
|||||||
|
|
||||||
expect(pointerUp.indexOf("commitDownloadColumnDrag")).toBeGreaterThanOrEqual(0);
|
expect(pointerUp.indexOf("commitDownloadColumnDrag")).toBeGreaterThanOrEqual(0);
|
||||||
expect(pointerUp.indexOf("commitDownloadColumnDrag")).toBeLessThan(pointerUp.indexOf("columnDragSettleTimerRef.current = window.setTimeout"));
|
expect(pointerUp.indexOf("commitDownloadColumnDrag")).toBeLessThan(pointerUp.indexOf("columnDragSettleTimerRef.current = window.setTimeout"));
|
||||||
|
expect(pointerUp.indexOf("commitDownloadColumnDrag")).toBeLessThan(pointerUp.indexOf("persistColumnOrder"));
|
||||||
expect(pointerUp).toContain("snapshot.settings.animatePackageDisclosure");
|
expect(pointerUp).toContain("snapshot.settings.animatePackageDisclosure");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -54,9 +54,19 @@ describe("animated download column drag", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
{ afterLeft: 150, beforeLeft: 0, expectedDelta: -150, next: ["size", "name", "status"] },
|
{ afterLeft: 150, beforeLeft: 0, expectedDelta: -150, measurements: columns, next: ["size", "name", "status"] },
|
||||||
{ afterLeft: 0, beforeLeft: 150, expectedDelta: 150, next: ["name", "size", "status"] }
|
{
|
||||||
])("commits the target grid before animating from a $expectedDelta px inverse offset", ({ afterLeft, beforeLeft, expectedDelta, next }) => {
|
afterLeft: 0,
|
||||||
|
beforeLeft: 150,
|
||||||
|
expectedDelta: 150,
|
||||||
|
measurements: [
|
||||||
|
{ id: "size", left: 0, width: 150 },
|
||||||
|
{ id: "name", left: 150, width: 300 },
|
||||||
|
{ id: "status", left: 450, width: 100 }
|
||||||
|
],
|
||||||
|
next: ["name", "size", "status"]
|
||||||
|
}
|
||||||
|
])("commits the target grid before animating from a $expectedDelta px inverse offset", ({ afterLeft, beforeLeft, expectedDelta, measurements, next }) => {
|
||||||
const commitDownloadColumnDrag = (columnDrag as unknown as {
|
const commitDownloadColumnDrag = (columnDrag as unknown as {
|
||||||
commitDownloadColumnDrag?: (session: DownloadColumnDragSession, order: string[], commit: (order: string[]) => void, prepare: () => void) => Animation[];
|
commitDownloadColumnDrag?: (session: DownloadColumnDragSession, order: string[], commit: (order: string[]) => void, prepare: () => void) => Animation[];
|
||||||
}).commitDownloadColumnDrag;
|
}).commitDownloadColumnDrag;
|
||||||
@@ -64,6 +74,7 @@ describe("animated download column drag", () => {
|
|||||||
if (!commitDownloadColumnDrag) return;
|
if (!commitDownloadColumnDrag) return;
|
||||||
|
|
||||||
const events: string[] = [];
|
const events: string[] = [];
|
||||||
|
const selectors: string[] = [];
|
||||||
let committed = false;
|
let committed = false;
|
||||||
const outerAnimate = vi.fn(() => ({ finished: Promise.resolve() } as unknown as Animation));
|
const outerAnimate = vi.fn(() => ({ finished: Promise.resolve() } as unknown as Animation));
|
||||||
const animate = vi.fn(() => ({ finished: Promise.resolve() } as unknown as Animation));
|
const animate = vi.fn(() => ({ finished: Promise.resolve() } as unknown as Animation));
|
||||||
@@ -76,7 +87,10 @@ describe("animated download column drag", () => {
|
|||||||
const root = {
|
const root = {
|
||||||
classList: { add: () => events.push("add-class"), remove: () => events.push("clear-classes") },
|
classList: { add: () => events.push("add-class"), remove: () => events.push("clear-classes") },
|
||||||
dataset: { columnDragging: "name" },
|
dataset: { columnDragging: "name" },
|
||||||
querySelectorAll: (selector: string) => selector === "[data-download-column]" ? [element] : [],
|
querySelectorAll: (selector: string) => {
|
||||||
|
selectors.push(selector);
|
||||||
|
return selector.includes('data-download-column="name"') ? [element] : [];
|
||||||
|
},
|
||||||
style: {
|
style: {
|
||||||
removeProperty: (property: string) => events.push(`clear:${property}`)
|
removeProperty: (property: string) => events.push(`clear:${property}`)
|
||||||
}
|
}
|
||||||
@@ -84,9 +98,9 @@ describe("animated download column drag", () => {
|
|||||||
const session = {
|
const session = {
|
||||||
active: true,
|
active: true,
|
||||||
draggedId: "name",
|
draggedId: "name",
|
||||||
measurements: columns,
|
measurements,
|
||||||
pointerId: -1,
|
pointerId: -1,
|
||||||
preview: calculateColumnDragPreview(columns, "name", next[0] === "name" ? -160 : 160),
|
preview: calculateColumnDragPreview(measurements, "name", next[0] === "name" ? -160 : 160),
|
||||||
root,
|
root,
|
||||||
startX: 0
|
startX: 0
|
||||||
} as DownloadColumnDragSession;
|
} as DownloadColumnDragSession;
|
||||||
@@ -97,6 +111,9 @@ describe("animated download column drag", () => {
|
|||||||
}, () => events.push("prepare-grid"));
|
}, () => events.push("prepare-grid"));
|
||||||
|
|
||||||
expect(animations).toHaveLength(1);
|
expect(animations).toHaveLength(1);
|
||||||
|
expect(selectors[0]).toContain('data-download-column="name"');
|
||||||
|
expect(selectors[0]).toContain('data-download-column="size"');
|
||||||
|
expect(selectors[0]).not.toContain('data-download-column="status"');
|
||||||
expect(events).toContain("prepare-grid");
|
expect(events).toContain("prepare-grid");
|
||||||
expect(events.indexOf("prepare-grid")).toBeLessThan(events.indexOf(`commit:${next.join("|")}`));
|
expect(events.indexOf("prepare-grid")).toBeLessThan(events.indexOf(`commit:${next.join("|")}`));
|
||||||
expect(events).toContain(`commit:${next.join("|")}`);
|
expect(events).toContain(`commit:${next.join("|")}`);
|
||||||
@@ -109,6 +126,7 @@ describe("animated download column drag", () => {
|
|||||||
|
|
||||||
it("commits immediately without WAAPI when application animations are disabled", () => {
|
it("commits immediately without WAAPI when application animations are disabled", () => {
|
||||||
const animate = vi.fn(() => ({ finished: Promise.resolve() } as unknown as Animation));
|
const animate = vi.fn(() => ({ finished: Promise.resolve() } as unknown as Animation));
|
||||||
|
const querySelectorAll = vi.fn((selector: string) => selector === "[data-column-dragging]" ? [] : [element]);
|
||||||
let committed = false;
|
let committed = false;
|
||||||
const element = {
|
const element = {
|
||||||
children: [{ animate }],
|
children: [{ animate }],
|
||||||
@@ -117,7 +135,7 @@ describe("animated download column drag", () => {
|
|||||||
const root = {
|
const root = {
|
||||||
classList: { add: vi.fn(), remove: vi.fn() },
|
classList: { add: vi.fn(), remove: vi.fn() },
|
||||||
dataset: {},
|
dataset: {},
|
||||||
querySelectorAll: (selector: string) => selector === "[data-download-column]" ? [element] : [],
|
querySelectorAll,
|
||||||
style: { removeProperty: vi.fn() }
|
style: { removeProperty: vi.fn() }
|
||||||
} as unknown as HTMLElement;
|
} as unknown as HTMLElement;
|
||||||
const session = {
|
const session = {
|
||||||
@@ -136,6 +154,8 @@ describe("animated download column drag", () => {
|
|||||||
|
|
||||||
expect(committed).toBe(true);
|
expect(committed).toBe(true);
|
||||||
expect(animations).toEqual([]);
|
expect(animations).toEqual([]);
|
||||||
|
expect(querySelectorAll).toHaveBeenCalledTimes(1);
|
||||||
|
expect(querySelectorAll).toHaveBeenCalledWith("[data-column-dragging]");
|
||||||
expect(animate).not.toHaveBeenCalled();
|
expect(animate).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user