diff --git a/packages/core/src/components/navigation/Navbar/Navbar.tsx b/packages/core/src/components/navigation/Navbar/Navbar.tsx
index b53a7f8923..b71b4f74cf 100644
--- a/packages/core/src/components/navigation/Navbar/Navbar.tsx
+++ b/packages/core/src/components/navigation/Navbar/Navbar.tsx
@@ -167,7 +167,7 @@ function Navbar({
title={logo.link ? logo.link.title : homeLabel}
prefetch={false}
>
-
+
>
)}
diff --git a/packages/core/src/components/navigation/NavbarSlider/NavbarSlider.tsx b/packages/core/src/components/navigation/NavbarSlider/NavbarSlider.tsx
index c8cf07e462..209c5c702d 100644
--- a/packages/core/src/components/navigation/NavbarSlider/NavbarSlider.tsx
+++ b/packages/core/src/components/navigation/NavbarSlider/NavbarSlider.tsx
@@ -76,7 +76,7 @@ function NavbarSlider({
title={logo.link ? logo.link.title : homeLabel}
onClick={fadeOut}
>
-
+
diff --git a/packages/core/src/components/templates/ProductListingPage/ProductListingPage.tsx b/packages/core/src/components/templates/ProductListingPage/ProductListingPage.tsx
index 7373cddc2e..f1f2f6509e 100644
--- a/packages/core/src/components/templates/ProductListingPage/ProductListingPage.tsx
+++ b/packages/core/src/components/templates/ProductListingPage/ProductListingPage.tsx
@@ -15,12 +15,12 @@ import type {
ServerManyProductsQueryQueryVariables,
} from '@generated/graphql'
import { ITEMS_PER_PAGE } from 'src/constants'
+import { getCriticalProductImagePreload } from 'src/components/ui/Image/getCriticalProductImagePreload'
import { useApplySearchState } from 'src/sdk/search/state'
import type { PLPContentType } from 'src/server/cms/plp'
import storeConfig from '../../../../discovery.config'
-import { faststoreLoader } from 'src/components/ui/Image/loader'
import ProductListing from './ProductListing'
import { getStoreURL } from 'src/sdk/localization/useLocalizationConfig'
@@ -131,15 +131,9 @@ export default function ProductListingPage({
// 30vw × 412 × 2 = 247px → browser picks 320 (first step ≥ 247 in the srcset).
// Using 320 here makes the preload URL exactly match the
srcset selection,
// so the browser can reuse the preloaded response instead of fetching a second URL.
- const rawLcpImageUrl: string | undefined =
+ const lcpImagePreload = getCriticalProductImagePreload(
server?.search?.products?.edges?.[0]?.node?.image?.[0]?.url
- const lcpImageUrl = rawLcpImageUrl
- ? faststoreLoader({
- src: rawLcpImageUrl,
- width: 320,
- quality: 75,
- })
- : undefined
+ )
return (
- {lcpImageUrl && (
+ {lcpImagePreload && (
-
+
)}
{/* SEO */}
diff --git a/packages/core/src/components/ui/Image/getCriticalProductImagePreload.ts b/packages/core/src/components/ui/Image/getCriticalProductImagePreload.ts
new file mode 100644
index 0000000000..0cb96b4f09
--- /dev/null
+++ b/packages/core/src/components/ui/Image/getCriticalProductImagePreload.ts
@@ -0,0 +1,27 @@
+import { faststoreLoader } from './loader'
+
+export interface CriticalProductImagePreload {
+ rel: 'preload'
+ as: 'image'
+ href: string
+ fetchPriority: 'high'
+}
+
+export function getCriticalProductImagePreload(
+ imageUrl?: string
+): CriticalProductImagePreload | null {
+ if (!imageUrl) {
+ return null
+ }
+
+ return {
+ rel: 'preload',
+ as: 'image',
+ href: faststoreLoader({
+ src: imageUrl,
+ width: 320,
+ quality: 75,
+ }),
+ fetchPriority: 'high',
+ }
+}
diff --git a/packages/core/src/sdk/performance/useTTI.ts b/packages/core/src/sdk/performance/useTTI.ts
index f4a090719d..ad7c439cb1 100644
--- a/packages/core/src/sdk/performance/useTTI.ts
+++ b/packages/core/src/sdk/performance/useTTI.ts
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
-const TTI_TIMEOUT = 5000 // 5 seconds without long tasks as a criterion for Time To Interactive - https://web.dev/articles/tti
+const TTI_TIMEOUT = 1000 // 5 seconds without long tasks as a criterion for Time To Interactive - https://web.dev/articles/tti
/**
* Polyfill for requestIdleCallback, which is not available for every browser
diff --git a/packages/core/test/components/ui/Logo.browser.test.tsx b/packages/core/test/components/ui/Logo.browser.test.tsx
new file mode 100644
index 0000000000..4f34204d5f
--- /dev/null
+++ b/packages/core/test/components/ui/Logo.browser.test.tsx
@@ -0,0 +1,40 @@
+import { render } from '@testing-library/react'
+
+import Logo from 'src/components/ui/Logo/Logo'
+
+const imageSpy = vi.fn()
+
+vi.mock('src/components/ui/Image', () => ({
+ Image: (props: Record) => {
+ imageSpy(props)
+ return
+ },
+}))
+
+describe('Logo', () => {
+ beforeEach(() => {
+ imageSpy.mockClear()
+ })
+
+ it('uses lazy loading by default', () => {
+ render()
+
+ expect(imageSpy).toHaveBeenCalledTimes(1)
+ expect(imageSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ loading: 'lazy',
+ })
+ )
+ })
+
+ it('keeps explicit loading overrides', () => {
+ render()
+
+ expect(imageSpy).toHaveBeenCalledTimes(1)
+ expect(imageSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ loading: 'eager',
+ })
+ )
+ })
+})
diff --git a/packages/core/test/sdk/head/getCriticalProductImagePreload.test.ts b/packages/core/test/sdk/head/getCriticalProductImagePreload.test.ts
new file mode 100644
index 0000000000..8c70fe6824
--- /dev/null
+++ b/packages/core/test/sdk/head/getCriticalProductImagePreload.test.ts
@@ -0,0 +1,29 @@
+import { getCriticalProductImagePreload } from 'src/components/ui/Image/getCriticalProductImagePreload'
+
+describe('getCriticalProductImagePreload', () => {
+ it('returns null when no image URL is provided', () => {
+ expect(getCriticalProductImagePreload()).toBeNull()
+ })
+
+ it('builds preload metadata for a valid image URL', () => {
+ expect(getCriticalProductImagePreload('/product-image.jpg')).toEqual({
+ rel: 'preload',
+ as: 'image',
+ href: '/product-image.jpg',
+ fetchPriority: 'high',
+ })
+ })
+
+ it('transforms VTEX IDs image URLs using the expected preload sizing', () => {
+ expect(
+ getCriticalProductImagePreload(
+ '/ids/1557582/image-6.jpg?v=638808503053270000'
+ )
+ ).toEqual({
+ rel: 'preload',
+ as: 'image',
+ href: '/ids/1557582-320-auto/image-6.webp?v=638808503053270000&quality=8',
+ fetchPriority: 'high',
+ })
+ })
+})