This is a demo report. Want one for your site?

Analyze Your Site Free

Audit Results

https://www.wayfair.com/furniture/sb0/beds-c46122.html

Magento

Lighthouse Lab Data

Measured in a simulated environment. Values may differ from real user experience.
54
Performance Score
Largest Contentful Paint
4.5 s
Cumulative Layout Shift
0
Interaction to Next PaintReal
104 ms
Good (90–100)Needs Improvement (50–89)Poor (0–49)

Field Data — Mobile (Real Users)

Core Web Vitals Poor

Chrome UX Report — p75 values from real mobile user experiences over the last 28 days

Largest Contentful Paint (LCP)
2.5 sNeeds Improvement
73.7%
16.3%
9.9%
Interaction to Next Paint (INP)
1206 msNeeds Improvement
31.4%
29.1%
39.4%
Cumulative Layout Shift (CLS)
0.08Needs Improvement
79.1%
17.1%
Time to First Byte (TTFB)
1088 msNeeds Improvement
57.0%
34.4%
8.5%
GoodNeeds ImprovementPoor

Lab vs Field Discrepancies

  • Field INP is POOR (1206ms) but lab TBT suggests GOOD. Real user interactions reveal issues not caught in lab. CrUX field data is the ground truth — INP recommendations are critical.
~

Summary

Needs improvement

The page suffers from severe JavaScript-induced interactivity problems on Next.js: 89 chunks totalling ~4.5MB cause 9.8s of blocking time in lab and 1206ms field INP. The LCP image is discovered as the 99th resource because heavy JS loads first, and HTML is not cached at the edge (cf-cache-status: DYNAMIC) despite running through Cloudflare. Fixing bundle architecture, preloading the LCP image, and enabling edge caching for anonymous traffic will move all three Core Web Vitals from needs-improvement into the good zone.

Must do
  • 1Reduce INP: split Next.js JS bundles and defer non-critical hydration
  • 2Preload LCP product image and add fetchpriority=high
  • 3Enable HTML edge caching on Cloudflare for category pages
  • 4Defer third-party tracking and analytics by 3 seconds
Can defer
  • Consolidate Next.js chunks — too many small files cause network contention
  • Add font-display: swap to Sofia web fonts
  • Serve product images in AVIF/WebP with correct sizes
  • Reduce DOM size from 5554 elements
  • Investigate CLS gap between lab (0.000) and CrUX (0.08)
  • Add scheduler.yield() to long-running click handlers
Expected outcome

LCP: 2548ms → ~1800ms (good), INP: 1206ms → ~450ms (still needs-improvement but usable, target good zone after recommendation #10), CLS: 0.08 → ~0.03 (good), TTFB: 1088ms → ~250ms (good).

Estimated cost to fix critical issues (Must Do)
🕐25-50 developer hours
💰$1000-3000 at average freelance rates ($40–60/hr)

Estimates account for detected platform. Actual time may vary based on codebase complexity and developer experience.

Recommendations

1criticalinpBoth

Reduce INP: split Next.js JS bundles and defer non-critical hydration

CrUX field INP = 1206ms (needs-improvement, threshold >200ms). The page loads 89 JS chunks totalling ~4.5MB from assets.wfcdn.com/webpack/sf-ui-core-funnel/_next/static/chunks/. Lab TBT = 9829ms with 37 long tasks totalling 11679ms — this is React hydration blocking the main thread. Real users click on products and the UI freezes for >1s.
1Audit shared bundle with @next/bundle-analyzer — many chunks (global-error 118KB, 5967 105KB, bc5c711d 114KB, 2d7c1f16 110KB) load on every page. Move page-specific imports out of _app.tsx/layout.tsx.
2Convert heavy components below the fold (product grid items beyond viewport, filters panel, footer recommendations) to next/dynamic with ssr: false or loading placeholder.
3Migrate to App Router + React Server Components where possible — RSCs ship zero JS to the client. The product grid is a perfect candidate.
4Apply Selective Hydration: wrap below-the-fold sections in granular <Suspense> boundaries so React can prioritize the visible Sort/Filter/Add-to-cart buttons first.
5Replace setTimeout(fn, 0) yield patterns with await scheduler.yield() so user clicks aren't queued behind 3rd-party scripts.
Expected impact
INP: 1206ms → ~400-500ms (still needs-improvement but usable). TBT: 9829ms → ~2000ms.
~16-32 hrsestimated developer time to implement this fix
2highlcpBoth

Preload LCP product image and add fetchpriority=high

LCP element is the product image Ardin+Solid+Wood+Bed+Frame...jpg but it loads as request #99 out of 322 (31% into the waterfall). Render Delay = 3817ms out of 4471ms LCP — the browser discovers the image far too late, after parsing 88 JS chunks. CrUX LCP = 2548ms (needs-improvement), lab LCP = 4471ms (poor).
1Add <link rel="preload" as="image" fetchpriority="high" href="https://assets.wfcdn.com/im/10051340/resize-h500-w500%5Ecompr-r85/3655/365590467/Ardin+Solid+Wood+Bed+Frame...jpg"> in the document <head>. On a category page where LCP varies between visits, preload the first 2-3 product images (grid LCP is unstable — different images become LCP on different devices).
2Set fetchpriority="high" and loading="eager" on the first 4-6 product image <img> tags (first 2 rows of the grid on mobile).
3Set fetchpriority="low" on hero/promo banners that are NOT LCP (e.g., "Dreamy Deals" carousel) to free bandwidth for the product LCP image.
4Ensure no loading="lazy" on the first row of product images.
Before
<img src="...Ardin+Solid+Wood+Bed...jpg" loading="lazy" />
After
<!-- in <head> -->
<link rel="preload" as="image" fetchpriority="high"
  href="https://assets.wfcdn.com/im/.../Ardin+Solid+Wood+Bed....jpg"
  imagesrcset="..." imagesizes="(max-width: 768px) 50vw, 25vw">

<!-- in markup -->
<img src="...Ardin+Solid+Wood+Bed....jpg"
  loading="eager" fetchpriority="high"
  width="500" height="500" alt="..." />
Expected impact
~764ms improvement
~1-2 hrsestimated developer time to implement this fix
3highlcpBoth

Enable HTML edge caching on Cloudflare for category pages

Response header is Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate and cf-cache-status: DYNAMIC — every HTML request hits the origin. TTFB = 654ms lab / 1088ms CrUX p75, contributing ~25% of LCP. Category page content (/beds-c46122.html) changes infrequently (prices, stock) and is identical for anonymous users — it should be cached at the edge.
1Add a Cloudflare Cache Rule: match host = www.wayfair.com AND URI Path matches /furniture/sb0/.*\.html AND cookie does not contain session/cart-id → action: Cache Eligibility: Eligible for cache, Edge TTL: 5 minutes, Browser TTL: 0.
2Change origin response for anonymous users to Cache-Control: public, s-maxage=300, stale-while-revalidate=86400. stale-while-revalidate lets the edge serve a slightly stale page instantly while revalidating in background.
3Personalized data (cart count, login state) must be loaded client-side via fetch after FCP — do NOT embed it in HTML.
4Enable Cloudflare 103 Early Hints (Speed → Optimization → Early Hints) to ship Link: preload headers before HTML generation completes.
Before
Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
cf-cache-status: DYNAMIC
After
Cache-Control: public, s-maxage=300, stale-while-revalidate=86400
cf-cache-status: HIT
Vary: Accept-Encoding
Expected impact
TTFB: 1088ms → ~150ms field, LCP gain ~600-900ms.
~4-8 hrsestimated developer time to implement this fix
4highinpBoth

Defer third-party tracking and analytics by 3 seconds

273 of 322 requests (85%) are third-party totalling 6.8MB. 80 Ping requests and 51 Fetch requests are analytics beacons firing during initial load, competing with critical JS for main thread time and bandwidth. This directly inflates INP because every interaction races against tracking handlers.
1Wrap non-essential tracking (Hotjar, Pinterest, TikTok, marketing pixels, A/B test SDKs) in a delayed loader:
2Keep core conversion tracking (GA4 page_view, server-side via Cloudflare Worker if possible) firing immediately, but defer behavioural/session tools.
3Use a Web Worker (Partytown) for any 3rd-party script that doesn't strictly need the main thread — Wayfair's recommendation/personalization SDKs are good candidates. Users who leave before 3s don't convert, so losing their tracking data is acceptable and produces cleaner analytics.
Before
<!-- Marketing pixels firing immediately on every page load -->
<script src="https://pixel.example.com/track.js"></script>
After
<script>
  function loadDeferred() {
    var s = document.createElement('script');
    s.async = true; s.src = 'https://pixel.example.com/track.js';
    document.head.appendChild(s);
  }
  if ('requestIdleCallback' in window) {
    requestIdleCallback(loadDeferred, { timeout: 3000 });
  } else {
    setTimeout(loadDeferred, 3000);
  }
</script>
Expected impact
INP: ~250ms reduction. TBT: ~1500-2500ms reduction.
~4-8 hrsestimated developer time to implement this fix
5highlcpMobile

Consolidate Next.js chunks — too many small files cause network contention

The waterfall shows 89 separate JS chunks, many under 10KB (e.g., chunks 9657 7.8KB, 108 5.5KB, 1762 7.2KB, 3977 5.8KB). Aggressive code-splitting past 15 chunks causes HTTP/2 scheduling overhead — headers + TLS record size outweigh the savings, especially on mobile with high RTT. Multiple chunks have TTFB >2000ms because they're queued behind earlier chunks. 1. In next.config.js, tune webpack.optimization.splitChunks to merge tiny chunks: set minSize: 30000 (30KB) and maxAsyncRequests: 15. 2. Use route-based splitting: 1 main chunk + 1 vendor chunk + 1 per-route chunk + a few async chunks for heavy below-fold widgets. Target 8-15 chunks total, not 89. 3. Ensure vendor chunk has a long cache hash so it's reused across navigations. 4. Remove unused dependencies — run npx depcheck and bundle analyzer to find dead imports.
Before
// next.config.js (default Next.js splitting)
// Result: 89 chunks, many <10KB
After
// next.config.js
module.exports = {
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.optimization.splitChunks = {
        chunks: 'all',
        minSize: 30000,
        maxAsyncRequests: 15,
        maxInitialRequests: 10,
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendor',
            priority: 10,
          },
        },
      };
    }
    return config;
  },
};
Expected impact
LCP: ~400-600ms improvement on mobile from reduced network contention.
~4-8 hrsestimated developer time to implement this fix
6mediumlcpBoth

Add font-display: swap to Sofia web fonts

Two custom fonts load without font-display: sofia-bold-subset.woff2 and sofia-reg-subset.woff2. Browsers default to block behaviour for ~3s — invisible text (FOIT) during the critical 0-3s window. This also delays the moment FCP/LCP can be measured if the LCP element contains text.
1Add font-display: optional for body text (zero CLS — if font loads within 100ms it's used, otherwise cached for next navigation).
2Use font-display: swap for headings only, combined with <link rel="preload" as="font" crossorigin> + size-adjust/ascent-override descriptors on the fallback to prevent CLS.
3Preload both .woff2 files in <head>.
Before
@font-face {
  font-family: 'Sofia';
  src: url('/sofia-reg-subset.woff2') format('woff2');
}
After
<link rel="preload" as="font" type="font/woff2" crossorigin
  href="https://assets.wfcdn.com/homebase/sofia/sofia-reg-subset.woff2">

@font-face {
  font-family: 'Sofia';
  src: url('/sofia-reg-subset.woff2') format('woff2');
  font-display: optional;
  size-adjust: 100%;
  ascent-override: 90%;
}
Expected impact
FCP: ~200-400ms improvement. Eliminates FOIT/CLS risk from font swap.
~1-2 hrsestimated developer time to implement this fix
7mediumlcpBoth

Serve product images in AVIF/WebP with correct sizes

70 images totalling 2061KB load on the category page. The LCP image is requested as JPEG (compr-r85 quality 85). AVIF typically saves 40-50% over JPEG at the same visual quality; WebP saves 25-35%. With 70 images, total image weight could drop by ~700KB.
1Generate AVIF and WebP variants on the CDN (assets.wfcdn.com likely supports format negotiation — add Accept header routing or f_auto URL parameter).
2Serve via <picture> with <source type="image/avif"> + <source type="image/webp"> + JPEG fallback. Add Vary: Accept response header on the CDN to prevent wrong format being cached for the wrong browser.
3Verify image dimensions match displayed size — currently resize-h500-w500 for cards that display at ~150x150 on mobile means 11x more pixels than needed. Use srcset with w=200, w=400, w=800 variants and proper sizes attribute.
Before
<img src=".../resize-h500-w500%5Ecompr-r85/.../bed.jpg" />
After
<picture>
  <source type="image/avif" srcset=".../bed.avif?w=200 200w, .../bed.avif?w=400 400w" sizes="(max-width: 768px) 50vw, 25vw">
  <source type="image/webp" srcset=".../bed.webp?w=200 200w, .../bed.webp?w=400 400w" sizes="(max-width: 768px) 50vw, 25vw">
  <img src=".../bed.jpg?w=400" width="400" height="400" loading="lazy" alt="...">
</picture>
Expected impact
Image weight: 2061KB → ~1100KB. LCP: ~200-400ms improvement on slow mobile networks.
~2-4 hrsestimated developer time to implement this fix
8mediumlcpBoth

Reduce DOM size from 5554 elements

The page has 5554 DOM elements with max depth 32 — well above the recommended <1500 / depth <32. Each element costs memory, style recalc time, and layout time. With 50+ products × ~50 elements per card + filters + recommendations, this is the main driver of the 11679ms main thread work.
1Reduce nodes per product card — current cards likely use 40+ elements for badges, ratings, prices, CTAs. Combine wrappers, use CSS for layout instead of nested divs.
2Paginate or virtualize the grid: render only the first 12 products in HTML, fetch the rest via Intersection Observer + Storefront API. With infinite scroll, the initial DOM should be ~500-1000 elements.
3Move below-the-fold sections (footer mega-menu, recently viewed, recommendations) into React Server Components or render them lazily on scroll.
Expected impact
Main thread work: ~1500-2500ms reduction. INP: ~100-200ms improvement.
~8-16 hrsestimated developer time to implement this fix
9lowclsBoth

Investigate CLS gap between lab (0.000) and CrUX (0.08)

Lab CLS is essentially zero but CrUX field CLS is 0.08 (needs-improvement, threshold >0.1). Lab only measures shifts during initial load — real users encounter shifts when scrolling, when the cookie banner appears, when ads/recommendations load, or when product filters update results. Field > lab means scroll-triggered or interaction-triggered shifts.
1Audit dynamically inserted elements: cookie consent banner, "Memorial Day Deal" promo bars, sticky filter chips. Reserve their height with CSS min-height BEFORE they appear, or position them fixed/sticky (fixed elements don't cause CLS).
2Audit lazy-loaded sections ("Recently Viewed", recommendations carousels) — each must have min-height or aspect-ratio set before content arrives.
3Audit the "$355.99 $1,199.00 70% Off — Flash Deal" overlay that appears on product cards — if it's injected after card render, it pushes content below. Pre-render it in SSR.
4Test on a real mobile device with DevTools → Performance → Web Vitals overlay while scrolling through the entire page.
Expected impact
CLS field: 0.08 → ~0.03 (good zone).
~2-4 hrsestimated developer time to implement this fix
10lowinpBoth

Add scheduler.yield() to long-running click handlers

With CrUX INP at 1206ms, individual handlers likely run >300ms each. Once the bundle is reduced (recommendation #1), the remaining bottleneck will be specific handlers — adding-to-cart, opening filters, sort dropdown — that synchronously update React state across hundreds of components.
1Identify the worst handlers using PerformanceObserver for event entries with duration >200ms in production.
2In each heavy handler, yield to the main thread between phases using scheduler.yield() (or await new Promise(r => setTimeout(r, 0)) as fallback). Update UI feedback (loading state) immediately, defer heavy work to after the yield.
3Use useTransition / startTransition in React 18 for non-urgent state updates (filter results, sort) so the input doesn't block.
4Use AbortController to cancel in-flight handlers when the user rapidly clicks/types (debounce + cancel).
Before
function onAddToCart(item) {
  updateCartState(item); // 400ms
  trackEvent('add_to_cart', item); // 100ms
  showToast('Added!'); // 50ms
}
After
async function onAddToCart(item) {
  showToast('Adding...'); // instant feedback
  if ('scheduler' in window && 'yield' in scheduler) {
    await scheduler.yield();
  }
  startTransition(() => updateCartState(item));
  await scheduler.yield();
  trackEvent('add_to_cart', item);
  showToast('Added!');
}
Expected impact
INP: additional ~100-200ms reduction on key interactions.
~4-8 hrsestimated developer time to implement this fix

This is a demo report. Want one for your site?

Analyze Your Site Free