Skip to content

fix(admin): lazy-load Media Library thumbnails#1827

Open
marcusbellamyshaw-cell wants to merge 5 commits into
emdash-cms:mainfrom
Emdash-Bug-Testing:fix/media-library-lazy-thumbnails
Open

fix(admin): lazy-load Media Library thumbnails#1827
marcusbellamyshaw-cell wants to merge 5 commits into
emdash-cms:mainfrom
Emdash-Bug-Testing:fix/media-library-lazy-thumbnails

Conversation

@marcusbellamyshaw-cell

Copy link
Copy Markdown
Contributor

What does this PR do?

The admin Media Library page (/_emdash/admin/media) loads and scrolls slowly on libraries with many assets. The first page fetches up to 100 items, and every thumbnail <img> (grid + list views, local + provider tabs) rendered without loading="lazy", so the browser fired every thumbnail's resize request on mount instead of only the visible ones. Each resize also round-trips through Astro's _image endpoint, so 100 concurrent requests stack up real cost on first paint.

This PR adds native loading="lazy" to all four thumbnail <img> tags in MediaLibrary.tsx and drops the initial page size from 100 to 40 (router.tsx) — existing cursor-based "Load More" pagination covers the rest.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (targeted: packages/admin/tests/components/MediaLibrary.test.tsx)
  • pnpm format has been run
  • I have added/updated tests for my changes
  • User-visible strings in the admin UI are wrapped for translation (n/a — no new strings)
  • I have added a changeset
  • New features link to an approved Discussion (n/a — bug fix)

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Sonnet 5

Screenshots / test output

 Test Files  1 passed (1)
      Tests  17 passed (17)

@changeset-bot

changeset-bot Bot commented Jul 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d0547c3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jul 5, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1827

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1827

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@1827

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1827

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1827

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@1827

emdash

npm i https://pkg.pr.new/emdash@1827

create-emdash

npm i https://pkg.pr.new/create-emdash@1827

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1827

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@1827

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@1827

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@1827

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@1827

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@1827

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@1827

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1827

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1827

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1827

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1827

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1827

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1827

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@1827

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1827

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1827

commit: d0547c3

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@github-actions

Copy link
Copy Markdown
Contributor

This PR has been inactive for 14 days. It will be closed automatically in 7 days if there is no further activity.

If you're still working on this, please push an update or leave a comment.

@github-actions github-actions Bot added stale and removed stale labels Jul 19, 2026
@marcusbellamyshaw-cell
marcusbellamyshaw-cell marked this pull request as ready for review July 20, 2026 03:57
@github-actions github-actions Bot added the review/needs-review No maintainer or bot review yet label Jul 20, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right fix for the reported problem: native loading="lazy" directly cuts the first-paint image request storm, and dropping the initial page size from 100 to 40 is a sensible complement that also lowers DOM and data cost. The implementation is minimal and safe — I don't see logic bugs, security issues, or backwards-incompatibility concerns.

What I checked:

  • The four changed <img> tags in MediaLibrary.tsx (local + provider grid/list) now carry loading="lazy".
  • The limit: 40 change in router.tsx is a straightforward constant change; cursor pagination still works.
  • The targeted tests pass according to the PR output (I didn't run them; verified statically).
  • No new user-facing strings, no Tailwind direction issues, no SQL/API/auth changes, and a changeset is present.

Headline gaps:

  1. Provider thumbnails are untested. The PR touches ProviderGridItem and ProviderListItem but the new tests only assert lazy loading for the local grid/list views. AGENTS.md expects TDD for bugs, so provider cases should be added.
  2. Stale test comment. The new grid test still says "A library page can hold up to 100 items", but the page size was reduced to 40.
  3. MediaPickerModal uses the same thumbnail pattern without lazy loading. That's a natural follow-up for consistency once this lands.

One subtle thing to keep an eye on: provider thumbnails also fire onLoad to backfill missing width/height for providers that don't return dimensions. With lazy loading, that backfill won't happen until the image scrolls into view, so selecting an off-screen provider item before its thumbnail loads could leave the detail panel's dimensions line empty until reselection. If providers usually send dimensions, this is a non-issue.

await screen.getByRole("button", { name: "List view" }).click();
await expect.element(screen.getByAltText("pic.jpg")).toHaveAttribute("loading", "lazy");
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] The PR adds loading="lazy" to all four thumbnail <img> tags, including ProviderGridItem and ProviderListItem, but the new tests only assert it for the local grid and list views. Per AGENTS.md's "TDD for bugs" convention, the provider paths should have matching regression coverage too.

Add provider-tab tests analogous to the local ones. This will need fetchMediaProviders and fetchProviderMedia to be imported from ../../src/lib/api so they can be mocked per-test.

Suggested change
});
});
describe("provider thumbnails", () => {
it("provider grid thumbnails are natively lazy-loaded", async () => {
vi.mocked(fetchMediaProviders).mockResolvedValue([
{
id: "demo",
name: "Demo Provider",
capabilities: { browse: true, search: false, upload: false, delete: false },
},
]);
vi.mocked(fetchProviderMedia).mockResolvedValue({
items: [
{
id: "1",
filename: "provider.jpg",
mimeType: "image/jpeg",
previewUrl: "https://example.com/provider.jpg",
},
],
});
const screen = await renderLibrary();
await screen.getByRole("button", { name: "Demo Provider" }).click();
await expect
.element(screen.getByAltText("provider.jpg"))
.toHaveAttribute("loading", "lazy");
});
it("provider list view thumbnails are natively lazy-loaded", async () => {
vi.mocked(fetchMediaProviders).mockResolvedValue([
{
id: "demo",
name: "Demo Provider",
capabilities: { browse: true, search: false, upload: false, delete: false },
},
]);
vi.mocked(fetchProviderMedia).mockResolvedValue({
items: [
{
id: "1",
filename: "provider.jpg",
mimeType: "image/jpeg",
previewUrl: "https://example.com/provider.jpg",
},
],
});
const screen = await renderLibrary();
await screen.getByRole("button", { name: "List view" }).click();
await screen.getByRole("button", { name: "Demo Provider" }).click();
await expect
.element(screen.getByAltText("provider.jpg"))
.toHaveAttribute("loading", "lazy");
});
});

});

it("grid thumbnails are natively lazy-loaded", async () => {
// A library page can hold up to 100 items; without `loading="lazy"` the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This comment now describes the old page size. The initial limit was reduced from 100 to 40 in packages/admin/src/router.tsx, so the comment is misleading.

Suggested change
// 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

marcusbellamyshaw-cell added a commit to Emdash-Bug-Testing/emdash that referenced this pull request Jul 20, 2026
Addresses emdashbot review on emdash-cms#1827: ProviderGridItem/ProviderListItem
thumbnails were untested, and the grid test comment still cited the
old 100-item page size (now 40).
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review needs-rebase size/M cla: signed and removed review/needs-review No maintainer or bot review yet size/S labels Jul 20, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
@marcusbellamyshaw-cell
marcusbellamyshaw-cell force-pushed the fix/media-library-lazy-thumbnails branch from e9af783 to a1a7db9 Compare July 22, 2026 04:20
marcusbellamyshaw-cell added a commit to Emdash-Bug-Testing/emdash that referenced this pull request Jul 22, 2026
Addresses emdashbot review on emdash-cms#1827: ProviderGridItem/ProviderListItem
thumbnails were untested, and the grid test comment still cited the
old 100-item page size (now 40).
@github-actions github-actions Bot added query-count changed PR diff modifies query-count snapshot files area/ci area/docs size/XL and removed size/M labels Jul 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
packages/admin/src/locales/ar/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/de/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/en-GB/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/en/messages.po Source changed, localizations will be marked as outdated.
packages/admin/src/locales/es-419/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/es-ES/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/eu/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/fa/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/fr/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/hu/messages.po Localization added, will be marked as complete. 🔄️
packages/admin/src/locales/id/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/ja/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/ko/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/nb/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/pl/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/pseudo/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/pt-BR/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/th/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/tr/messages.po Localization added, will be marked as complete. 🔄️
packages/admin/src/locales/zh-CN/messages.po Localization changed, will be marked as complete. 🔄️
packages/admin/src/locales/zh-TW/messages.po Localization changed, will be marked as complete. 🔄️
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

marcusbellamyshaw-cell and others added 3 commits July 21, 2026 23:32
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
<img> tags and trims the initial page size to 40.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Addresses emdashbot review on emdash-cms#1827: ProviderGridItem/ProviderListItem
thumbnails were untested, and the grid test comment still cited the
old 100-item page size (now 40).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@marcusbellamyshaw-cell
marcusbellamyshaw-cell force-pushed the fix/media-library-lazy-thumbnails branch from a1a7db9 to 39f0502 Compare July 22, 2026 04:38
@github-actions github-actions Bot added size/M and removed size/XL needs-rebase query-count changed PR diff modifies query-count snapshot files labels Jul 22, 2026
marcusbellamyshaw-cell and others added 2 commits July 22, 2026 13:06
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 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants