Audit Results
https://www.nytimes.com/2026/03/23/opinion/denmark-mette-frederiksen-election.html
ReactLighthouse 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
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.
- 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
- —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
CLS: 0.99 → ~0.10–0.15, LCP: 2174ms → ~1700–1900ms, INP: 149ms → maintained at <150ms (protected from degradation)
Recommendations
Add font-display to 5 web fonts missing it to prevent text swap shifts
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: 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.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.<link rel="preload" as="font" type="font/woff2" crossorigin href="...cheltenham-normal-700..."> for the headline font to reduce load time.@font-face {
font-family: 'nyt-cheltenham';
src: url('cheltenham-normal-700.woff2') format('woff2');
font-weight: 700;
font-style: normal;
}@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;
}Make the app download banner position:fixed to eliminate CLS
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. position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000.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..app-banner {
position: sticky;
bottom: 0;
/* dynamically inserted, shifts content */
}.app-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
}
/* Reserve space in body from initial render */
body {
padding-bottom: 68px;
}Consolidate 2 GTM containers into 1 to eliminate duplicate payload
subscription.* and purchaseFlow.*). On an article page, this container does almost nothing but still loads its full payload. /subscription/ pages — they're irrelevant on article pages.<head> component) for GTM-N5P6T9S and delete the entire <script> block.Delete 49 paused tags and duplicate trackers from GTM-P528B3
Delete 595 unused GTM variables to reduce container parse time
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. subscription.step, subscription.flow, subscription.bundle_type, purchaseFlow.*, etc.).Defer ad-tech and measurement scripts that block main thread
Window Loaded event — ad verification doesn't need to block initial render.Preconnect to critical third-party origins used before LCP
<link rel="preconnect"> for domains used in the critical path saves DNS+TCP+TLS time by establishing the connection in parallel with HTML parsing. <link rel="preconnect" href="https://g1.nyt.com" crossorigin><link rel="preconnect" href="https://myaccount.nytimes.com"><head> <!-- no preconnect hints --> </head>
<head> <link rel="preconnect" href="https://g1.nyt.com" crossorigin> <link rel="preconnect" href="https://myaccount.nytimes.com"> <!-- existing head content --> </head>
Optimize LCP image loading: add preload and fetchpriority="high"
<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: <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>.<img> element itself, add fetchpriority="high" and ensure loading="eager" (not lazy).fetchpriority="high" only on this one image — do NOT add it to other images on the page.<img src="...23krasnik-fmbk-mobileMasterAt3x-v2.jpg?quality=75&auto=webp&disable=upscale&width=600" alt="...">
<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">Reduce main JS bundle: story.js (631KB) and main.js (914KB) dominate the payload
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.<Suspense> boundaries to avoid hydrating the entire 914KB main.js synchronously.Reserve space for ad slots to eliminate catastrophic field CLS
<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.min-height: 90px; medium rectangle (300×250) → min-height: 250px; mobile banner (320×50) → min-height: 50px.aspect-ratio for responsive ad slots where the ad format is known.min-height plus overflow: hidden to prevent the empty container from collapsing back and causing a reverse shift.<div class="ad-slot" id="mid-article-ad"> <!-- GPT ad injected here dynamically --> </div>
<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>