Skip to content

Commit

Permalink
feat(common): add <link> preload tag on server for priority img
Browse files Browse the repository at this point in the history
While in Angular Universal, for images that are priority add a preload tag to the to ensure the image is preloaded before it is rendered. This resolves a warning when running Lighthouse.

docs(common): add explanation for ng optimized image constants

Add documentation for the default image limit and preloaded images injection token

fix(common): use plain method parameters for createPreloadLinkTag

Convert method back to plain method parameters instead of spread object

docs(common): explain each of the params for createPreloadLinkTag

Add additional documentation explaining the parameters to the `createPreloadTag` method

docs(common): remove private annotation from create function

Remove `@private` annotation from method for creating preload link

test(common): ensure max preload limit is abided by

Test to ensure that the limit of images is abided by when in the browser and server

test(common): update preload tests to match on original ngSrc

Wrap preload tests in describe, change references to `preconnect` to `preload` and use original `ngSrc` for the `preload` href instead of the rewritten `src`

docs(common): explain why imagesizes and imagesrcset are set on link tag

Add comment and link to external resource for why imagesizes and imagesrecset are set on the preload link tag from the sizes attribute and formatted srcset property

docs(common): update goldens for NgOptimizedImage

Update golden file to no longer have ng optimized image config token that was removed

docs(common): more clearly explain max preloaded image limit

Update error thrown in `ngDevMode` to more clearly explain what the issue is

fix(common): use src instead of url for preload tag function

Src is more accurate when referencing an `<img>` tag

docs(common): use nodoc on sizes input

Use `@nodoc` to keep it in DTS files but remove it from documentation

fix(common): forward sizes and srcset to preload link tag

Set imagesrcset and imagesizes on preload `<link>` tag

fix(common): remove config for optimized image directive

Remove the config injection token for the NgOptimizedImage directive and use the default constant directly

feat(common): do not duplicate ng optimized preload tags

Do not duplicate the preload `<link>` tags for images that show up more than once and throw a `RuntimeError` if there are too many preloaded images.

test(common): switch to ngSrc from rawSrc

update test for preload link to use ngSrc instead of removed rawSrc

fix(common): do not add fetchpriority to preload link tag

`fetchpriority` is not needed on preload image `<link>` tags, an image inside an `<img>` tag with `fetchpriority` set to `high` is sufficient.

fix(common): add missing semicolon

formats ng_optimized_image.ts file to add missing semicolon

fix(common): use setAttribute instead of assign to as

Chrome throws a warning about `as` not being a valid value for a preload attribute is we set it by assigning to the value to the `as` property on the preload element. Instead, if we use `setAttribute` the `as` property is correctly set on the element.

fix(common): change to relative paths for server check imports

Fix imports to `DOCUMENT` DI token and `isPlatformServer` function to remove circular dependencies

feat(common): add <link> preload tag on server for priority img

While in Angular Universal, for images that are priority add a preload <link> tag to the <head> to ensure the image is preloaded before it is rendered. This resolves a warning when running Lighthouse.

docs(common): update doc for createPreloadTag

fix(common): use set host attribute for sizes

Ensure that sizes is set on the host attribute

fix(common): move preload link creation to service

fix(common): use rewritten src for preload link href

test(common): switch quotes back to double quotes

fix(common): pass Renderer2 in from directive to create <link>

fix(common): use setAttribute instead of setProperty

Add tests to ensure that setAttribute is setting rel, as, imagesizes and imagesrcset correctly

fix(common): merge sizes input from rebase
  • Loading branch information
yharaskrik committed Oct 10, 2022
1 parent 4fde292 commit d0edffd
Show file tree
Hide file tree
Showing 6 changed files with 309 additions and 24 deletions.
2 changes: 2 additions & 0 deletions goldens/public-api/common/errors.md
Expand Up @@ -25,6 +25,8 @@ export const enum RuntimeErrorCode {
// (undocumented)
REQUIRED_INPUT_MISSING = 2954,
// (undocumented)
TOO_MANY_PRELOADED_IMAGES = 2961,
// (undocumented)
UNEXPECTED_DEV_MODE_CHECK_IN_PROD_MODE = 2958,
// (undocumented)
UNEXPECTED_INPUT_CHANGE = 2953,
Expand Down
Expand Up @@ -6,14 +6,16 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, ElementRef, inject, InjectionToken, Injector, Input, NgZone, OnChanges, OnDestroy, OnInit, Renderer2, SimpleChanges, ɵformatRuntimeError as formatRuntimeError, ɵRuntimeError as RuntimeError} from '@angular/core';
import {Directive, ElementRef, inject, InjectionToken, Injector, Input, NgZone, OnChanges, OnDestroy, OnInit, PLATFORM_ID, Renderer2, SimpleChanges, ɵformatRuntimeError as formatRuntimeError, ɵRuntimeError as RuntimeError} from '@angular/core';

import {RuntimeErrorCode} from '../../errors';
import {isPlatformServer} from '../../platform_id';

import {imgDirectiveDetails} from './error_helper';
import {IMAGE_LOADER} from './image_loaders/image_loader';
import {LCPImageObserver} from './lcp_image_observer';
import {PreconnectLinkChecker} from './preconnect_link_checker';
import {PreloadLinkCreator} from './preload-link-creator';

/**
* When a Base64-encoded image is passed as an input to the `NgOptimizedImage` directive,
Expand Down Expand Up @@ -207,6 +209,8 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
private renderer = inject(Renderer2);
private imgElement: HTMLImageElement = inject(ElementRef).nativeElement;
private injector = inject(Injector);
private readonly isServer = isPlatformServer(inject(PLATFORM_ID));
private readonly preloadLinkChecker = inject(PreloadLinkCreator);

// a LCP image observer - should be injected only in the dev mode
private lcpObserver = ngDevMode ? this.injector.get(LCPImageObserver) : null;
Expand Down Expand Up @@ -386,12 +390,23 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
this.setHostAttribute('fetchpriority', this.getFetchPriority());
// The `src` and `srcset` attributes should be set last since other attributes
// could affect the image's loading behavior.
this.setHostAttribute('src', this.getRewrittenSrc());
const rewrittenSrc = this.getRewrittenSrc();
this.setHostAttribute('src', rewrittenSrc);

let rewrittenSrcset: string|undefined = undefined;

if (this.sizes) {
this.setHostAttribute('sizes', this.sizes);
}

if (this.ngSrcset) {
this.setHostAttribute('srcset', this.getRewrittenSrcset());
rewrittenSrcset = this.getRewrittenSrcset();
this.setHostAttribute('srcset', rewrittenSrcset);
}

if (this.isServer && this.priority) {
this.preloadLinkChecker.createPreloadLinkTag(
this.renderer, rewrittenSrc, rewrittenSrcset, this.sizes);
} else if (!this._disableOptimizedSrcset && !this.srcset) {
this.setHostAttribute('srcset', this.getAutomaticSrcset());
}
Expand Down
@@ -0,0 +1,79 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {inject, Injectable, Renderer2, ɵRuntimeError as RuntimeError} from '@angular/core';

import {DOCUMENT} from '../../dom_tokens';
import {RuntimeErrorCode} from '../../errors';

import {DEFAULT_PRELOADED_IMAGES_LIMIT, PRELOADED_IMAGES} from './tokens';

/**
* @description Contains the logic needed to track and add preload link tags to the `<head>` tag. It
* will also track what images have already had preload link tags added so as to not duplicate link
* tags.
*
* In dev mode this service will validate that the number of preloaded images does not exceed the
* configured default preloaded images limit: {@link DEFAULT_PRELOADED_IMAGES_LIMIT}.
*/
@Injectable({providedIn: 'root'})
export class PreloadLinkCreator {
private readonly preloadedImages = inject(PRELOADED_IMAGES);
private readonly document = inject(DOCUMENT);

/**
* @description Add a preload `<link>` to the `<head>` of the `index.html` that is served from the
* server while using Angular Universal and SSR to kick off image loads for high priority images.
*
* The `sizes` (passed in from the user) and `srcset` (parsed and formatted from `ngSrcset`)
* properties used to set the corresponding attributes, `imagesizes` and `imagesrcset`
* respectively, on the preload `<link>` tag so that the correctly sized image is preloaded from
* the CDN.
*
* {@link https://web.dev/preload-responsive-images/#imagesrcset-and-imagesizes}
*
* @param renderer The `Renderer2` passed in from the directive
* @param src The original src of the image that is set on the `ngSrc` input.
* @param srcset The parsed and formatted srcset created from the `ngSrcset` input
* @param sizes The value of the `sizes` attribute passed in to the `<img>` tag
*/
createPreloadLinkTag(renderer: Renderer2, src: string, srcset?: string, sizes?: string): void {
if (ngDevMode) {
if (this.preloadedImages.size >= DEFAULT_PRELOADED_IMAGES_LIMIT) {
throw new RuntimeError(
RuntimeErrorCode.TOO_MANY_PRELOADED_IMAGES,
ngDevMode &&
`The \`NgOptimizedImage\` directive has detected that more than ` +
`${DEFAULT_PRELOADED_IMAGES_LIMIT} images were marked as priority. ` +
`This might negatively affect an overall performance of the page. ` +
`To fix this, remove the "priority" attribute from images with less priority.`);
}
}

if (this.preloadedImages.has(src)) {
return;
}

this.preloadedImages.add(src);

const preload = renderer.createElement('link');
renderer.setAttribute(preload, 'as', 'image');
renderer.setAttribute(preload, 'href', src);
renderer.setAttribute(preload, 'rel', 'preload');

if (sizes) {
renderer.setAttribute(preload, 'imageSizes', sizes);
}

if (srcset) {
renderer.setAttribute(preload, 'imageSrcset', srcset);
}

renderer.appendChild(this.document.head, preload);
}
}
27 changes: 27 additions & 0 deletions packages/common/src/directives/ng_optimized_image/tokens.ts
@@ -0,0 +1,27 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {InjectionToken} from '@angular/core';

/**
* In SSR scenarios, a preload `<link>` element is generated for priority images.
* Having a large number of preload tags may negatively affect the performance,
* so we warn developers (by throwing an error) if the number of preloaded images
* is above a certain threshold. This const specifies this threshold.
*/
export const DEFAULT_PRELOADED_IMAGES_LIMIT = 5;

/**
* Helps to keep track of priority images that already have a corresponding
* preload tag (to avoid generating multiple preload tags with the same URL).
*
* This Set tracks the original src passed into the `ngSrc` input not the src after it has been
* run through the specified `IMAGE_LOADER`.
*/
export const PRELOADED_IMAGES = new InjectionToken<Set<string>>(
'NG_OPTIMIZED_PRELOADED_IMAGES', {providedIn: 'root', factory: () => new Set<string>()});
1 change: 1 addition & 0 deletions packages/common/src/errors.ts
Expand Up @@ -31,4 +31,5 @@ export const enum RuntimeErrorCode {
UNEXPECTED_DEV_MODE_CHECK_IN_PROD_MODE = 2958,
INVALID_LOADER_ARGUMENTS = 2959,
OVERSIZED_IMAGE = 2960,
TOO_MANY_PRELOADED_IMAGES = 2961,
}

0 comments on commit d0edffd

Please sign in to comment.