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

Analyze Your Site Free

Audit Results

https://www.nytimes.com/2026/03/23/opinion/denmark-mette-frederiksen-election.html

React

Lighthouse Lab Data

Measured in a simulated environment. Values may differ from real user experience.
70
Performance Score
Largest Contentful Paint
0.5 s
Cumulative Layout Shift
0.0362
Interaction to Next Paint
1702 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.2 sNeeds Improvement
80.3%
11.1%
8.6%
Interaction to Next Paint (INP)
149 msNeeds Improvement
85.6%
11.4%
Cumulative Layout Shift (CLS)
0.99Needs Improvement
56.6%
39.5%
Time to First Byte (TTFB)
440 msGood
88.1%
9.1%
GoodNeeds ImprovementPoor
!

Summary

Critical issues detected

CrUX field CLS of 0.99 is catastrophically poor (threshold: 0.25) and is the most urgent issue — real users experience massive layout shifts from ads, font swaps, and dynamic content during scrolling that lab tests completely miss. LCP at 2174ms is in the needs-improvement zone, impacted by 2 GTM containers (305KB combined), 49 paused dead tags, 595 unused variables, and heavy ad-tech scripts competing with LCP resources. INP at 149ms is currently good but at risk given 37 long tasks and 6104ms main thread work.

Must do
  • 1Reserve space for ad slots to eliminate catastrophic field CLS
  • 2Add font-display to 5 web fonts missing it to prevent text swap shifts
  • 3Delete 49 paused tags and duplicate trackers from GTM-P528B3
  • 4Consolidate 2 GTM containers into 1 to eliminate duplicate payload
Can defer
  • Preconnect to critical third-party origins used before LCP
  • Reduce main JS bundle: story.js (631KB) and main.js (914KB) dominate the payload
  • Defer ad-tech and measurement scripts that block main thread
Expected outcome

CLS: 0.99 → ~0.10–0.15, LCP: 2174ms → ~1700–1900ms, INP: 149ms → maintained at <150ms (protected from degradation)

Recommendations

1highclsBoth

Add font-display to 5 web fonts missing it to prevent text swap shifts

5 web fonts loaded from g1.nyt.com have no font-display property: franklin-normal-700, franklin-normal-500, cheltenham-normal-700, cheltenham-cond-normal-700, imperial-normal-400. Without font-display, browsers use their default behavior (Chrome: 3s invisible text block, then swap). When fonts finish loading at ~1700–2350ms, the swap from fallback to custom font causes layout shifts — especially with the large headline font (Cheltenham) which has very different metrics from system fallbacks. This is a significant contributor to field CLS = 0.99. The fonts are loaded via a CSS file at g1.nyt.com/fonts/css/web-fonts.css. Since NYT controls this domain:
1Add font-display: optional to the @font-face rules for body text fonts (imperial-normal-400, franklin-normal-500). font-display: optional gives the browser ~100ms to download the font; if it misses, it uses the system font with zero CLS — the custom font is cached for the next navigation.
2Add font-display: swap to headline fonts (cheltenham-normal-700, cheltenham-cond-normal-700, franklin-normal-700) since branding matters for headlines, but pair with size-adjust, ascent-override, descent-override on the fallback @font-face to minimize the shift.
3Add <link rel="preload" as="font" type="font/woff2" crossorigin href="...cheltenham-normal-700..."> for the headline font to reduce load time.
Before
@font-face {
  font-family: 'nyt-cheltenham';
  src: url('cheltenham-normal-700.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
}
After
@font-face {
  font-family: 'nyt-cheltenham';
  src: url('cheltenham-normal-700.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* Fallback font-face with metric overrides to minimize CLS */
@font-face {
  font-family: 'nyt-cheltenham-fallback';
  src: local('Georgia');
  size-adjust: 105%;
  ascent-override: 95%;
  descent-override: 22%;
  line-gap-override: 0%;
}

@font-face {
  font-family: 'nyt-imperial';
  src: url('imperial-normal-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: optional;
}
Expected impact
CLS: reduce font-swap contribution by ~0.05–0.15. Combined with ad slot fixes, targets CLS p75 < 0.1.
2highclsMobile

Make the app download banner position:fixed to eliminate CLS

The screenshot shows a "Continue reading in the app — it's better" banner at the bottom of the viewport. This banner is injected dynamically after page load (likely by the unified-lire.bundle.js script loaded at 3629ms, 287KB). If this banner is rendered with position: relative or position: sticky with a top offset, it shifts page content when it appears. Fixed-positioned elements do not cause layout shifts because they're removed from the document flow.
1Change the banner's CSS positioning to position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000.
2Add padding-bottom to <body> equal to the banner height (approximately 60–70px) to prevent the banner from covering article text — but add this padding from the start, not dynamically.
Before
.app-banner {
  position: sticky;
  bottom: 0;
  /* dynamically inserted, shifts content */
}
After
.app-banner {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 1000;
}

/* Reserve space in body from initial render */
body {
  padding-bottom: 68px;
}
Expected impact
CLS: eliminate ~0.05–0.10 from banner insertion shift. This is a consistent shift affecting every mobile visitor.
3highlcpBoth

Consolidate 2 GTM containers into 1 to eliminate duplicate payload

The page loads 2 separate GTM containers: GTM-P528B3 (188.7KB, starts at 1060ms, takes 2663ms to fully load — 660ms Conn+TLS + 356ms TTFB + 1309ms download) and GTM-N5P6T9S (115.9KB, starts at 5007ms). That's ~305KB of GTM JavaScript — two separate network requests, two separate parse+execute cycles competing with LCP resources. GTM-N5P6T9S appears to be a subscription/conversion tracking container (all its variables reference subscription.* and purchaseFlow.*). On an article page, this container does almost nothing but still loads its full payload.
1Move the 25 enabled tags from GTM-N5P6T9S into GTM-P528B3 (the primary container with more active infrastructure).
2Add a trigger condition in GTM-P528B3 so the migrated subscription tags only fire on /subscription/ pages — they're irrelevant on article pages.
3Remove the GTM-N5P6T9S snippet from the page template. Search the source code (likely in the Express server templates or React <head> component) for GTM-N5P6T9S and delete the entire <script> block.
4The 24 Custom Image Pixel tags in GTM-N5P6T9S are likely conversion pixels — verify they fire correctly from the consolidated container before removing the old one.
Google Tag Manager · GTM-N5P6T9S3 tags
Custom Tag (__cvt_12450453_170)
cvt_12450453_170
MODIFY
Custom HTML
html
MODIFY
24 × Custom Image (Pixel) (unidentifiable)
img
MODIFY
Expected impact
LCP: save ~116KB download + ~200–400ms main thread time on mobile. Field LCP 2174ms → ~1900ms.
4highlcpBoth

Delete 49 paused tags and duplicate trackers from GTM-P528B3

GTM-P528B3 contains 49 paused tags that still contribute to the container's JavaScript bundle size and browser parsing time. Key dead tags include paused ad-tech and social pixels from vendors likely no longer in use:
1Open GTM container GTM-P528B3 → Tags → filter Status: Paused.
2Delete the following identifiable paused tags: - Facebook Pixel SDK (loads connect.facebook.net/fbevents.js) — paused, adds ~60KB to container - Snapchat Pixel #1 and Snapchat Pixel #2 (both load sc-static.net/scevent.min.js) — duplicate paused pixels - TikTok Pixel #1 and TikTok Pixel #2 (both load analytics.tiktok.com/events.js) — duplicate paused pixels - Script: c.amazon-adsystem.com #1 through #6 (6 paused tags all loading c.amazon-adsystem.com/amzn.js) — massive duplication - Script: platform.iteratehq.com (loads platform.iteratehq.com/loader.js) — paused survey tool - Script: cdn.brandmetrics.com (loads cdn.brandmetrics.com/nyt.js) — paused brand measurement - Cookie Script (loads static.chartbeat.com/chartbeat_video.js) — paused Chartbeat - Custom HTML (dd.nytimes.com/tags.js) — paused, but note this script still loads inline (waterfall #39) - DataLayer Push #11 (loads apps.rokt-api.com) — paused Rokt integration - Google Ads Conversion (1008590664) — paused, Conv ID: 1008590664 - Google Ads Linker — paused
3Delete grouped paused tags: 8 × Floodlight Counter, 7 × Custom Image Pixel, 2 × Custom Tag __cvt_2703797_1426, 2 × Custom Tag __cvt_2703797_1514, 2 × Custom Tag __pntr. Find them via Tags → filter Status: Paused → look for those tag types.
4Also resolve the duplicate Google Ads issue: 4 Google Ads-related tags exist (Conversion __awct paused + Linker paused + AdSense enabled + Conversion __html paused). Keep only the enabled AdSense tag; delete the 3 paused ones.
5Publish the container after cleanup.
Google Tag Manager · GTM-P528B328 tags
Facebook Pixel SDK
paused· connect.facebook.net/fbevents.js
DELETE
Snapchat Pixel #1
paused· sc-static.net/scevent.min.js
DELETE
Snapchat Pixel #2
paused· sc-static.net/scevent.min.js
DELETE
TikTok Pixel #1
paused· analytics.tiktok.com/events.js
DELETE
TikTok Pixel #2
paused· analytics.tiktok.com/events.js
DELETE
Script: c.amazon-adsystem.com #1
paused· c.amazon-adsystem.com/amzn.js
DELETE
Script: c.amazon-adsystem.com #2
paused· c.amazon-adsystem.com/amzn.js
DELETE
Script: c.amazon-adsystem.com #3
paused· c.amazon-adsystem.com/amzn.js
DELETE
Script: c.amazon-adsystem.com #4
paused· c.amazon-adsystem.com/amzn.js
DELETE
Script: c.amazon-adsystem.com #5
paused· c.amazon-adsystem.com/amzn.js
DELETE
Script: c.amazon-adsystem.com #6
paused· c.amazon-adsystem.com/amzn.js
DELETE
Script: platform.iteratehq.com
paused· platform.iteratehq.com/loader.js
DELETE
Script: cdn.brandmetrics.com
paused· cdn.brandmetrics.com/nyt.js
DELETE
Cookie Script
paused· static.chartbeat.com/chartbeat_video.js
DELETE
Custom HTML (dd.nytimes.com/tags.js)
paused· dd.nytimes.com/tags.js
DELETE
DataLayer Push #11
paused· apps.rokt-api.com
DELETE
Google Ads Conversion (1008590664)
paused· GTM: 1008590664
DELETE
Google Ads Linker
paused
DELETE
Google Ads Conversion
paused
DELETE
Script: sb.scorecardresearch.com
paused· sb.scorecardresearch.com/beacon.js
DELETE
8 × Floodlight Counter (unidentifiable)
paused
DELETE
7 × Custom Image (Pixel) (unidentifiable)
paused
DELETE
2 × Custom Tag (__cvt_2703797_1426) (unidentifiable)
paused
DELETE
2 × Custom Tag (__cvt_2703797_1514) (unidentifiable)
paused
DELETE
Custom Tag (__cvt_2703797_1513)
paused
DELETE
Custom Tag (__pntr) — pagevisit
paused
DELETE
Custom Tag (__pntr)
paused
DELETE
Paused Tag
paused
DELETE
Expected impact
LCP: removing 49 paused tags reduces GTM-P528B3 bundle by estimated 30–50KB (parsed JS) → ~100–200ms faster main thread on mobile. Field LCP 2174ms → ~2000ms.
5highlcpBoth

Delete 595 unused GTM variables to reduce container parse time

GTM-P528B3 has 566 unused variables (out of 674) and GTM-N5P6T9S has 29 unused variables (out of 31). Every variable — even unused — is included in the container's JavaScript bundle and must be parsed by the browser. Many of these are Custom JavaScript variables containing actual code (DOM queries, window.nytAnalytics lookups, regex operations) that the JS engine must parse even if never executed. Examples of dead variables: Custom JavaScript #6 (comScore page tracking logic), Custom JavaScript #22 (campaign URL builder), location.search, nytAnalytics.campaignMap.campaignSource, and 551 more. The 53 orphaned triggers in GTM-P528B3 and 37 in GTM-N5P6T9S similarly add dead weight.
1In GTM-P528B3 → Variables → sort by usage. Delete all 566 unused variables. Start with Custom JavaScript variables (67 total, mostly unused) since they contain executable code that increases parse time.
2In GTM-N5P6T9S → Variables → delete 29 unused variables (all subscription-related: subscription.step, subscription.flow, subscription.bundle_type, purchaseFlow.*, etc.).
3In both containers → Triggers → delete orphaned triggers (53 + 37 = 90 total).
4Publish both containers.
Expected impact
LCP: reducing 595 variables + 90 orphaned triggers removes estimated 20–40KB parsed JavaScript → ~50–100ms saved on mobile main thread. Cumulative with tag cleanup for total GTM improvement of ~200–400ms.
6mediuminpBoth

Defer ad-tech and measurement scripts that block main thread

CrUX field INP = 149ms (good, threshold <200ms), but lab TBT is 4254ms with 37 long tasks totaling 6104ms — indicating the main thread is heavily loaded. The waterfall shows ad-tech scripts starting early and competing for CPU: datadog-rum.js (57KB, starts at 990ms — 358ms Conn+TLS, 1915ms total), gpt.js from doubleclick (34.5KB, 422ms Conn+TLS, 1596ms total), grumi-ip.js from geoedge (7.2KB, 573ms Conn+TLS, 1609ms total), clientag.js from media.net (193KB, 450ms Conn+TLS, 1962ms total), apstag.js from Amazon (89.6KB, 736ms Conn+TLS, 2167ms total). These all load before LCP content renders. Since INP is still in the good zone, this is a preventive optimization — but with 37 long tasks, it's at risk of degrading:
1Defer DataDog RUM (datadog-rum.js) by 3 seconds — monitoring doesn't need to start immediately; users who bounce before 3s are not generating meaningful session data.
2Load GeoEdge (grumi-ip.js + grumi.js, combined 214KB) after Window Loaded event — ad verification doesn't need to block initial render.
3Stagger ad bidding scripts: load GPT first (essential for ads), then Amazon APS and Media.net with a 200ms delay between each to prevent a CPU spike from parallel parsing.
Expected impact
INP: protect the current 149ms from degradation under heavy conditions. Reduce TBT by estimated ~1000–1500ms which buffers against INP regression on slower mobile devices.
7mediumlcpBoth

Preconnect to critical third-party origins used before LCP

The waterfall shows high connection setup overhead to third-party domains that load critical resources. The font CSS at g1.nyt.com takes 375ms Conn + 218ms TLS = 593ms before the first byte (waterfall #2). The LCP image at static01.nyt.com takes 20ms Conn + 20ms TLS (fast, already optimized). However, myaccount.nytimes.com (#26) takes 557ms Conn + 557ms TLS = 1114ms setup for a prefetch-assets call. Adding <link rel="preconnect"> for domains used in the critical path saves DNS+TCP+TLS time by establishing the connection in parallel with HTML parsing.
1Add preconnect for the font domain (highest impact — blocks text rendering): <link rel="preconnect" href="https://g1.nyt.com" crossorigin>
2Add preconnect for the auth domain (blocks lire bundle): <link rel="preconnect" href="https://myaccount.nytimes.com">
3Do NOT add preconnect for ad-tech domains (doubleclick, amazon-adsystem, etc.) — they should be deferred, not pre-connected.
Before
<head>
  <!-- no preconnect hints -->
</head>
After
<head>
  <link rel="preconnect" href="https://g1.nyt.com" crossorigin>
  <link rel="preconnect" href="https://myaccount.nytimes.com">
  <!-- existing head content -->
</head>
Expected impact
LCP: save ~200–400ms on font CSS delivery (currently 593ms connection setup). Field LCP 2174ms → ~1900ms.
8mediumlcpBoth

Optimize LCP image loading: add preload and fetchpriority="high"

The LCP element is a hero image (23krasnik-fmbk-mobileMasterAt3x-v2.jpg, 21.2KB WebP, waterfall #7). It loads at 1041ms with only 20ms Conn + 20ms TLS + 220ms TTFB + 910ms download = 786ms total. While the image is reasonably optimized (WebP format, 21.2KB), there's no <link rel="preload"> for it — the browser discovers it only after parsing HTML. Lab LCP is 496ms but field LCP is 2174ms, meaning on real mobile networks the image discovery delay is more significant. The image already has fetchpriority at Medium priority (#7 in waterfall). Upgrading to High and adding preload will help on slower connections:
1Add <link rel="preload" as="image" href="https://static01.nyt.com/images/2026/03/23/multimedia/23krasnik-fmbk/23krasnik-fmbk-mobileMasterAt3x-v2.jpg?quality=75&auto=webp&disable=upscale&width=600" fetchpriority="high"> in <head>.
2On the <img> element itself, add fetchpriority="high" and ensure loading="eager" (not lazy).
3Keep fetchpriority="high" only on this one image — do NOT add it to other images on the page.
Before
<img src="...23krasnik-fmbk-mobileMasterAt3x-v2.jpg?quality=75&auto=webp&disable=upscale&width=600" alt="...">
After
<head>
  <link rel="preload" as="image" fetchpriority="high"
    href="https://static01.nyt.com/images/2026/03/23/multimedia/23krasnik-fmbk/23krasnik-fmbk-mobileMasterAt3x-v2.jpg?quality=75&auto=webp&disable=upscale&width=600">
</head>
<!-- ... -->
<img src="...23krasnik-fmbk-mobileMasterAt3x-v2.jpg?quality=75&auto=webp&disable=upscale&width=600"
  alt="..." fetchpriority="high" loading="eager">
Expected impact
LCP: save ~100–300ms by starting the image download earlier on real mobile connections. Field LCP: 2174ms → ~1900–2000ms.
9mediumlcpBoth

Reduce main JS bundle: story.js (631KB) and main.js (914KB) dominate the payload

Two first-party JavaScript bundles dominate the page: main-8495eb2e.js (914KB, waterfall #12) and story-d12d6c70.js (631KB, waterfall #9) — together 1.55MB of JavaScript. They load at 1059ms and take ~850–930ms to download. These are React application bundles that must parse and hydrate after download. On mobile with 4x CPU throttle, parsing 1.55MB of JS is the primary source of the 37 long tasks and 6104ms main thread work. Since this is a React/Express SSR application:
1Audit bundle contents with webpack-bundle-analyzer or source-map-explorer to identify what's inside main.js and story.js. Common bloat: full lodash, moment.js, unused component code from other page types.
2Move non-article components (subscription flows, cooking recipes, games, etc.) to separate route-based chunks using React.lazy() + dynamic import().
3For the article page specifically, implement React Server Components (if migrating to Next.js App Router) or selective hydration with <Suspense> boundaries to avoid hydrating the entire 914KB main.js synchronously.
4Target: reduce combined first-party JS on article pages to <500KB.
Expected impact
LCP: reducing JS parse/execute frees the main thread earlier for rendering. Field LCP: ~200–400ms improvement. Also protects INP from degradation by reducing long task count.
10lowclsBoth

Reserve space for ad slots to eliminate catastrophic field CLS

CrUX field CLS = 0.99 (poor, threshold >0.25) while lab CLS is only 0.036 — this 30x gap means layout shifts happen during real user scrolling and ad loading, invisible to Lighthouse. The waterfall shows heavy ad-tech: doubleclick.net, amazon-adsystem.com, media.net, criteo.com, geoedge.be — all inject ad content asynchronously into DOM without pre-reserved container dimensions. Each ad slot that renders after initial paint shifts all content below it. On a long-form article like this, multiple ad slots between paragraphs create cascading shifts.
1Identify all ad container <div> elements on the page (typically classes like ad-slot, ad-container, dfp-ad, or IDs containing ad-). Set explicit min-height matching the expected ad creative size.
2For standard IAB ad sizes: leaderboard (728×90) → min-height: 90px; medium rectangle (300×250) → min-height: 250px; mobile banner (320×50) → min-height: 50px.
3Use CSS aspect-ratio for responsive ad slots where the ad format is known.
4For slots where no ad fills (blank), use CSS min-height plus overflow: hidden to prevent the empty container from collapsing back and causing a reverse shift.
Before
<div class="ad-slot" id="mid-article-ad">
  <!-- GPT ad injected here dynamically -->
</div>
After
<div class="ad-slot" id="mid-article-ad" style="min-height: 250px; min-width: 300px; overflow: hidden; contain: layout;">
  <!-- GPT ad injected here dynamically -->
</div>
Expected impact
CLS: 0.99 → ~0.15–0.25. Ad slot shifts are the primary driver of the lab-vs-field CLS gap. Reserving space eliminates the largest shift contributors.

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

Analyze Your Site Free
Nytimes — CWV Audit Report | FixMyCWV