diff --git a/packages/common/src/directives/ng_optimized_image/lcp_image_observer.ts b/packages/common/src/directives/ng_optimized_image/lcp_image_observer.ts index 86f280a8a227..e1df63a47042 100644 --- a/packages/common/src/directives/ng_optimized_image/lcp_image_observer.ts +++ b/packages/common/src/directives/ng_optimized_image/lcp_image_observer.ts @@ -19,6 +19,7 @@ import {RuntimeErrorCode} from '../../errors'; import {assertDevMode} from './asserts'; import {imgDirectiveDetails} from './error_helper'; import {getUrl} from './url'; +import {PlatformLocation} from '../../location'; interface ObservedImageState { priority: boolean; @@ -43,7 +44,7 @@ export class LCPImageObserver implements OnDestroy { // Map of full image URLs -> original `ngSrc` values. private images = new Map(); - private window: Window | null = inject(DOCUMENT).defaultView; + private platformLocation = inject(PlatformLocation); private observer: PerformanceObserver | null = null; constructor() { @@ -95,7 +96,7 @@ export class LCPImageObserver implements OnDestroy { registerImage(rewrittenSrc: string, isPriority: boolean) { if (!this.observer) return; - const url = getUrl(rewrittenSrc, this.window!).href; + const url = getUrl(rewrittenSrc, this.platformLocation).href; const existingState = this.images.get(url); if (existingState) { @@ -116,7 +117,7 @@ export class LCPImageObserver implements OnDestroy { unregisterImage(rewrittenSrc: string) { if (!this.observer) return; - const url = getUrl(rewrittenSrc, this.window!).href; + const url = getUrl(rewrittenSrc, this.platformLocation).href; const existingState = this.images.get(url); if (existingState) { @@ -129,8 +130,8 @@ export class LCPImageObserver implements OnDestroy { updateImage(originalSrc: string, newSrc: string) { if (!this.observer) return; - const originalUrl = getUrl(originalSrc, this.window!).href; - const newUrl = getUrl(newSrc, this.window!).href; + const originalUrl = getUrl(originalSrc, this.platformLocation).href; + const newUrl = getUrl(newSrc, this.platformLocation).href; // URL hasn't changed if (originalUrl === newUrl) return; diff --git a/packages/common/src/directives/ng_optimized_image/preconnect_link_checker.ts b/packages/common/src/directives/ng_optimized_image/preconnect_link_checker.ts index 8ac7e92aaacd..4575ab9aa8e1 100644 --- a/packages/common/src/directives/ng_optimized_image/preconnect_link_checker.ts +++ b/packages/common/src/directives/ng_optimized_image/preconnect_link_checker.ts @@ -20,6 +20,7 @@ import {RuntimeErrorCode} from '../../errors'; import {assertDevMode} from './asserts'; import {imgDirectiveDetails} from './error_helper'; import {extractHostname, getUrl} from './url'; +import {PlatformLocation} from '../../location'; // Set of origins that are always excluded from the preconnect checks. const INTERNAL_PRECONNECT_CHECK_BLOCKLIST = new Set(['localhost', '127.0.0.1', '0.0.0.0', '[::1]']); @@ -56,6 +57,7 @@ export const PRECONNECT_CHECK_BLOCKLIST = new InjectionToken tags found on this page. @@ -68,8 +70,6 @@ export class PreconnectLinkChecker implements OnDestroy { */ private alreadySeen = new Set(); - private window: Window | null = this.document.defaultView; - private blocklist = new Set(INTERNAL_PRECONNECT_CHECK_BLOCKLIST); constructor() { @@ -100,7 +100,12 @@ export class PreconnectLinkChecker implements OnDestroy { assertPreconnect(rewrittenSrc: string, originalNgSrc: string): void { if (typeof ngServerMode !== 'undefined' && ngServerMode) return; - const imgUrl = getUrl(rewrittenSrc, this.window!); + const imgUrl = getUrl(rewrittenSrc, this.platformLocation); + + // Do not check preconnect hints for same-origin URLs as the browser already + // establishes a connection to the origin of the page. + if (imgUrl.origin === new URL(this.platformLocation.href).origin) return; + if (this.blocklist.has(imgUrl.hostname) || this.alreadySeen.has(imgUrl.origin)) return; // Register this origin as seen, so we don't check it again later. @@ -130,7 +135,7 @@ export class PreconnectLinkChecker implements OnDestroy { const preconnectUrls = new Set(); const links = this.document.querySelectorAll('link[rel=preconnect]'); for (const link of links) { - const url = getUrl(link.href, this.window!); + const url = getUrl(link.href, this.platformLocation); preconnectUrls.add(url.origin); } return preconnectUrls; diff --git a/packages/common/src/directives/ng_optimized_image/url.ts b/packages/common/src/directives/ng_optimized_image/url.ts index bad572d8e6dc..a3d03e7335e8 100644 --- a/packages/common/src/directives/ng_optimized_image/url.ts +++ b/packages/common/src/directives/ng_optimized_image/url.ts @@ -6,10 +6,12 @@ * found in the LICENSE file at https://angular.dev/license */ +import {PlatformLocation} from '../../location'; + // Converts a string that represents a URL into a URL class instance. -export function getUrl(src: string, win: Window): URL { +export function getUrl(src: string, win: PlatformLocation): URL { // Don't use a base URL is the URL is absolute. - return isAbsoluteUrl(src) ? new URL(src) : new URL(src, win.location.href); + return isAbsoluteUrl(src) ? new URL(src) : new URL(src, win.href); } // Checks whether a URL is absolute (i.e. starts with `http://` or `https://`). diff --git a/packages/common/test/directives/ng_optimized_image_spec.ts b/packages/common/test/directives/ng_optimized_image_spec.ts index 786e4d299ab9..8bd56ab9d593 100644 --- a/packages/common/test/directives/ng_optimized_image_spec.ts +++ b/packages/common/test/directives/ng_optimized_image_spec.ts @@ -11,7 +11,7 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser'; import {isBrowser, isNode, withHead} from '@angular/private/testing'; import {expect} from '@angular/private/testing/matchers'; -import {CommonModule, DOCUMENT, IMAGE_CONFIG, ImageConfig} from '../../index'; +import {CommonModule, DOCUMENT, IMAGE_CONFIG, ImageConfig, PlatformLocation} from '../../index'; import {RuntimeErrorCode} from '../../src/errors'; import {PLATFORM_SERVER_ID} from '../../src/platform_id'; @@ -32,6 +32,7 @@ import { resetImagePriorityCount, } from '../../src/directives/ng_optimized_image/ng_optimized_image'; import {PRECONNECT_CHECK_BLOCKLIST} from '../../src/directives/ng_optimized_image/preconnect_link_checker'; +import {MockPlatformLocation} from '../../testing'; describe('Image directive', () => { const PLACEHOLDER_BLUR_AMOUNT = 15; @@ -1622,6 +1623,29 @@ describe('Image directive', () => { }), ); + it( + 'should not log a warning if there is no preconnect link, but the image is loaded from the same origin', + withHead('', () => { + setupTestingModule({ + imageLoader, + extraProviders: [ + { + provide: PlatformLocation, + useValue: new MockPlatformLocation({startUrl: 'https://angular.dev/some-page'}), + }, + ], + }); + + const consoleWarnSpy = spyOn(console, 'warn'); + const template = ''; + const fixture = createTestComponent(template); + fixture.detectChanges(); + + // Expect no warnings in the console. + expect(consoleWarnSpy.calls.count()).toBe(0); + }), + ); + ['localhost', '127.0.0.1', '0.0.0.0', '[::1]'].forEach((blocklistedHostname) => { it( `should not log a warning if an origin domain is blocklisted ` +