From 43dfd4183f60810c52a3c81005c8bb999058c1b8 Mon Sep 17 00:00:00 2001 From: "Marcus (bug-testing)" Date: Sun, 5 Jul 2026 00:14:15 -0500 Subject: [PATCH 1/5] fix(admin): lazy-load Media Library thumbnails Media Library page fetched 100 items on first load and rendered every thumbnail eagerly, firing ~100 concurrent resize requests through the Astro image endpoint on mount. Adds loading="lazy" to all thumbnail tags and trims the initial page size to 40. Co-Authored-By: Claude Sonnet 5 --- .changeset/media-library-lazy-thumbnails.md | 5 +++++ packages/admin/src/components/MediaLibrary.tsx | 4 ++++ packages/admin/src/router.tsx | 2 +- .../admin/tests/components/MediaLibrary.test.tsx | 15 +++++++++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .changeset/media-library-lazy-thumbnails.md diff --git a/.changeset/media-library-lazy-thumbnails.md b/.changeset/media-library-lazy-thumbnails.md new file mode 100644 index 0000000000..874723b8a7 --- /dev/null +++ b/.changeset/media-library-lazy-thumbnails.md @@ -0,0 +1,5 @@ +--- +"emdash": patch +--- + +Fixes slow load and scroll on the Media Library admin page. Thumbnails now use native `loading="lazy"` so the browser only fetches images as they scroll into view, instead of firing every visible page's worth of resize requests on mount. The initial page size is also reduced from 100 to 40 items. diff --git a/packages/admin/src/components/MediaLibrary.tsx b/packages/admin/src/components/MediaLibrary.tsx index ee01231531..e0433118c6 100644 --- a/packages/admin/src/components/MediaLibrary.tsx +++ b/packages/admin/src/components/MediaLibrary.tsx @@ -722,6 +722,7 @@ function MediaGridItem({ item, selected, onClick }: MediaGridItemProps) { {item.alt fallbackToOriginalThumbnail(e.currentTarget, item.url)} /> @@ -773,6 +774,7 @@ function ProviderGridItem({ item, selected, onClick, onDimensionsLoaded }: Provi {item.alt @@ -815,6 +817,7 @@ function MediaListItem({ item, selected, onClick }: MediaListItemProps) { {item.alt fallbackToOriginalThumbnail(e.currentTarget, item.url)} /> @@ -870,6 +873,7 @@ function ProviderListItem({ item, selected, onClick, onDimensionsLoaded }: Provi {item.alt diff --git a/packages/admin/src/router.tsx b/packages/admin/src/router.tsx index 4b8fb9530f..1c1af21a17 100644 --- a/packages/admin/src/router.tsx +++ b/packages/admin/src/router.tsx @@ -1312,7 +1312,7 @@ function MediaPage() { queryFn: ({ pageParam }) => fetchMediaList({ cursor: pageParam, - limit: 100, + limit: 40, search: search || undefined, mimeType: mimeFilter, }), diff --git a/packages/admin/tests/components/MediaLibrary.test.tsx b/packages/admin/tests/components/MediaLibrary.test.tsx index 555fdd927d..12153cc7e7 100644 --- a/packages/admin/tests/components/MediaLibrary.test.tsx +++ b/packages/admin/tests/components/MediaLibrary.test.tsx @@ -90,6 +90,14 @@ describe("MediaLibrary", () => { await expect.element(img).toBeInTheDocument(); await expect.element(img).toHaveAttribute("src", "https://example.com/photo.jpg"); }); + + it("grid thumbnails are natively lazy-loaded", async () => { + // A library page can hold up to 100 items; without `loading="lazy"` the + // browser fetches every thumbnail on mount instead of only the visible ones. + const items = [makeMediaItem({ id: "1", filename: "pic.jpg", mimeType: "image/jpeg" })]; + const screen = await renderLibrary({ items }); + await expect.element(screen.getByAltText("pic.jpg")).toHaveAttribute("loading", "lazy"); + }); }); describe("view mode toggle", () => { @@ -268,6 +276,13 @@ describe("MediaLibrary", () => { await expect.element(screen.getByText("application/pdf")).toBeInTheDocument(); await expect.element(screen.getByText("1 MB")).toBeInTheDocument(); }); + + it("list view thumbnails are natively lazy-loaded", async () => { + const items = [makeMediaItem({ id: "1", filename: "pic.jpg", mimeType: "image/jpeg" })]; + const screen = await renderLibrary({ items }); + await screen.getByRole("button", { name: "List view" }).click(); + await expect.element(screen.getByAltText("pic.jpg")).toHaveAttribute("loading", "lazy"); + }); }); describe("header", () => { From 365bb4c521a1d2a05741372e80ba47863e75f717 Mon Sep 17 00:00:00 2001 From: "Marcus (bug-testing)" Date: Sun, 19 Jul 2026 23:17:43 -0500 Subject: [PATCH 2/5] test(admin): cover provider thumbnail lazy loading, fix stale comment Addresses emdashbot review on #1827: ProviderGridItem/ProviderListItem thumbnails were untested, and the grid test comment still cited the old 100-item page size (now 40). --- .../tests/components/MediaLibrary.test.tsx | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/admin/tests/components/MediaLibrary.test.tsx b/packages/admin/tests/components/MediaLibrary.test.tsx index 12153cc7e7..84da2a77f0 100644 --- a/packages/admin/tests/components/MediaLibrary.test.tsx +++ b/packages/admin/tests/components/MediaLibrary.test.tsx @@ -3,8 +3,8 @@ import * as React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { MediaLibrary } from "../../src/components/MediaLibrary"; -import type { MediaItem } from "../../src/lib/api"; -import { deleteMedia } from "../../src/lib/api"; +import { deleteMedia, fetchMediaProviders, fetchProviderMedia } from "../../src/lib/api"; +import type { MediaItem, MediaProviderItem } from "../../src/lib/api"; import { render } from "../utils/render.tsx"; // --------------------------------------------------------------------------- @@ -92,7 +92,7 @@ describe("MediaLibrary", () => { }); it("grid thumbnails are natively lazy-loaded", async () => { - // A library page can hold up to 100 items; without `loading="lazy"` the + // A library page can hold up to 40 items; without `loading="lazy"` the // browser fetches every thumbnail on mount instead of only the visible ones. const items = [makeMediaItem({ id: "1", filename: "pic.jpg", mimeType: "image/jpeg" })]; const screen = await renderLibrary({ items }); @@ -100,6 +100,46 @@ describe("MediaLibrary", () => { }); }); + describe("provider thumbnails", () => { + function makeProviderItem(overrides: Partial = {}): MediaProviderItem { + return { + id: "provider_01", + filename: "cloud-pic.jpg", + mimeType: "image/jpeg", + previewUrl: "https://provider.example.com/cloud-pic.jpg", + width: 800, + height: 600, + ...overrides, + }; + } + + function mockProvider(items: MediaProviderItem[]) { + vi.mocked(fetchMediaProviders).mockResolvedValue([ + { + id: "acme", + name: "Acme Provider", + capabilities: { upload: false, search: false, delete: false }, + }, + ]); + vi.mocked(fetchProviderMedia).mockResolvedValue({ items }); + } + + it("provider grid thumbnails are natively lazy-loaded", async () => { + mockProvider([makeProviderItem()]); + const screen = await renderLibrary(); + await screen.getByRole("button", { name: "Acme Provider" }).click(); + await expect.element(screen.getByAltText("cloud-pic.jpg")).toHaveAttribute("loading", "lazy"); + }); + + it("provider list thumbnails are natively lazy-loaded", async () => { + mockProvider([makeProviderItem()]); + const screen = await renderLibrary(); + await screen.getByRole("button", { name: "Acme Provider" }).click(); + await screen.getByRole("button", { name: "List view" }).click(); + await expect.element(screen.getByAltText("cloud-pic.jpg")).toHaveAttribute("loading", "lazy"); + }); + }); + describe("view mode toggle", () => { it("switches between grid and list view", async () => { const items = [makeMediaItem({ id: "1", filename: "test.jpg" })]; From 39f050233038495477bec5838ed74c4e6bd8afd7 Mon Sep 17 00:00:00 2001 From: "Marcus (bug-testing)" Date: Tue, 21 Jul 2026 23:38:36 -0500 Subject: [PATCH 3/5] chore: sync lockfile to upstream main Co-Authored-By: Claude Sonnet 5 --- pnpm-lock.yaml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d556eb31dc..fb92bee22e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -468,6 +468,40 @@ importers: specifier: 'catalog:' version: 4.100.0(@cloudflare/workers-types@4.20260305.1) + demos/emdash-dev-main: + dependencies: + '@astrojs/cloudflare': + specifier: 'catalog:' + version: 14.0.0(@types/node@26.1.1)(astro@7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@26.1.1)(aws4fetch@1.0.20)(jiti@2.7.0)(rollup@4.55.2)(tsx@4.21.0)(yaml@2.9.0))(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.21.0)(workerd@1.20260611.1)(wrangler@4.100.0(@cloudflare/workers-types@4.20260305.1))(yaml@2.9.0) + '@astrojs/react': + specifier: 'catalog:' + version: 6.0.0(@types/node@26.1.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(esbuild@0.28.1)(jiti@2.7.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tsx@4.21.0)(yaml@2.9.0) + '@emdash-cms/cloudflare': + specifier: workspace:* + version: link:../../packages/cloudflare + astro: + specifier: 'catalog:' + version: 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@26.1.1)(aws4fetch@1.0.20)(jiti@2.7.0)(rollup@4.55.2)(tsx@4.21.0)(yaml@2.9.0) + emdash: + specifier: workspace:* + version: link:../../packages/core + react: + specifier: 'catalog:' + version: 19.2.4 + react-dom: + specifier: 'catalog:' + version: 19.2.4(react@19.2.4) + devDependencies: + '@astrojs/check': + specifier: 'catalog:' + version: 0.9.7(prettier-plugin-astro@0.14.1)(prettier@3.9.5)(typescript@6.0.0-beta) + '@cloudflare/workers-types': + specifier: 'catalog:' + version: 4.20260305.1 + wrangler: + specifier: 'catalog:' + version: 4.100.0(@cloudflare/workers-types@4.20260305.1) + demos/playground: dependencies: '@astrojs/cloudflare': From bd3835017bd3b77654cce01133a9d9ec9ac89f58 Mon Sep 17 00:00:00 2001 From: "Marcus (bug-testing)" Date: Wed, 22 Jul 2026 13:06:38 -0500 Subject: [PATCH 4/5] ci: retrigger From d0547c329613870fc0fbad83bf4ece7ef96b74b4 Mon Sep 17 00:00:00 2001 From: "Marcus (bug-testing)" Date: Wed, 22 Jul 2026 13:41:13 -0500 Subject: [PATCH 5/5] fix(admin): correct ARIA role in new lazy-load thumbnail tests List view and provider tabs are rendered by Kumo's Tabs component (role="tab"), not plain buttons. The new lazy-load tests used getByRole("button", ...) for them, so the click never resolved and the test timed out instead of asserting anything. Co-Authored-By: Claude Sonnet 5 --- packages/admin/tests/components/MediaLibrary.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/admin/tests/components/MediaLibrary.test.tsx b/packages/admin/tests/components/MediaLibrary.test.tsx index 84da2a77f0..7e54ad1e0a 100644 --- a/packages/admin/tests/components/MediaLibrary.test.tsx +++ b/packages/admin/tests/components/MediaLibrary.test.tsx @@ -127,15 +127,15 @@ describe("MediaLibrary", () => { it("provider grid thumbnails are natively lazy-loaded", async () => { mockProvider([makeProviderItem()]); const screen = await renderLibrary(); - await screen.getByRole("button", { name: "Acme Provider" }).click(); + await screen.getByRole("tab", { name: "Acme Provider" }).click(); await expect.element(screen.getByAltText("cloud-pic.jpg")).toHaveAttribute("loading", "lazy"); }); it("provider list thumbnails are natively lazy-loaded", async () => { mockProvider([makeProviderItem()]); const screen = await renderLibrary(); - await screen.getByRole("button", { name: "Acme Provider" }).click(); - await screen.getByRole("button", { name: "List view" }).click(); + await screen.getByRole("tab", { name: "Acme Provider" }).click(); + await screen.getByRole("tab", { name: "List view" }).click(); await expect.element(screen.getByAltText("cloud-pic.jpg")).toHaveAttribute("loading", "lazy"); }); }); @@ -320,7 +320,7 @@ describe("MediaLibrary", () => { it("list view thumbnails are natively lazy-loaded", async () => { const items = [makeMediaItem({ id: "1", filename: "pic.jpg", mimeType: "image/jpeg" })]; const screen = await renderLibrary({ items }); - await screen.getByRole("button", { name: "List view" }).click(); + await screen.getByRole("tab", { name: "List view" }).click(); await expect.element(screen.getByAltText("pic.jpg")).toHaveAttribute("loading", "lazy"); }); });