feat(core): add warnings for oversized images and lazy-lcp (#51846)

Add warnings for two image-related performance problems that apply beyond just apps using NgOptimizedImage.

PR Close #51846
This commit is contained in:
Alex Castle
2023-09-19 02:09:24 -07:00
committed by Andrew Scott
parent 85843e8212
commit 048f400efc
17 changed files with 382 additions and 38 deletions
+39
View File
@@ -0,0 +1,39 @@
@name Runtime Performance Warnings
@category runtime
@shortDescription Performance-harming image elements detected at runtime
@description
After an application is loaded, Angular checks included `<img>` elements for performance-harming misconfiguration. This warning can be triggered two ways:
### Oversized images
When images are loaded, the **intrinsic size** of the downloaded file is checked against the actual size of the image on the page. The actual size is calculated using the **rendered size** of the image with CSS applied, multiplied by the [pixel device ratio](https://web.dev/codelab-density-descriptors/#pixel-density). If the downloaded image is much larger (more than 1200px too large in either dimension), this warning is triggered. Downloading oversized images can slow down page loading and have a negative effect on [Core Web Vitals](https://web.dev/vitals/).
### Lazy-loaded LCP element
The largest contentful element on a page during load is considered the "LCP Element", which relates to [Largest Contentful Paint](https://web.dev/lcp/), one of the Core Web Vitals. Lazy loading an LCP element will have a strong negative effect on page performance. With this strategy, the browser has to complete layout calculations to determine whether the element is in the viewport before starting the image download. As a result, a warning is triggered when Angular detects that the LCP element has been given the `loading="lazy"` attribute.
@debugging
Use the image URL provided in the console warning to find the `<img>` element in question.
### Ways to fix oversized images
* Use a smaller source image
* Add a [`srcset`](https://web.dev/learn/design/responsive-images/#responsive-images-with-srcset) if multiple sizes are needed for different layouts.
* Switch to use Angular's built-in image directive ([`NgOptimizedImage`](https://angular.io/api/common/NgOptimizedImage)), which generates [srcsets automatically](https://angular.io/guide/image-directive#request-images-at-the-correct-size-with-automatic-srcset).
### Ways to fix lazy-loaded LCP element
* Change the `loading` attribute to a different value such as `"eager"`.
* Switch to use Angular's built-in image directive ([`NgOptimizedImage`](https://angular.io/api/common/NgOptimizedImage)), which allows for easily [prioritizing LCP images](https://angular.io/guide/image-directive#step-4-mark-images-as-priority).
### Disabling Image Performance Warnings
Both warnings can be disabled individually, site-wide, using a provider at the root of your application:
<code-example format="typescript" language="typescript">
providers: [
{
provide: IMAGE_CONFIG,
useValue: {
disableImageSizeWarning: true,
disableImageLazyLoadWarning: true
}
},
],
</code-example>
+4 -6
View File
@@ -8,6 +8,8 @@ import { ChangeDetectorRef } from '@angular/core';
import { DoCheck } from '@angular/core';
import { ElementRef } from '@angular/core';
import * as i0 from '@angular/core';
import { ɵIMAGE_CONFIG as IMAGE_CONFIG } from '@angular/core';
import { ɵImageConfig as ImageConfig } from '@angular/core';
import { InjectionToken } from '@angular/core';
import { Injector } from '@angular/core';
import { IterableDiffers } from '@angular/core';
@@ -311,16 +313,12 @@ export class I18nSelectPipe implements PipeTransform {
static ɵpipe: i0.ɵɵPipeDeclaration<I18nSelectPipe, "i18nSelect", true>;
}
// @public
export const IMAGE_CONFIG: InjectionToken<ImageConfig>;
export { IMAGE_CONFIG }
// @public
export const IMAGE_LOADER: InjectionToken<ImageLoader>;
// @public
export type ImageConfig = {
breakpoints?: number[];
};
export { ImageConfig }
// @public
export type ImageLoader = (config: ImageLoaderConfig) => string;
+2
View File
@@ -53,6 +53,8 @@ export const enum RuntimeErrorCode {
// (undocumented)
HYDRATION_STABLE_TIMEDOUT = -506,
// (undocumented)
IMAGE_PERFORMANCE_WARNING = -913,
// (undocumented)
IMPORT_PROVIDERS_FROM_STANDALONE = 800,
// (undocumented)
INJECTOR_ALREADY_DESTROYED = 205,
@@ -6,11 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
export {ɵIMAGE_CONFIG as IMAGE_CONFIG, ɵImageConfig as ImageConfig} from '@angular/core';
// These exports represent the set of symbols exposed as a public API.
export {provideCloudflareLoader} from './image_loaders/cloudflare_loader';
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 {IMAGE_CONFIG, ImageConfig, NgOptimizedImage} from './ng_optimized_image';
export {NgOptimizedImage} from './ng_optimized_image';
export {PRECONNECT_CHECK_BLOCKLIST} from './preconnect_link_checker';
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {booleanAttribute, Directive, ElementRef, inject, InjectionToken, Injector, Input, NgZone, numberAttribute, OnChanges, OnDestroy, OnInit, PLATFORM_ID, Renderer2, SimpleChanges, ɵformatRuntimeError as formatRuntimeError, ɵRuntimeError as RuntimeError, ɵSafeValue as SafeValue, ɵunwrapSafeValue as unwrapSafeValue} from '@angular/core';
import {booleanAttribute, Directive, ElementRef, inject, Injector, Input, NgZone, numberAttribute, OnChanges, OnDestroy, OnInit, PLATFORM_ID, Renderer2, SimpleChanges, ɵformatRuntimeError as formatRuntimeError, ɵIMAGE_CONFIG as IMAGE_CONFIG, ɵIMAGE_CONFIG_DEFAULTS as IMAGE_CONFIG_DEFAULTS, ɵImageConfig as ImageConfig, ɵRuntimeError as RuntimeError, ɵSafeValue as SafeValue, ɵunwrapSafeValue as unwrapSafeValue} from '@angular/core';
import {RuntimeErrorCode} from '../../errors';
import {isPlatformServer} from '../../platform_id';
@@ -86,32 +86,6 @@ const FIXED_SRCSET_HEIGHT_LIMIT = 1080;
/** Info about built-in loaders we can test for. */
export const BUILT_IN_LOADERS = [imgixLoaderInfo, imageKitLoaderInfo, cloudinaryLoaderInfo];
/**
* A configuration object for the NgOptimizedImage directive. Contains:
* - breakpoints: An array of integer breakpoints used to generate
* srcsets for responsive images.
*
* Learn more about the responsive image configuration in [the NgOptimizedImage
* guide](guide/image-directive).
* @publicApi
*/
export type ImageConfig = {
breakpoints?: number[]
};
const defaultConfig: ImageConfig = {
breakpoints: [16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840],
};
/**
* Injection token that configures the image optimized image functionality.
*
* @see {@link NgOptimizedImage}
* @publicApi
*/
export const IMAGE_CONFIG = new InjectionToken<ImageConfig>(
'ImageConfig', {providedIn: 'root', factory: () => defaultConfig});
/**
* Directive that improves image loading performance by enforcing best practices.
*
@@ -565,7 +539,7 @@ function processConfig(config: ImageConfig): ImageConfig {
if (config.breakpoints) {
sortedBreakpoints.breakpoints = config.breakpoints.sort((a, b) => a - b);
}
return Object.assign({}, defaultConfig, config, sortedBreakpoints);
return Object.assign({}, IMAGE_CONFIG_DEFAULTS, config, sortedBreakpoints);
}
/***** Assert functions *****/
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CommonModule, DOCUMENT} from '@angular/common';
import {CommonModule, DOCUMENT, IMAGE_CONFIG, ImageConfig} from '@angular/common';
import {RuntimeErrorCode} from '@angular/common/src/errors';
import {PLATFORM_SERVER_ID} from '@angular/common/src/platform_id';
import {Component, PLATFORM_ID, Provider, Type} from '@angular/core';
@@ -17,7 +17,7 @@ import {withHead} from '@angular/private/testing';
import {PRELOADED_IMAGES} from '../..//src/directives/ng_optimized_image/tokens';
import {createImageLoader, IMAGE_LOADER, ImageLoader, ImageLoaderConfig} from '../../src/directives/ng_optimized_image/image_loaders/image_loader';
import {ABSOLUTE_SRCSET_DENSITY_CAP, assertValidNgSrcset, IMAGE_CONFIG, ImageConfig, NgOptimizedImage, RECOMMENDED_SRCSET_DENSITY_CAP} from '../../src/directives/ng_optimized_image/ng_optimized_image';
import {ABSOLUTE_SRCSET_DENSITY_CAP, assertValidNgSrcset, NgOptimizedImage, RECOMMENDED_SRCSET_DENSITY_CAP} from '../../src/directives/ng_optimized_image/ng_optimized_image';
import {PRECONNECT_CHECK_BLOCKLIST} from '../../src/directives/ng_optimized_image/preconnect_link_checker';
describe('Image directive', () => {
+5
View File
@@ -26,6 +26,7 @@ import {ErrorHandler} from './error_handler';
import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from './errors';
import {DEFAULT_LOCALE_ID} from './i18n/localization';
import {LOCALE_ID} from './i18n/tokens';
import {ImagePerformanceWarning} from './image_performance_warning';
import {InitialRenderPendingTasks} from './initial_render_pending_tasks';
import {Type} from './interface/type';
import {COMPILER_OPTIONS, CompilerOptions} from './linker/compiler';
@@ -285,6 +286,10 @@ export function internalCreateApplication(config: {
if (rootComponent !== undefined) {
appRef.bootstrap(rootComponent);
}
if (typeof ngDevMode === 'undefined' || ngDevMode) {
const imagePerformanceService = envInjector.get(ImagePerformanceWarning);
imagePerformanceService.start();
}
return appRef;
});
});
+38
View File
@@ -126,3 +126,41 @@ export const ENABLED_SSR_FEATURES = new InjectionToken<Set<string>>(
providedIn: 'root',
factory: () => new Set(),
});
/**
* A configuration object for the image-related options. Contains:
* - breakpoints: An array of integer breakpoints used to generate
* srcsets for responsive images.
* - disableImageSizeWarning: A boolean value. Setting this to true will
* disable console warnings about oversized images.
* - disableImageLazyLoadWarning: A boolean value. Setting this to true will
* disable console warnings about LCP images configured with `loading="lazy"`.
* Learn more about the responsive image configuration in [the NgOptimizedImage
* guide](guide/image-directive).
* Learn more about image warning options in [the related error page](errors/NG0913).
* @publicApi
*/
export type ImageConfig = {
breakpoints?: number[],
disableImageSizeWarning?: boolean,
disableImageLazyLoadWarning?: boolean,
};
export const IMAGE_CONFIG_DEFAULTS: ImageConfig = {
breakpoints: [16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840],
disableImageSizeWarning: false,
disableImageLazyLoadWarning: false,
};
/**
* Injection token that configures the image optimized image functionality.
* See {@link ImageConfig} for additional information about parameters that
* can be used.
*
* @see {@link NgOptimizedImage}
* @see {@link ImageConfig}
* @publicApi
*/
export const IMAGE_CONFIG = new InjectionToken<ImageConfig>(
'ImageConfig', {providedIn: 'root', factory: () => IMAGE_CONFIG_DEFAULTS});
+1 -1
View File
@@ -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, IMAGE_CONFIG as ɵIMAGE_CONFIG, IMAGE_CONFIG_DEFAULTS as ɵIMAGE_CONFIG_DEFAULTS, ImageConfig as ɵImageConfig} 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';
+1
View File
@@ -111,6 +111,7 @@ export const enum RuntimeErrorCode {
UNSAFE_IFRAME_ATTRS = -910,
VIEW_ALREADY_DESTROYED = 911,
COMPONENT_ID_COLLISION = -912,
IMAGE_PERFORMANCE_WARNING = -913,
// Runtime dependency tracker errors
RUNTIME_DEPS_INVALID_IMPORTED_TYPE = 1000,
@@ -0,0 +1,169 @@
/**
* @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 {IMAGE_CONFIG, ImageConfig} from './application_tokens';
import {Injectable} from './di';
import {inject} from './di/injector_compatibility';
import {formatRuntimeError, RuntimeErrorCode} from './errors';
import {OnDestroy} from './interface/lifecycle_hooks';
import {getDocument} from './render3/interfaces/document';
// A delay in milliseconds before the scan is run after onLoad, to avoid any
// potential race conditions with other LCP-related functions. This delay
// happens outside of the main JavaScript execution and will only effect the timing
// on when the warning becomes visible in the console.
const SCAN_DELAY = 200;
const OVERSIZED_IMAGE_TOLERANCE = 1200;
@Injectable({providedIn: 'root'})
export class ImagePerformanceWarning implements OnDestroy {
// Map of full image URLs -> original `ngSrc` values.
private window: Window|null = null;
private observer: PerformanceObserver|null = null;
private options: ImageConfig = inject(IMAGE_CONFIG);
private lcpImageUrl?: string;
constructor() {}
public start() {
if (typeof PerformanceObserver === 'undefined' ||
(this.options?.disableImageSizeWarning && this.options?.disableImageLazyLoadWarning)) {
return;
}
this.observer = this.initPerformanceObserver();
const win = getDocument().defaultView;
if (typeof win !== 'undefined') {
this.window = win;
// Wait to avoid race conditions where LCP image triggers
// load event before it's recorded by the performance observer
const waitToScan = () => {
setTimeout(this.scanImages.bind(this), SCAN_DELAY);
};
this.window?.addEventListener('load', waitToScan);
}
}
ngOnDestroy() {
this.observer?.disconnect();
}
private initPerformanceObserver(): PerformanceObserver|null {
if (typeof PerformanceObserver === 'undefined') {
return null;
}
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
if (entries.length === 0) return;
// We use the latest entry produced by the `PerformanceObserver` as the best
// signal on which element is actually an LCP one. As an example, the first image to load on
// a page, by virtue of being the only thing on the page so far, is often a LCP candidate
// and gets reported by PerformanceObserver, but isn't necessarily the LCP element.
const lcpElement = entries[entries.length - 1];
// Cast to `any` due to missing `element` on the `LargestContentfulPaint` type of entry.
// See https://developer.mozilla.org/en-US/docs/Web/API/LargestContentfulPaint
const imgSrc = (lcpElement as any).element?.src ?? '';
// Exclude `data:` and `blob:` URLs, since they are fetched resources.
if (imgSrc.startsWith('data:') || imgSrc.startsWith('blob:')) return;
this.lcpImageUrl = imgSrc;
});
observer.observe({type: 'largest-contentful-paint', buffered: true});
return observer;
}
private scanImages(): void {
const images = getDocument().querySelectorAll('img');
let lcpElementFound, lcpElementLoadedCorrectly = false;
images.forEach(image => {
if (!this.options?.disableImageSizeWarning) {
for (const image of images) {
// Image elements using the NgOptimizedImage directive are excluded,
// as that directive has its own version of this check.
if (!image.getAttribute('ng-img') && this.isOversized(image)) {
logOversizedImageWarning(image.src);
}
}
}
if (!this.options?.disableImageLazyLoadWarning && this.lcpImageUrl) {
if (image.src === this.lcpImageUrl) {
lcpElementFound = true;
if (image.loading !== 'lazy' || image.getAttribute('ng-img')) {
// This variable is set to true and never goes back to false to account
// for the case where multiple images have the same src url, and some
// have lazy loading while others don't.
// Also ignore NgOptimizedImage because there's a different warning for that.
lcpElementLoadedCorrectly = true;
}
}
}
});
if (lcpElementFound && !lcpElementLoadedCorrectly && this.lcpImageUrl &&
!this.options?.disableImageLazyLoadWarning) {
logLazyLCPWarning(this.lcpImageUrl);
}
}
private isOversized(image: HTMLImageElement): boolean {
if (!this.window) {
return false;
}
const computedStyle = this.window.getComputedStyle(image);
let renderedWidth = parseFloat(computedStyle.getPropertyValue('width'));
let renderedHeight = parseFloat(computedStyle.getPropertyValue('height'));
const boxSizing = computedStyle.getPropertyValue('box-sizing');
const objectFit = computedStyle.getPropertyValue('object-fit');
if (objectFit === `cover`) {
// Object fit cover may indicate a use case such as a sprite sheet where
// this warning does not apply.
return false;
}
if (boxSizing === 'border-box') {
const paddingTop = computedStyle.getPropertyValue('padding-top');
const paddingRight = computedStyle.getPropertyValue('padding-right');
const paddingBottom = computedStyle.getPropertyValue('padding-bottom');
const paddingLeft = computedStyle.getPropertyValue('padding-left');
renderedWidth -= parseFloat(paddingRight) + parseFloat(paddingLeft);
renderedHeight -= parseFloat(paddingTop) + parseFloat(paddingBottom);
}
const intrinsicWidth = image.naturalWidth;
const intrinsicHeight = image.naturalHeight;
const recommendedWidth = this.window.devicePixelRatio * renderedWidth;
const recommendedHeight = this.window.devicePixelRatio * renderedHeight;
const oversizedWidth = (intrinsicWidth - recommendedWidth) >= OVERSIZED_IMAGE_TOLERANCE;
const oversizedHeight = (intrinsicHeight - recommendedHeight) >= OVERSIZED_IMAGE_TOLERANCE;
return oversizedWidth || oversizedHeight;
}
}
function logLazyLCPWarning(src: string) {
console.warn(formatRuntimeError(
RuntimeErrorCode.IMAGE_PERFORMANCE_WARNING,
`An image with src ${src} is the Largest Contentful Paint (LCP) element ` +
`but was given a "loading" value of "lazy", which can negatively impact` +
`application loading performance. This warning can be addressed by ` +
`changing the loading value of the LCP image to "eager", or by using the ` +
`NgOptimizedImage directive's prioritization utilities. For more ` +
`information about addressing or disabling this warning, see ` +
`https://angular.io/errors/NG2965`));
}
function logOversizedImageWarning(src: string) {
console.warn(formatRuntimeError(
RuntimeErrorCode.IMAGE_PERFORMANCE_WARNING,
`An image with src ${src} has intrinsic file dimensions much larger than its ` +
`rendered size. This can ` +
`For more information about addressing or disabling this warning, see ` +
`https://angular.io/errors/NG2965`));
}
@@ -8,6 +8,8 @@ ng_module(
"e2e/basic/basic.ts",
"e2e/fill-mode/fill-mode.ts",
"e2e/image-distortion/image-distortion.ts",
"e2e/image-perf-warnings-lazy/image-perf-warnings-lazy.ts",
"e2e/image-perf-warnings-oversized/image-perf-warnings-oversized.ts",
"e2e/lcp-check/lcp-check.ts",
"e2e/oversized-image/oversized-image.ts",
"e2e/preconnect-check/preconnect-check.ts",
@@ -0,0 +1,34 @@
/**
* @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
*/
/* tslint:disable:no-console */
import {browser, by, element} from 'protractor';
import {logging} from 'selenium-webdriver';
import {collectBrowserLogs} from '../browser-logs-util';
describe('Image performance warnings', () => {
it('should log a warning when a LCP image is loaded lazily', async () => {
await browser.get('/e2e/image-perf-warnings-lazy');
// Wait for load event
await new Promise(resolve => setTimeout(resolve, 600));
// Verify that both images were rendered.
const imgs = element.all(by.css('img'));
let srcA = await imgs.get(0).getAttribute('src');
expect(srcA.endsWith('a.png')).toBe(true);
let srcB = await imgs.get(1).getAttribute('src');
expect(srcB.endsWith('b.png')).toBe(true);
// Make sure that only one warning 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(1);
// Verify that the error code and the image src are present in the error message.
expect(logs[0].message).toMatch(/NG0913.*?a\.png/);
});
});
@@ -0,0 +1,28 @@
/**
* @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 {Component} from '@angular/core';
@Component({
selector: 'image-perf-warnings-lazy',
standalone: true,
template: `
<!-- 'a.png' should be treated as an LCP element -->
<img src="/e2e/a.png" width="2500" height="2500" loading="lazy">
<br>
<!--
'b.png' should *not* be treated as an LCP element here
as well, since it's below the fold
-->
<img src="/e2e/b.png" width="10" height="10">
`,
})
export class ImagePerfWarningsLazyComponent {
}
@@ -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
*/
/* tslint:disable:no-console */
import {browser} from 'protractor';
import {logging} from 'selenium-webdriver';
import {collectBrowserLogs} from '../browser-logs-util';
describe('Image performance warnings', () => {
it('should warn if rendered image size is much smaller than intrinsic size', async () => {
await browser.get('/e2e/image-perf-warnings-oversized');
// Wait for load event
await new Promise(resolve => setTimeout(resolve, 600));
const logs = await collectBrowserLogs(logging.Level.WARNING);
expect(logs.length).toEqual(1);
const expectedMessageRegex = /has intrinsic file dimensions much larger than/;
expect(expectedMessageRegex.test(logs[0].message)).toBeTruthy();
});
});
@@ -0,0 +1,22 @@
/**
* @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 {Component} from '@angular/core';
@Component({
selector: 'image-perf-warnings-oversized',
standalone: true,
template: `
<!-- Image is rendered too small -->
<div style="width: 200px; height: 200px">
<img src="/e2e/logo-1500w.jpg" width="100" height="100">
</div>
`,
})
export class ImagePerfWarningsOversizedComponent {
}
@@ -13,6 +13,8 @@ import {RouterModule} from '@angular/router';
import {BasicComponent} from './e2e/basic/basic';
import {FillModeFailingComponent, FillModePassingComponent} from './e2e/fill-mode/fill-mode';
import {ImageDistortionFailingComponent, ImageDistortionPassingComponent} from './e2e/image-distortion/image-distortion';
import {ImagePerfWarningsLazyComponent} from './e2e/image-perf-warnings-lazy/image-perf-warnings-lazy';
import {ImagePerfWarningsOversizedComponent} from './e2e/image-perf-warnings-oversized/image-perf-warnings-oversized';
import {LcpCheckComponent} from './e2e/lcp-check/lcp-check';
import {OversizedImageComponentFailing, OversizedImageComponentPassing} from './e2e/oversized-image/oversized-image';
import {PreconnectCheckComponent} from './e2e/preconnect-check/preconnect-check';
@@ -34,6 +36,8 @@ const ROUTES = [
// Paths below are used for e2e testing:
{path: 'e2e/basic', component: BasicComponent},
{path: 'e2e/lcp-check', component: LcpCheckComponent},
{path: 'e2e/image-perf-warnings-lazy', component: ImagePerfWarningsLazyComponent},
{path: 'e2e/image-perf-warnings-oversized', component: ImagePerfWarningsOversizedComponent},
{path: 'e2e/preconnect-check', component: PreconnectCheckComponent},
{path: 'e2e/image-distortion-passing', component: ImageDistortionPassingComponent},
{path: 'e2e/image-distortion-failing', component: ImageDistortionFailingComponent},