Audit Results
https://www.gymshark.com/collections/leggings
Lighthouse Lab Data
Measured in a simulated environment. Values may differ from real user experience.Field Data — Mobile (Real Users)
Core Web Vitals PoorChrome UX Report — p75 values from real mobile user experiences over the last 28 days
Summary
All five CrUX field metrics are in the needs-improvement zone, with INP at 548ms effectively crossing into poor territory (>500ms threshold). The root causes are clear: a 1.1MB _app.js Next.js shared bundle that blocks rendering and interactivity, a bloated GTM container with 239 paused tags and 288 unused variables accumulated from years of campaigns, and an uncached SSR response with 1761ms field TTFB. The GTM container alone has 341 tags (70% paused) with duplicate Facebook, Snapchat, TikTok, and dead Universal Analytics trackers.
- 1Split the 1.1MB _app.js Next.js bundle
- 2Delete 239 paused GTM tags, 288 unused variables, and 298 orphaned triggers
- 3Edge-cache SSR HTML on Cloudflare to reduce 1761ms field TTFB
- 4Consolidate duplicate Facebook Pixel: remove 5 GTM Custom HTML tags + inline script, keep 1 native GTM tag
- 5Defer advertising pixels by 3 seconds via GTM Timer triggers
- —Lazy-load 20+ country flag SVGs that consume bandwidth during initial load
- —Preload the icon font to eliminate 5.4-second load delay
- —Consolidate duplicate Snapchat and TikTok pixel tags
LCP: 3638ms → ~2500ms (good zone), CLS: 0.09 → ~0.03 (good zone), INP: 548ms → ~250-300ms (needs-improvement zone, out of poor), TTFB: 1761ms → ~500ms (good zone)
Recommendations
Split the 1.1MB _app.js Next.js bundle
_app.js (or layout.tsx in App Router) ends up in the shared bundle loaded on every page. This single file causes a 1581ms render delay (blocking LCP paint until JS executes) and is the primary driver of 4436ms TBT in lab — which maps directly to the 548ms field INP (poor zone, >500ms threshold). On a 4x CPU-throttled mobile device, parsing and executing 1.1MB of JavaScript monopolizes the main thread for several seconds, making the page completely unresponsive to taps. npx @next/bundle-analyzer to identify what's inside _app.js — look for heavy libraries (moment.js, lodash, full CSS frameworks, analytics SDKs, feature flags)._app.js into individual page components using next/dynamic with { ssr: false } for non-critical components.moment → date-fns, lodash → lodash-es (tree-shakeable), full icon libraries → individual icon imports.next/dynamic for below-the-fold components (footer, modals, country selector, chat widgets).next build output — the shared First Load JS should be under 200KB.// _app.js or layout.tsx
import HeavyAnalytics from '@/lib/analytics'
import CountrySelector from '@/components/CountrySelector'
import ChatWidget from '@/components/ChatWidget'
import { FullLibrary } from 'lodash'
import FeatureFlags from '@/lib/feature-flags'// _app.js or layout.tsx — keep only essentials
import '@/styles/globals.css'
// Move everything else to page-level or lazy-load
import dynamic from 'next/dynamic'
const CountrySelector = dynamic(() => import('@/components/CountrySelector'), {
ssr: false,
loading: () => null
})
const ChatWidget = dynamic(() => import('@/components/ChatWidget'), {
ssr: false,
loading: () => null
})
// Replace lodash with tree-shakeable imports
// import { debounce } from 'lodash-es'Delete 239 paused GTM tags, 288 unused variables, and 298 orphaned triggers
Consolidate duplicate Facebook Pixel: remove 5 GTM Custom HTML tags + inline script, keep 1 native GTM tag
connect.facebook.net/en_US/fbevents.js (93.8KB, 1147ms load time). This wastes ~250ms of main thread time from duplicate SDK initialization and event processing.
Facebook Pixel via Custom HTML runs synchronously in the main thread. A native GTM Facebook Pixel template is sandboxed, loads asynchronously, and is ~80-120ms faster because GTM optimizes its execution.
Steps: _document.js or <Head> component — search for connect.facebook.net/en_US/fbevents.js.Edge-cache SSR HTML on Cloudflare to reduce 1761ms field TTFB
/collections/* paths:
- Cache Level: Cache Everything
- Edge TTL: 60 seconds
- Browser TTL: 0 (respect origin)Cache-Control header from your Next.js server for collection pages.stale-while-revalidate so Cloudflare serves stale content while fetching fresh in the background — users never wait for SSR.curl -sI https://www.gymshark.com/collections/leggings | grep -i cf-cache-status — should show HIT on second request.// next.config.js or API route — no cache headers // Response header: (none related to caching)
// In Next.js getServerSideProps or middleware for /collections/* pages:
export async function getServerSideProps(context) {
context.res.setHeader(
'Cache-Control',
'public, s-maxage=60, stale-while-revalidate=300'
);
// ... fetch collection data
return { props: { /* ... */ } };
}Preload LCP image and set fetchpriority on all above-fold product images
<link rel="preload"> in <Head> for the first product image on the collection page.fetchpriority="high" on the first product image only (to avoid bandwidth contention).loading="eager" on the first 4-6 product images (the ones visible in the 2-column mobile grid).loading="lazy".<Image
src={product.image}
alt={product.name}
width={384}
height={480}
/><!-- In _document.js or Head for collection pages -->
<link
rel="preload"
as="image"
href={firstProduct.image.replace('_384x', '_384x')}
fetchpriority="high"
/>
<!-- First product card image -->
<Image
src={product.image}
alt={product.name}
width={384}
height={480}
priority={index === 0}
loading={index < 6 ? 'eager' : 'lazy'}
fetchPriority={index === 0 ? 'high' : 'auto'}
/>Defer advertising pixels by 3 seconds via GTM Timer triggers
Fix USP revolving banner and MainContent layout shifts (CLS 0.09)
min-height on div.usp-revolving_usp-revolving__SjiW5 matching the rendered height (~40px on mobile) to reserve space before Slick initializes.
2. For MainContent: ensure the SSR output includes the product grid skeleton with correct dimensions, so hydration doesn't cause a reflow.
3. For product card tags: reserve space for the "NEW" badge via CSS min-height on div.product-card_tag-container__u_4lQ or use position: absolute so badges don't shift surrounding content..usp-revolving_usp-revolving__SjiW5 {
/* No reserved height */
}
.product-card_tag-container__u_4lQ {
/* Dynamically sized */
}.usp-revolving_usp-revolving__SjiW5 {
min-height: 40px;
contain: layout style;
}
.product-card_tag-container__u_4lQ {
position: absolute;
bottom: 12px;
left: 12px;
z-index: 1;
}Delete 6 dead Universal Analytics tags — UA was sunset July 2023
analytics.js library, parse it, and attempt network requests to a non-functional service.
Open GTM GTM-WQXVFFG → Tags → filter by type Universal Analytics. Delete all 6 tags:Lazy-load 20+ country flag SVGs that consume bandwidth during initial load
// Country selector component — all flags loaded eagerly import usFlag from '@/static/media/us.svg' import auFlag from '@/static/media/au.svg' import bhFlag from '@/static/media/bh.svg' // ... 17 more imports
// Country selector component — flags loaded on interaction
import dynamic from 'next/dynamic'
const CountryFlags = dynamic(
() => import('@/components/CountryFlags'),
{ ssr: false, loading: () => <div className="flag-placeholder" /> }
)
// Only render when selector is open
{isSelectorOpen && <CountryFlags />}Preload the icon font to eliminate 5.4-second load delay
@font-face), and on a throttled mobile connection it's queued behind the massive JS bundles competing for bandwidth. The browser's preload scanner cannot discover fonts referenced in CSS. Adding a <link rel="preload"> in the <head> lets the browser start fetching the font immediately alongside other critical resources.<!-- No preload for icon font -->
<link rel="preload" as="font" type="font/woff" href="/_next/static/media/gymshark-icons.70b7ab93.woff" crossorigin="anonymous" />