Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion aio/content/guide/image-directive.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Marking an image as `priority` applies the following optimizations:
* Sets `loading=eager` (read more about native lazy loading [here](https://web.dev/browser-level-image-lazy-loading))
* Automatically generates a [preload link element](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload) if [rendering on the server](/guide/universal).

Angular displays a warning during development if the LCP element is an image that does not have the `priority` attribute. A page’s LCP element can vary based on a number of factors - such as the dimensions of a user's screen, so a page may have multiple images that should be marked `priority`. See [CSS for Web Vitals](https://web.dev/css-web-vitals/#images-and-largest-contentful-paint-lcp) for more details.
Angular throws an error during development if the LCP element is an image that does not have the `priority` attribute, as this can hurt loading performance significantly. A page’s LCP element can vary based on a number of factors - such as the dimensions of a user's screen, so a page may have multiple images that should be marked `priority`. See [CSS for Web Vitals](https://web.dev/css-web-vitals/#images-and-largest-contentful-paint-lcp) for more details.

#### Step 5: Include Height and Width

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {inject, Injectable, OnDestroy, ɵformatRuntimeError as formatRuntimeError} from '@angular/core';
import {inject, Injectable, OnDestroy, ɵERROR_ON_IMAGE_PERFORMANCE as ERROR_ON_IMAGE_PERFORMANCE, ɵformatRuntimeError as formatRuntimeError} from '@angular/core';

import {DOCUMENT} from '../../dom_tokens';
import {RuntimeErrorCode} from '../../errors';
Expand Down Expand Up @@ -40,6 +40,8 @@ export class LCPImageObserver implements OnDestroy {
private window: Window|null = null;
private observer: PerformanceObserver|null = null;

private errorOnImagePerformance = inject(ERROR_ON_IMAGE_PERFORMANCE);

constructor() {
assertDevMode('LCP checker');
const win = inject(DOCUMENT).defaultView;
Expand Down Expand Up @@ -74,7 +76,11 @@ export class LCPImageObserver implements OnDestroy {
if (!img) return;
if (!img.priority && !img.alreadyWarnedPriority) {
img.alreadyWarnedPriority = true;
logMissingPriorityWarning(imgSrc);
if (this.errorOnImagePerformance) {
console.error(formatLazyLCPWarningText(imgSrc));
} else {
console.warn(formatLazyLCPWarningText(imgSrc));
}
}
if (img.modified && !img.alreadyWarnedModified) {
img.alreadyWarnedModified = true;
Expand Down Expand Up @@ -118,22 +124,22 @@ export class LCPImageObserver implements OnDestroy {
}
}

function logMissingPriorityWarning(ngSrc: string) {
function logModifiedWarning(ngSrc: string) {
const directiveDetails = imgDirectiveDetails(ngSrc);
console.warn(formatRuntimeError(
RuntimeErrorCode.LCP_IMG_MISSING_PRIORITY,
RuntimeErrorCode.LCP_IMG_NGSRC_MODIFIED,
`${directiveDetails} this image is the Largest Contentful Paint (LCP) ` +
`element but was not marked "priority". This image should be marked ` +
`"priority" in order to prioritize its loading. ` +
`To fix this, add the "priority" attribute.`));
`element and has had its "ngSrc" attribute modified. This can cause ` +
`slower loading performance. It is recommended not to modify the "ngSrc" ` +
`property on any image which could be the LCP element.`));
}

function logModifiedWarning(ngSrc: string) {
function formatLazyLCPWarningText(ngSrc: string) {
const directiveDetails = imgDirectiveDetails(ngSrc);
console.warn(formatRuntimeError(
return formatRuntimeError(
RuntimeErrorCode.LCP_IMG_NGSRC_MODIFIED,
`${directiveDetails} this image is the Largest Contentful Paint (LCP) ` +
`element and has had its "ngSrc" attribute modified. This can cause ` +
`slower loading performance. It is recommended not to modify the "ngSrc" ` +
`property on any image which could be the LCP element.`));
`property on any image which could be the LCP element.`);
}
12 changes: 12 additions & 0 deletions packages/core/src/application_tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,15 @@ export const ENABLED_SSR_FEATURES = new InjectionToken<Set<string>>(
providedIn: 'root',
factory: () => new Set(),
});

/**
* Internal token to allow image-related errors to be downgraded in component testing scenarios.
*
* Note: the token is in `core` because it's used to pass data from test configuration to
* the NgOptimizedImage directive (in the common package).
*/
export const ERROR_ON_IMAGE_PERFORMANCE =
new InjectionToken<boolean>('ERROR_ON_IMAGE_PERFORMANCE', {
providedIn: 'root',
factory: () => false,
});
2 changes: 1 addition & 1 deletion packages/core/src/core_private_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS, internalCreateApplication as ɵinternalCreateApplication, whenStable as ɵwhenStable} from './application_ref';
export {ENABLED_SSR_FEATURES as ɵENABLED_SSR_FEATURES} from './application_tokens';
export {ENABLED_SSR_FEATURES as ɵENABLED_SSR_FEATURES, ERROR_ON_IMAGE_PERFORMANCE as ɵERROR_ON_IMAGE_PERFORMANCE} from './application_tokens';
export {defaultIterableDiffers as ɵdefaultIterableDiffers, defaultKeyValueDiffers as ɵdefaultKeyValueDiffers} from './change_detection/change_detection';
export {Console as ɵConsole} from './console';
export {convertToBitFlags as ɵconvertToBitFlags, setCurrentInjector as ɵsetCurrentInjector} from './di/injector_compatibility';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,11 @@ describe('NgOptimizedImage directive', () => {
srcB = await imgs.get(2).getAttribute('src');
expect(srcB.endsWith('b.png')).toBe(true);

// Make sure that only one warning is in the console for image `a.png`,
// Make sure that only one error is in the console for image `a.png`,
// since the `b.png` should be below the fold and not treated as an LCP element.
const logs = await collectBrowserLogs(logging.Level.WARNING);
expect(logs.length).toEqual(2);
// Verify that the error code and the image src are present in the error message.
expect(logs[0].message).toMatch(/NG02955.*?a\.png/);
expect(logs[1].message).toMatch(/NG02964.*?logo-500w\.jpg/);

expect(logs[0].message).toMatch(/has detected that this image is the Largest Contentful Paint/);
});
});
27 changes: 27 additions & 0 deletions packages/core/test/test_bed_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2336,3 +2336,30 @@ describe('TestBed module `errorOnUnknownProperties`', () => {
expect(TestBedImpl.INSTANCE.shouldThrowErrorOnUnknownProperties()).toBe(false);
});
});

describe('TestBed module `errorOnImagePerformance`', () => {
beforeEach(() => {
TestBed.resetTestingModule();
});

it('should not throw based on the default behavior', () => {
expect(TestBedImpl.INSTANCE.shouldThrowErrorOnImagePerformance()).toBe(false);
});

it('should not throw if the option is omitted', () => {
TestBed.configureTestingModule({});
expect(TestBedImpl.INSTANCE.shouldThrowErrorOnImagePerformance()).toBe(false);
});

it('should be able to configure the option', () => {
TestBed.configureTestingModule({errorOnImagePerformance: true});
expect(TestBedImpl.INSTANCE.shouldThrowErrorOnImagePerformance()).toBe(true);
});

it('should reset the option back to the default when TestBed is reset', () => {
TestBed.configureTestingModule({errorOnImagePerformance: true});
expect(TestBedImpl.INSTANCE.shouldThrowErrorOnImagePerformance()).toBe(true);
TestBed.resetTestingModule();
expect(TestBedImpl.INSTANCE.shouldThrowErrorOnImagePerformance()).toBe(false);
});
});
17 changes: 16 additions & 1 deletion packages/core/testing/src/test_bed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
Directive,
EnvironmentInjector,
InjectFlags,
InjectionToken,
InjectOptions,
Injector,
NgModule,
Expand Down Expand Up @@ -218,6 +219,11 @@ export class TestBedImpl implements TestBed {
*/
private _instanceErrorOnUnknownPropertiesOption: boolean|undefined;

/**
* "Error on image performance" option that has been configured at the `TestBed` instance level.
*/
private _instanceErrorOnImagePerformanceOption: boolean|undefined;

/**
* Stores the previous "Error on unknown elements" option value,
* allowing to restore it in the reset testing module logic.
Expand Down Expand Up @@ -480,6 +486,7 @@ export class TestBedImpl implements TestBed {
this._instanceTeardownOptions = undefined;
this._instanceErrorOnUnknownElementsOption = undefined;
this._instanceErrorOnUnknownPropertiesOption = undefined;
this._instanceErrorOnImagePerformanceOption = undefined;
this._instanceDeferBlockBehavior = DeferBlockBehavior.Manual;
}
}
Expand Down Expand Up @@ -511,14 +518,16 @@ export class TestBedImpl implements TestBed {
this._instanceTeardownOptions = moduleDef.teardown;
this._instanceErrorOnUnknownElementsOption = moduleDef.errorOnUnknownElements;
this._instanceErrorOnUnknownPropertiesOption = moduleDef.errorOnUnknownProperties;
this._instanceErrorOnImagePerformanceOption =
moduleDef.errorOnImagePerformance !== undefined ? moduleDef.errorOnImagePerformance : false;
this._instanceDeferBlockBehavior = moduleDef.deferBlockBehavior ?? DeferBlockBehavior.Manual;
// Store the current value of the strict mode option,
// so we can restore it later
this._previousErrorOnUnknownElementsOption = getUnknownElementStrictMode();
setUnknownElementStrictMode(this.shouldThrowErrorOnUnknownElements());
this._previousErrorOnUnknownPropertiesOption = getUnknownPropertyStrictMode();
setUnknownPropertyStrictMode(this.shouldThrowErrorOnUnknownProperties());
this.compiler.configureTestingModule(moduleDef);
this.compiler.configureTestingModule(moduleDef, this._instanceErrorOnImagePerformanceOption);
return this;
}

Expand Down Expand Up @@ -743,6 +752,12 @@ export class TestBedImpl implements TestBed {
THROW_ON_UNKNOWN_PROPERTIES_DEFAULT;
}

shouldThrowErrorOnImagePerformance(): boolean {
// Check if a configuration has been provided to log an error when an image performance error is
// encountered
return this._instanceErrorOnImagePerformanceOption ?? false;
}

shouldTearDownTestingModule(): boolean {
return this._instanceTeardownOptions?.destroyAfterEach ??
TestBedImpl._environmentTeardownOptions?.destroyAfterEach ??
Expand Down
13 changes: 12 additions & 1 deletion packages/core/testing/src/test_bed_common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,12 @@ export interface TestModuleMetadata {
* @see [NG8002](/errors/NG8002) for the description of the error and how to fix it
*/
errorOnUnknownProperties?: boolean;

/**
* Whether errors should be thrown based on image misconfiguration, such as lazy-loading.
* the LCP element. Defaults to `true`, where and error is thrown.
* If set to `false`, a warning is logged instead.
*/
errorOnImagePerformance?: boolean;
/**
* Whether defer blocks should behave with manual triggering or play through normally.
* Defaults to `manual`.
Expand Down Expand Up @@ -91,6 +96,12 @@ export interface TestEnvironmentOptions {
* @see [NG8002](/errors/NG8002) for the description of the error and how to fix it
*/
errorOnUnknownProperties?: boolean;
/**
* Whether errors should be thrown based on image misconfiguration, such as lazy-loading.
* the LCP element. Defaults to `true`, where and error is thrown.
* If set to `false`, a warning is logged instead.
*/
errorOnImagePerformance?: boolean;
}

/**
Expand Down
10 changes: 8 additions & 2 deletions packages/core/testing/src/test_bed_compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {ResourceLoader} from '@angular/compiler';
import {ApplicationInitStatus, Compiler, COMPILER_OPTIONS, Component, Directive, Injector, InjectorType, LOCALE_ID, ModuleWithComponentFactories, ModuleWithProviders, NgModule, NgModuleFactory, NgZone, Pipe, PlatformRef, Provider, provideZoneChangeDetection, resolveForwardRef, StaticProvider, Type, ɵclearResolutionOfComponentResourcesQueue, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵDEFER_BLOCK_CONFIG as DEFER_BLOCK_CONFIG, ɵDeferBlockBehavior as DeferBlockBehavior, ɵdepsTracker as depsTracker, ɵDirectiveDef as DirectiveDef, ɵgenerateStandaloneInDeclarationsError, ɵgetAsyncClassMetadata as getAsyncClassMetadata, ɵgetInjectableDef as getInjectableDef, ɵInternalEnvironmentProviders as InternalEnvironmentProviders, ɵisComponentDefPendingResolution, ɵisEnvironmentProviders as isEnvironmentProviders, ɵNG_COMP_DEF as NG_COMP_DEF, ɵNG_DIR_DEF as NG_DIR_DEF, ɵNG_INJ_DEF as NG_INJ_DEF, ɵNG_MOD_DEF as NG_MOD_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵNgModuleFactory as R3NgModuleFactory, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵNgModuleType as NgModuleType, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, ɵresolveComponentResources, ɵrestoreComponentResolutionQueue, ɵsetLocaleId as setLocaleId, ɵtransitiveScopesFor as transitiveScopesFor, ɵUSE_RUNTIME_DEPS_TRACKER_FOR_JIT as USE_RUNTIME_DEPS_TRACKER_FOR_JIT, ɵɵInjectableDeclaration as InjectableDeclaration} from '@angular/core';
import {ApplicationInitStatus, Compiler, COMPILER_OPTIONS, Component, Directive, Injector, InjectorType, LOCALE_ID, ModuleWithComponentFactories, ModuleWithProviders, NgModule, NgModuleFactory, NgZone, Pipe, PlatformRef, Provider, provideZoneChangeDetection, resolveForwardRef, StaticProvider, Type, ɵclearResolutionOfComponentResourcesQueue, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵDEFER_BLOCK_CONFIG as DEFER_BLOCK_CONFIG, ɵDeferBlockBehavior as DeferBlockBehavior, ɵdepsTracker as depsTracker, ɵDirectiveDef as DirectiveDef, ɵERROR_ON_IMAGE_PERFORMANCE as ERROR_ON_IMAGE_PERFORMANCE, ɵgenerateStandaloneInDeclarationsError, ɵgetAsyncClassMetadata as getAsyncClassMetadata, ɵgetInjectableDef as getInjectableDef, ɵInternalEnvironmentProviders as InternalEnvironmentProviders, ɵisComponentDefPendingResolution, ɵisEnvironmentProviders as isEnvironmentProviders, ɵNG_COMP_DEF as NG_COMP_DEF, ɵNG_DIR_DEF as NG_DIR_DEF, ɵNG_INJ_DEF as NG_INJ_DEF, ɵNG_MOD_DEF as NG_MOD_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵNgModuleFactory as R3NgModuleFactory, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵNgModuleType as NgModuleType, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, ɵresolveComponentResources, ɵrestoreComponentResolutionQueue, ɵsetLocaleId as setLocaleId, ɵtransitiveScopesFor as transitiveScopesFor, ɵUSE_RUNTIME_DEPS_TRACKER_FOR_JIT as USE_RUNTIME_DEPS_TRACKER_FOR_JIT, ɵɵInjectableDeclaration as InjectableDeclaration} from '@angular/core';

import {ComponentDef, ComponentType} from '../../src/render3';

Expand Down Expand Up @@ -94,6 +94,8 @@ export class TestBedCompiler {
private _injector: Injector|null = null;
private compilerProviders: Provider[]|null = null;

private errorOnImagePerformance: boolean = false;

private providerOverrides: Provider[] = [];
private rootProviderOverrides: Provider[] = [];
// Overrides for injectables with `{providedIn: SomeModule}` need to be tracked and added to that
Expand All @@ -117,7 +119,8 @@ export class TestBedCompiler {
this._injector = null;
}

configureTestingModule(moduleDef: TestModuleMetadata): void {
configureTestingModule(moduleDef: TestModuleMetadata, errorOnImagePerformance: boolean): void {
this.errorOnImagePerformance = errorOnImagePerformance;
// Enqueue any compilation tasks for the directly declared component.
if (moduleDef.declarations !== undefined) {
// Verify that there are no standalone components
Expand Down Expand Up @@ -811,6 +814,9 @@ export class TestBedCompiler {
...this.providers,
...this.providerOverrides,
];
if (this.errorOnImagePerformance) {
providers.push({provide: ERROR_ON_IMAGE_PERFORMANCE, useValue: true});
}
const imports = [RootScopeModule, this.additionalModuleTypes, this.imports || []];

// clang-format off
Expand Down