perf(ui): download only images and optimize image selection for document edit view, prioritize best-fit size (#11844)

### What?

In the same vein as #11696, this PR optimizes how images are selected
for display in the document edit view. It ensures that only image files
are processed and selects the most appropriate size to minimize
unnecessary downloads and improve performance.

#### Previously:

- Non-image files were being processed unnecessarily, despite not
generating thumbnails.
- Images without a `thumbnailURL` defaulted to their original full size,
even when smaller, optimized versions were available.

#### Now:

- **Only images** are processed for thumbnails, avoiding redundant
requests for non-images.
- **The smallest available image within a target range** (`40px -
180px`) is prioritized for display.
- **If no images fit within this range**, the logic selects:
  - The next smallest larger image (if available).
- The **original** image if it is smaller than the next available larger
size.
  - The largest **smaller** image if no better fit exists.

### Why?

Prevents unnecessary downloads of non-image files, reduces bandwidth
usage by selecting more efficient image sizes and improves load times
and performance in the edit view.

### How?

- **Filters out non-image files** when determining which assets to
display.
- Uses the same algorithm as in #11696 but turns it into a reusable
function to be used in various areas around the codebase. Namely the
upload field hasOne and hasMany components.

Before (4.5mb transfer):

![edit-view-before](https://github.com/user-attachments/assets/ff3513b7-b874-48c3-bce7-8a9425243e00)

After (15.9kb transfer):

![edit-view-after](https://github.com/user-attachments/assets/fce8c463-65ae-4f1d-81b5-8781e89f06f1)
This commit is contained in:
Said Akhrarov
2025-03-26 16:13:52 -04:00
committed by GitHub
parent 98e4db07c3
commit 5ae5255ba3
8 changed files with 217 additions and 50 deletions

View File

@@ -735,6 +735,31 @@ export default buildConfigWithDefaults({
},
],
},
{
slug: 'best-fit',
fields: [
{
name: 'withAdminThumbnail',
type: 'upload',
relationTo: 'admin-thumbnail-function',
},
{
name: 'withinRange',
type: 'upload',
relationTo: enlargeSlug,
},
{
name: 'nextSmallestOutOfRange',
type: 'upload',
relationTo: 'focal-only',
},
{
name: 'original',
type: 'upload',
relationTo: 'focal-only',
},
],
},
],
onInit: async (payload) => {
const uploadsDir = path.resolve(dirname, './media')

View File

@@ -66,6 +66,7 @@ let uploadsOne: AdminUrlUtil
let uploadsTwo: AdminUrlUtil
let customUploadFieldURL: AdminUrlUtil
let hideFileInputOnCreateURL: AdminUrlUtil
let bestFitURL: AdminUrlUtil
let consoleErrorsFromPage: string[] = []
let collectErrorsFromPage: () => boolean
let stopCollectingErrorsFromPage: () => boolean
@@ -99,6 +100,7 @@ describe('Uploads', () => {
uploadsTwo = new AdminUrlUtil(serverURL, 'uploads-2')
customUploadFieldURL = new AdminUrlUtil(serverURL, customUploadFieldSlug)
hideFileInputOnCreateURL = new AdminUrlUtil(serverURL, hideFileInputOnCreateSlug)
bestFitURL = new AdminUrlUtil(serverURL, 'best-fit')
const context = await browser.newContext()
page = await context.newPage()
@@ -1349,4 +1351,53 @@ describe('Uploads', () => {
await expect(page.locator('.file-field .file-details__remove')).toBeHidden()
})
describe('imageSizes best fit', () => {
test('should select adminThumbnail if one exists', async () => {
await page.goto(bestFitURL.create)
await page.locator('#field-withAdminThumbnail button.upload__listToggler').click()
await page.locator('tr.row-1 td.cell-filename button.default-cell__first-cell').click()
const thumbnail = page.locator('#field-withAdminThumbnail div.thumbnail > img')
await expect(thumbnail).toHaveAttribute(
'src',
'https://payloadcms.com/images/universal-truth.jpg',
)
})
test('should select an image within target range', async () => {
await page.goto(bestFitURL.create)
await page.locator('#field-withinRange button.upload__createNewToggler').click()
const fileChooserPromise = page.waitForEvent('filechooser')
await page.getByText('Select a file').click()
const fileChooser = await fileChooserPromise
await fileChooser.setFiles(path.join(dirname, 'test-image.jpg'))
await page.locator('dialog button#action-save').click()
const thumbnail = page.locator('#field-withinRange div.thumbnail > img')
await expect(thumbnail).toHaveAttribute('src', '/api/enlarge/file/test-image-180x50.jpg')
})
test('should select next smallest image outside of range but smaller than original', async () => {
await page.goto(bestFitURL.create)
await page.locator('#field-nextSmallestOutOfRange button.upload__createNewToggler').click()
const fileChooserPromise = page.waitForEvent('filechooser')
await page.getByText('Select a file').click()
const fileChooser = await fileChooserPromise
await fileChooser.setFiles(path.join(dirname, 'test-image.jpg'))
await page.locator('dialog button#action-save').click()
const thumbnail = page.locator('#field-nextSmallestOutOfRange div.thumbnail > img')
await expect(thumbnail).toHaveAttribute('src', '/api/focal-only/file/test-image-400x300.jpg')
})
test('should select original if smaller than next available size', async () => {
await page.goto(bestFitURL.create)
await page.locator('#field-original button.upload__createNewToggler').click()
const fileChooserPromise = page.waitForEvent('filechooser')
await page.getByText('Select a file').click()
const fileChooser = await fileChooserPromise
await fileChooser.setFiles(path.join(dirname, 'small.png'))
await page.locator('dialog button#action-save').click()
const thumbnail = page.locator('#field-original div.thumbnail > img')
await expect(thumbnail).toHaveAttribute('src', '/api/focal-only/file/small.png')
})
})
})

View File

@@ -101,6 +101,7 @@ export interface Config {
'media-without-relation-preview': MediaWithoutRelationPreview;
'relation-preview': RelationPreview;
'hide-file-input-on-create': HideFileInputOnCreate;
'best-fit': BestFit;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
@@ -142,6 +143,7 @@ export interface Config {
'media-without-relation-preview': MediaWithoutRelationPreviewSelect<false> | MediaWithoutRelationPreviewSelect<true>;
'relation-preview': RelationPreviewSelect<false> | RelationPreviewSelect<true>;
'hide-file-input-on-create': HideFileInputOnCreateSelect<false> | HideFileInputOnCreateSelect<true>;
'best-fit': BestFitSelect<false> | BestFitSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -1260,6 +1262,19 @@ export interface RelationPreview {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "best-fit".
*/
export interface BestFit {
id: string;
withAdminThumbnail?: (string | null) | AdminThumbnailFunction;
withinRange?: (string | null) | Enlarge;
nextSmallestOutOfRange?: (string | null) | FocalOnly;
original?: (string | null) | FocalOnly;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
@@ -1420,6 +1435,10 @@ export interface PayloadLockedDocument {
relationTo: 'hide-file-input-on-create';
value: string | HideFileInputOnCreate;
} | null)
| ({
relationTo: 'best-fit';
value: string | BestFit;
} | null)
| ({
relationTo: 'users';
value: string | User;
@@ -2632,6 +2651,18 @@ export interface HideFileInputOnCreateSelect<T extends boolean = true> {
focalX?: T;
focalY?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "best-fit_select".
*/
export interface BestFitSelect<T extends boolean = true> {
withAdminThumbnail?: T;
withinRange?: T;
nextSmallestOutOfRange?: T;
original?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".