Skip to content

Commit

Permalink
feat(common): add placeholder to NgOptimizedImage (#53783)
Browse files Browse the repository at this point in the history
Add a automatic placeholder implementation supporting loader-based and data URL placeholders

PR Close #53783
  • Loading branch information
atcastle authored and thePunderWoman committed Jan 29, 2024
1 parent f3a2b49 commit f5c520b
Show file tree
Hide file tree
Showing 11 changed files with 525 additions and 9 deletions.
51 changes: 51 additions & 0 deletions adev/src/content/guide/image-optimization.md
Expand Up @@ -112,6 +112,57 @@ You can also style your image with the [object-position property](https://develo

IMPORTANT: For the "fill" image to render properly, its parent element **must** be styled with `position: "relative"`, `position: "fixed"`, or `position: "absolute"`.

## Using placeholders

### Automatic placeholders

NgOptimizedImage can display an automatic low-resolution placeholder for your image if you're using a CDN or image host that provides automatic image resizing. Take advantage of this feature by adding the `placeholder` attribute to your image:

<code-example format="typescript" language="typescript">

&lt;img ngSrc="cat.jpg" width="400" height="200" placeholder&gt;

</code-example>

Adding this attribute automatically requests a second, smaller version of the image using your specified image loader. This small image will be applied as a `background-image` style with a CSS blur while your image loads. If no image loader is provided, no placeholder image can be generated and an error will be thrown.

The default size for generated placeholders is 30px wide. You can change this size by specifying a pixel value in the `IMAGE_CONFIG` provider, as seen below:

<code-example format="typescript" language="typescript">
providers: [
{
provide: IMAGE_CONFIG,
useValue: {
placeholderResolution: 40
}
},
],
</code-example>

If you want sharp edges around your blurred placeholder, you can wrap your image in a containing `<div>` with the `overflow: hidden` style. As long as the `<div>` is the same size as the image (such as by using the `width: fit-content` style), the "fuzzy edges" of the placeholder will be hidden.

### Data URL placeholders

You can also specify a placeholder using a base64 [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs) without an image loader. The data url format is `data:image/[imagetype];[data]`, where `[imagetype]` is the image format, just as `png`, and `[data]` is a base64 encoding of the image. That encoding can be done using the command line or in JavaScript. For specific commands, see [the MDN documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs#encoding_data_into_base64_format). An example of a data URL placeholder with truncated data is shown below:

<code-example format="typescript" language="typescript">

&lt;img ngSrc="cat.jpg" width="400" height="200" placeholder="data:image/png;base64,iVBORw0K..."&gt;

</code-example>

However, large data URLs increase the size of your Angular bundles and slow down page load. If you cannot use an image loader, the Angular team recommends keeping base64 placeholder images smaller than 4KB and using them exclusively on critical images. In addition to decreasing placeholder dimensions, consider changing image formats or parameters used when saving images. At very low resolutions, these parameters can have a large effect on file size.

### Non-blurred placeholders

By default, NgOptimizedImage applies a CSS blur effect to image placeholders. To render a placeholder without blur, provide a `placeholderConfig` argument with an object that includes the `blur` property, set to false. For example:

<code-example format="typescript" language="typescript">

&lt;img ngSrc="cat.jpg" width="400" height="200" placeholder [placeholderConfig]="{blur: false}"&gt;

</code-example>

## Adjusting image styling

Depending on the image's styling, adding `width` and `height` attributes may cause the image to render differently. `NgOptimizedImage` warns you if your image styling renders the image at a distorted aspect ratio.
Expand Down
53 changes: 52 additions & 1 deletion aio/content/guide/image-directive.md
Expand Up @@ -103,7 +103,58 @@ You can also style your image with the [object-position property](https://develo

**Important note:** For the "fill" image to render properly, its parent element **must** be styled with `position: "relative"`, `position: "fixed"`, or `position: "absolute"`.

### Adjusting image styling
## Using placeholders

### Automatic placeholders

NgOptimizedImage can provide an automatic low-resolution placeholder for your image, as long as you're using a CDN or image host which provides automatic image resizing. To take advantage of this feature, just add the `placeholder` attribute to your image, as so:

<code-example format="typescript" language="typescript">

&lt;img ngSrc="cat.jpg" width="400" height="200" placeholder&gt;

</code-example>

Adding this attribute will automatically request a second, smaller version of the image, using your specified image loader. This small image will be applied as a `background-image` style with a CSS blur while your image loads. If no image loader is provided, no placeholder image can be generated and an error will be thrown.

The default size for generated placeholders is 30px wide, but you can make it larger or smaller by specifying a value in the `IMAGE_CONFIG` provider, as seen below:

<code-example format="typescript" language="typescript">
providers: [
{
provide: IMAGE_CONFIG,
useValue: {
placeholderResolution: 40
}
},
],
</code-example>

If you want sharp edges around your blurred placeholder, you can wrap your image in a containing `<div>` with the `overflow: hidden` style. As long as the `<div>` is the same size as the image (such as by using the `width: fit-content` style), the "fuzzy edges" of the placeholder will be hidden.

### Data URL placeholders

You can also specify a placeholder using a base64 [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs) without an image loader. The data url format is `data:image/[imagetype];[data]`, where `[imagetype]` is the image format, just as `png`, and `[data]` is a base64 encoding of the image. That encoding can be done using the command line or in JavaScript. For specific commands, see [the MDN documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs#encoding_data_into_base64_format). An example of a data URL placeholder with truncated data is shown below:

<code-example format="typescript" language="typescript">

&lt;img ngSrc="cat.jpg" width="400" height="200" placeholder="data:image/png;base64,iVBORw0K..."&gt;

</code-example>

However, large data URLs increase the size of your Angular bundles and slow down page load. If you cannot use an image loader, the Angular team recommends keeping base64 placeholder images smaller than 4KB and using them exclusively on critical images. In addition to decreasing placeholder dimensions, consider changing image formats or parameters used when saving images. At very low resolutions, these parameters can have a large effect on file size.

### Non-blurred placeholders

By default, NgOptimizedImage applies a CSS blur effect to image placeholders. To render a placeholder without blur, provide a `placeholderConfig` argument with an object that includes the `blur` property, set to false. For example:

<code-example format="typescript" language="typescript">

&lt;img ngSrc="cat.jpg" width="400" height="200" placeholder [placeholderConfig]="{blur: false}"&gt;

</code-example>

## Adjusting image styling

Depending on the image's styling, adding `width` and `height` attributes may cause the image to render differently. `NgOptimizedImage` warns you if your image styling renders the image at a distorted aspect ratio.

Expand Down
2 changes: 2 additions & 0 deletions goldens/public-api/common/errors.md
Expand Up @@ -27,6 +27,8 @@ export const enum RuntimeErrorCode {
// (undocumented)
OVERSIZED_IMAGE = 2960,
// (undocumented)
OVERSIZED_PLACEHOLDER = 2965,
// (undocumented)
PARENT_NG_SWITCH_NOT_FOUND = 2000,
// (undocumented)
PRIORITY_IMG_MISSING_PRECONNECT_TAG = 2956,
Expand Down
13 changes: 12 additions & 1 deletion goldens/public-api/common/index.md
Expand Up @@ -325,13 +325,20 @@ export type ImageLoader = (config: ImageLoaderConfig) => string;

// @public
export interface ImageLoaderConfig {
isPlaceholder?: boolean;
loaderParams?: {
[key: string]: any;
};
src: string;
width?: number;
}

// @public
export interface ImagePlaceholderConfig {
// (undocumented)
blur?: boolean;
}

// @public
export function isPlatformBrowser(platformId: Object): boolean;

Expand Down Expand Up @@ -617,6 +624,8 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
// (undocumented)
static ngAcceptInputType_ngSrc: string | i0SafeValue;
// (undocumented)
static ngAcceptInputType_placeholder: boolean | string;
// (undocumented)
static ngAcceptInputType_priority: unknown;
// (undocumented)
static ngAcceptInputType_width: unknown;
Expand All @@ -628,11 +637,13 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
ngOnInit(): void;
ngSrc: string;
ngSrcset: string;
placeholder?: string | boolean;
placeholderConfig?: ImagePlaceholderConfig;
priority: boolean;
sizes?: string;
width: number | undefined;
// (undocumented)
static ɵdir: i0.ɵɵDirectiveDeclaration<NgOptimizedImage, "img[ngSrc]", never, { "ngSrc": { "alias": "ngSrc"; "required": true; }; "ngSrcset": { "alias": "ngSrcset"; "required": false; }; "sizes": { "alias": "sizes"; "required": false; }; "width": { "alias": "width"; "required": false; }; "height": { "alias": "height"; "required": false; }; "loading": { "alias": "loading"; "required": false; }; "priority": { "alias": "priority"; "required": false; }; "loaderParams": { "alias": "loaderParams"; "required": false; }; "disableOptimizedSrcset": { "alias": "disableOptimizedSrcset"; "required": false; }; "fill": { "alias": "fill"; "required": false; }; "src": { "alias": "src"; "required": false; }; "srcset": { "alias": "srcset"; "required": false; }; }, {}, never, never, true, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<NgOptimizedImage, "img[ngSrc]", never, { "ngSrc": { "alias": "ngSrc"; "required": true; }; "ngSrcset": { "alias": "ngSrcset"; "required": false; }; "sizes": { "alias": "sizes"; "required": false; }; "width": { "alias": "width"; "required": false; }; "height": { "alias": "height"; "required": false; }; "loading": { "alias": "loading"; "required": false; }; "priority": { "alias": "priority"; "required": false; }; "loaderParams": { "alias": "loaderParams"; "required": false; }; "disableOptimizedSrcset": { "alias": "disableOptimizedSrcset"; "required": false; }; "fill": { "alias": "fill"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "placeholderConfig": { "alias": "placeholderConfig"; "required": false; }; "src": { "alias": "src"; "required": false; }; "srcset": { "alias": "srcset"; "required": false; }; }, {}, never, never, true, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<NgOptimizedImage, never>;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/common.ts
Expand Up @@ -27,5 +27,5 @@ export {PLATFORM_BROWSER_ID as ɵPLATFORM_BROWSER_ID, PLATFORM_SERVER_ID as ɵPL
export {VERSION} from './version';
export {ViewportScroller, NullViewportScroller as ɵNullViewportScroller} from './viewport_scroller';
export {XhrFactory} from './xhr';
export {IMAGE_CONFIG, ImageConfig, IMAGE_LOADER, ImageLoader, ImageLoaderConfig, NgOptimizedImage, PRECONNECT_CHECK_BLOCKLIST, provideCloudflareLoader, provideCloudinaryLoader, provideImageKitLoader, provideImgixLoader} from './directives/ng_optimized_image';
export {IMAGE_CONFIG, ImageConfig, IMAGE_LOADER, ImageLoader, ImageLoaderConfig, NgOptimizedImage, ImagePlaceholderConfig, PRECONNECT_CHECK_BLOCKLIST, provideCloudflareLoader, provideCloudinaryLoader, provideImageKitLoader, provideImgixLoader} from './directives/ng_optimized_image';
export {normalizeQueryParams as ɵnormalizeQueryParams} from './location/util';
Expand Up @@ -27,6 +27,11 @@ export interface ImageLoaderConfig {
* Width of the requested image (to be used when generating srcset).
*/
width?: number;
/**
* Whether the loader should generate a URL for a small image placeholder instead of a full-sized
* image.
*/
isPlaceholder?: boolean;
/**
* Additional user-provided parameters for use by the ImageLoader.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/directives/ng_optimized_image/index.ts
Expand Up @@ -13,5 +13,5 @@ export {provideCloudinaryLoader} from './image_loaders/cloudinary_loader';
export {IMAGE_LOADER, ImageLoader, ImageLoaderConfig} from './image_loaders/image_loader';
export {provideImageKitLoader} from './image_loaders/imagekit_loader';
export {provideImgixLoader} from './image_loaders/imgix_loader';
export {NgOptimizedImage} from './ng_optimized_image';
export {ImagePlaceholderConfig, NgOptimizedImage} from './ng_optimized_image';
export {PRECONNECT_CHECK_BLOCKLIST} from './preconnect_link_checker';

0 comments on commit f5c520b

Please sign in to comment.