Skip to content

Commit ee3bb81

Browse files
JeanMechedylhunn
authored andcommitted
refactor(common): request low quality placeholder images (#54899)
For every built-in load, this commit adds a parameter to load low quality placeholder images. Using 20/100 as base value. PR Close #54899
1 parent 13554f9 commit ee3bb81

File tree

7 files changed

+92
-2
lines changed

7 files changed

+92
-2
lines changed

packages/common/src/directives/ng_optimized_image/image_loaders/cloudflare_loader.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {PLACEHOLDER_QUALITY} from './constants';
910
import {createImageLoader, ImageLoaderConfig} from './image_loader';
1011

1112
/**
@@ -29,6 +30,12 @@ function createCloudflareUrl(path: string, config: ImageLoaderConfig) {
2930
if (config.width) {
3031
params += `,width=${config.width}`;
3132
}
33+
34+
// When requesting a placeholder image we ask for a low quality image to reduce the load time.
35+
if (config.isPlaceholder) {
36+
params += `,quality=${PLACEHOLDER_QUALITY}`;
37+
}
38+
3239
// Cloudflare image URLs format:
3340
// https://developers.cloudflare.com/images/image-resizing/url-format/
3441
return `${path}/cdn-cgi/image/${params}/${config.src}`;

packages/common/src/directives/ng_optimized_image/image_loaders/cloudinary_loader.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,15 @@ function createCloudinaryUrl(path: string, config: ImageLoaderConfig) {
5252
// https://cloudinary.com/documentation/image_transformations#transformation_url_structure
5353
// Example of a Cloudinary image URL:
5454
// https://res.cloudinary.com/mysite/image/upload/c_scale,f_auto,q_auto,w_600/marketing/tile-topics-m.png
55-
let params = `f_auto,q_auto`; // sets image format and quality to "auto"
55+
56+
// For a placeholder image, we use the lowest image setting available to reduce the load time
57+
// else we use the auto size
58+
const quality = config.isPlaceholder ? 'q_auto:low' : 'q_auto';
59+
60+
let params = `f_auto,${quality}`;
5661
if (config.width) {
5762
params += `,w_${config.width}`;
5863
}
64+
5965
return `${path}/image/upload/${params}/${config.src}`;
6066
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
/**
10+
* Value (out of 100) of the requested quality for placeholder images.
11+
*/
12+
export const PLACEHOLDER_QUALITY = '20';

packages/common/src/directives/ng_optimized_image/image_loaders/imagekit_loader.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {PLACEHOLDER_QUALITY} from './constants';
910
import {createImageLoader, ImageLoaderConfig, ImageLoaderInfo} from './image_loader';
1011

1112
/**
@@ -53,5 +54,11 @@ export function createImagekitUrl(path: string, config: ImageLoaderConfig): stri
5354
urlSegments = [path, src];
5455
}
5556

56-
return urlSegments.join('/');
57+
const url = new URL(urlSegments.join('/'));
58+
59+
// When requesting a placeholder image we ask for a low quality image to reduce the load time.
60+
if (config.isPlaceholder) {
61+
url.searchParams.set('q', PLACEHOLDER_QUALITY);
62+
}
63+
return url.href;
5764
}

packages/common/src/directives/ng_optimized_image/image_loaders/imgix_loader.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {PLACEHOLDER_QUALITY} from './constants';
910
import {createImageLoader, ImageLoaderConfig, ImageLoaderInfo} from './image_loader';
1011

1112
/**
@@ -45,5 +46,10 @@ function createImgixUrl(path: string, config: ImageLoaderConfig) {
4546
if (config.width) {
4647
url.searchParams.set('w', config.width.toString());
4748
}
49+
50+
// When requesting a placeholder image we ask a low quality image to reduce the load time.
51+
if (config.isPlaceholder) {
52+
url.searchParams.set('q', PLACEHOLDER_QUALITY);
53+
}
4854
return url.href;
4955
}

packages/common/src/directives/ng_optimized_image/image_loaders/netlify_loader.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {RuntimeErrorCode} from '../../../errors';
1616
import {isAbsoluteUrl, isValidPath} from '../url';
1717

1818
import {IMAGE_LOADER, ImageLoaderConfig, ImageLoaderInfo} from './image_loader';
19+
import {PLACEHOLDER_QUALITY} from './constants';
1920

2021
/**
2122
* Name and URL tester for Netlify.
@@ -90,6 +91,13 @@ function createNetlifyUrl(config: ImageLoaderConfig, path?: string) {
9091
url.searchParams.set('w', config.width.toString());
9192
}
9293

94+
// When requesting a placeholder image we ask for a low quality image to reduce the load time.
95+
// If the quality is specified in the loader config - always use provided value.
96+
const configQuality = config.loaderParams?.['quality'] ?? config.loaderParams?.['q'];
97+
if (config.isPlaceholder && !configQuality) {
98+
url.searchParams.set('q', PLACEHOLDER_QUALITY);
99+
}
100+
93101
for (const [param, value] of Object.entries(config.loaderParams ?? {})) {
94102
if (validParams.has(param)) {
95103
url.searchParams.set(validParams.get(param)!, value.toString());

packages/common/test/image_loaders/image_loader_spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ describe('Built-in image directive loaders', () => {
7575
const loader = createImgixLoader(path);
7676
expect(() => loader({src})).toThrowError(absoluteUrlError(src, path));
7777
});
78+
79+
it('should load a low quality image when a placeholder is requested', () => {
80+
const path = 'https://somesite.imgix.net';
81+
const loader = createImgixLoader(path);
82+
const config = {src: 'img.png', isPlaceholder: true};
83+
expect(loader(config)).toBe(`${path}/img.png?auto=format&q=20`);
84+
});
7885
});
7986

8087
describe('Cloudinary loader', () => {
@@ -97,6 +104,13 @@ describe('Built-in image directive loaders', () => {
97104
).toBe(`${path}/image/upload/f_auto,q_auto/marketing/img-2.png`);
98105
});
99106

107+
it('should load a low quality image when a placeholder is requested', () => {
108+
const path = 'https://res.cloudinary.com/mysite';
109+
const loader = createCloudinaryLoader(path);
110+
const config = {src: 'img.png', isPlaceholder: true};
111+
expect(loader(config)).toBe(`${path}/image/upload/f_auto,q_auto:low/img.png`);
112+
});
113+
100114
describe('input validation', () => {
101115
it('should throw if an absolute URL is provided as a loader input', () => {
102116
const path = 'https://res.cloudinary.com/mysite';
@@ -154,6 +168,13 @@ describe('Built-in image directive loaders', () => {
154168
);
155169
});
156170

171+
it('should load a low quality image when a placeholder is requested', () => {
172+
const path = 'https://ik.imageengine.io/imagetest';
173+
const loader = createImageKitLoader(path);
174+
const config = {src: 'img.png', isPlaceholder: true};
175+
expect(loader(config)).toBe(`${path}/img.png?q=20`);
176+
});
177+
157178
describe('input validation', () => {
158179
it('should throw if an absolute URL is provided as a loader input', () => {
159180
const path = 'https://ik.imageengine.io/imagetest';
@@ -210,6 +231,15 @@ describe('Built-in image directive loaders', () => {
210231
const loader = createCloudflareLoader(path);
211232
expect(() => loader({src})).toThrowError(absoluteUrlError(src, path));
212233
});
234+
235+
it('should load a low quality image when a placeholder is requested', () => {
236+
const path = 'https://mysite.com';
237+
const loader = createCloudflareLoader(path);
238+
const config = {src: 'img.png', isPlaceholder: true};
239+
expect(loader(config)).toBe(
240+
'https://mysite.com/cdn-cgi/image/format=auto,quality=20/img.png',
241+
);
242+
});
213243
});
214244

215245
describe('Netlify loader', () => {
@@ -261,6 +291,20 @@ describe('Built-in image directive loaders', () => {
261291
`NG0${RuntimeErrorCode.INVALID_LOADER_ARGUMENTS}: The Netlify image loader has detected an \`<img>\` tag with the unsupported attribute "\`unknown\`".`,
262292
);
263293
});
294+
295+
it('should load a low quality image when a placeholder is requested', () => {
296+
const path = 'https://mysite.com';
297+
const loader = createNetlifyLoader(path);
298+
const config = {src: 'img.png', isPlaceholder: true};
299+
expect(loader(config)).toBe('https://mysite.com/.netlify/images?url=%2Fimg.png&q=20');
300+
});
301+
302+
it('should not load a low quality image when a placeholder is requested with a quality param', () => {
303+
const path = 'https://mysite.com';
304+
const loader = createNetlifyLoader(path);
305+
const config = {src: 'img.png', isPlaceholder: true, loaderParams: {quality: 50}};
306+
expect(loader(config)).toBe('https://mysite.com/.netlify/images?url=%2Fimg.png&q=50');
307+
});
264308
});
265309

266310
describe('loader utils', () => {

0 commit comments

Comments
 (0)