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

Analyze Your Site Free

Audit Results

https://www.nytimes.com/2026/02/24/business/paramount-netflix-warner-bros-discovery.html

Lighthouse Lab Data

Measured in a simulated environment. Values may differ from real user experience.
70
Performance Score
Largest Contentful Paint
1.2 s
Cumulative Layout Shift
0.0025
Interaction to Next Paint
2188 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.1 sNeeds Improvement
82.1%
10.1%
Interaction to Next Paint (INP)
180 msNeeds Improvement
79.3%
16.7%
Cumulative Layout Shift (CLS)
0.95Needs Improvement
59.5%
36.4%
Time to First Byte (TTFB)
412 msGood
89.0%
8.3%
GoodNeeds ImprovementPoor
!

Summary

Critical issues detected

The NYT article page has a catastrophic CLS problem — field p75 CLS is 0.95 (poor threshold is 0.25), primarily caused by the app-download banner (#bottom-wrapper) inserting into document flow during scroll (0.82 shift score) and ad slots without reserved dimensions. Lab CLS is only 0.0025 because Lighthouse doesn't measure scroll-triggered shifts — this is a massive blind spot. INP (180ms) and LCP (2,060ms) are both in the needs-improvement zone, driven by 1.5MB of first-party JS, ~500KB of third-party scripts loading before LCP, and two bloated GTM containers with 49 paused tags and 595 unused variables.

Must do
  • 1Fix catastrophic CLS from #bottom-wrapper (0.82 shift score in lab, drives field CLS to 0.95)
  • 2Reserve space for ad containers to eliminate scroll-triggered CLS (0.088 shift scores)
  • 3Reduce 1.5MB of first-party JavaScript blocking the main thread
  • 4Defer third-party scripts: DataDog RUM, media.net, and ad scripts by 3 seconds
Can defer
  • Optimize font loading: 6 font files create a sequential chain adding 400ms+ render delay
  • Delete duplicate Google Ads and TikTok Pixel trackers in GTM
  • Defer non-critical GraphQL API calls that fire before LCP
Expected outcome

LCP: 2,060ms → ~1,700ms (needs-improvement → borderline good), CLS: 0.95 → ~0.05 (poor → good), INP: 180ms → ~120ms (needs-improvement → good), TTFB: 412ms → ~150ms (needs-improvement → good)

Recommendations

1highclsMobile

Reserve space for ad containers to eliminate scroll-triggered CLS (0.088 shift scores)

Lab data shows div#after-story-ad-3 and article body paragraphs (p.css-ac37hb) each produce 0.088 CLS scores during scroll. These are ad slots injected between article paragraphs — when the ad creative loads asynchronously, surrounding content shifts. The div#google_ads_iframe_/29390238/nyt/business_3__container__ also contributes a 0.0012 shift. Since NYT uses Google Publisher Tags (GPT) for ad delivery (visible at securepubads.g.doubleclick.net in the waterfall), the fix is to set explicit min-height on all ad slot containers before GPT fills them. For mid-article ad slots, use standard IAB sizes — 300×250 for mobile inline ads is the most common NYT placement.
Before
/* Ad container has no reserved dimensions */
#after-story-ad-3,
[id*="google_ads_iframe"] {
  /* height determined by ad creative loading */
}
After
/* Reserve space for standard mobile ad sizes */
.ad-slot--inline,
#after-story-ad-3 {
  min-height: 250px;
  min-width: 300px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f7f7f7;
}

/* Collapse gracefully if no ad fills */
.ad-slot--inline:empty {
  min-height: 0;
  transition: min-height 0.3s ease;
}
Expected impact
Eliminating 0.088 × 2 = ~0.176 CLS from ad slots. Combined with the #bottom-wrapper fix, this should bring field p75 CLS from 0.95 → ~0.05 (good zone).
2highinpMobile

Reduce 1.5MB of first-party JavaScript blocking the main thread

The two largest scripts — main-b463ec093786c0fb658f.js (905KB) and story-b085c8aaa469f0174654.js (624KB) — total ~1.5MB and both load with High priority starting at 209ms. Lab TBT is 5,469ms (main thread work: 7,469ms), meaning the CPU is saturated for over 5 seconds on a throttled mobile device. Field INP is 180ms (needs-improvement, close to the 200ms good threshold). These bundles likely contain article rendering logic, paywall/metering, navigation, and interactive components. The critical path only needs above-the-fold article text + headline rendering — everything else can be deferred.
1Code-split story.js: Separate the article body renderer (critical) from interactive features like comments, share functionality, audio player, and related articles.
2Lazy-load main.js components: Navigation hamburger menu, account features, and footer should be loaded via dynamic import() on user interaction or after LCP.
3Tree-shake unused code: At ~905KB, main.js likely includes dead code paths for other page types (homepage, section fronts).
Expected impact
Reducing main-thread JS by 40-50% could lower lab TBT from 5,469ms → ~2,500ms and improve field p75 INP from 180ms → ~120ms (comfortably in the good zone).
3highinpMobile

Defer third-party scripts: DataDog RUM, media.net, and ad scripts by 3 seconds

Multiple heavy third-party scripts load in the critical path and compete with first-party code for main thread time: - datadoghq-browser-agent.com/datadog-rum.js — 55.9KB, starts at 223ms (before LCP) - warp.media.net/clientag.js — 192.8KB, starts at 381ms - securepubads.g.doubleclick.net/gpt.js — 34KB at 352ms, then pubads_impl.js — 196.5KB at 2,510ms - a1.nyt.com/analytics/comscore-streaming.js — 18.6KB at 5,433ms - a1.nyt.com/analytics/show-ads.js — at 5,431ms DataDog RUM and media.net alone add ~250KB of JS before LCP. Per expert guidance: deferring analytics/ad scripts by 3 seconds removes them from the critical rendering path while preserving 95%+ measurement accuracy — users who leave before 3 seconds won't convert anyway. For the GTM-loaded Custom HTML (a1.nyt.com/show-ads.js) tag, convert its trigger from All Pages to a Timer trigger (3000ms delay).
Expected impact
Deferring ~500KB of third-party JS from the critical path could reduce lab TBT by ~1,500ms and improve field p75 INP from 180ms → ~140ms. Also reduces LCP Render Delay (currently 1,042ms) by freeing CPU during paint.
4highinpBoth

Consolidate 2 GTM containers into 1 — each extra container adds ~80-150KB payload

The site loads two GTM containers: GTM-P528B3 (93 tags, 674 variables) and GTM-N5P6T9S (26 tags, 31 variables). Each container is a separate JS file (~80-150KB), requiring its own HTTP request, parsing, and execution. This is a common problem — the second container was likely added by a different team or agency and never consolidated. GTM-N5P6T9S has only 25 enabled tags (mostly 24 Custom Image pixels + 1 Custom HTML + 1 custom tag). These should be migrated into GTM-P528B3, and GTM-N5P6T9S should be removed entirely. Steps:
1Open GTM-P528B3 → recreate the 2 active non-pixel tags from GTM-N5P6T9S (the Custom Tag and Custom HTML).
2For the 24 Custom Image (Pixel) tags — recreate as Custom Image tags in GTM-P528B3 with the same pixel URLs and triggers.
3Remove the GTM-N5P6T9S snippet from the page template: View Page Source → search for GTM-N5P6T9S → delete that <script> block and the <noscript> iframe.
4Publish GTM-P528B3 with the migrated tags.
5Verify in Network tab that only one gtm.js request appears.
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
Removing one GTM container saves ~80-150KB of JS + 1 network request + ~100-200ms main thread time. Field p75 INP improvement: 180ms → ~160ms.
5highinpBoth

Delete 49 paused tags and 595 unused variables from GTM containers

GTM-P528B3 contains 49 paused tags that are still included in the container JS bundle — the browser must download, parse, and partially evaluate them even though they never fire. It also has 566 unused variables (out of 674 total — that's 84% waste). GTM-N5P6T9S has 29 unused variables (out of 31 — 94% waste). Paused tags still add to container JS file size and browser parsing time. Unused variables (Custom JavaScript variables are worst — they contain executable code) similarly bloat the container. Steps for GTM-P528B3:
1Open GTM container GTM-P528B3 → Tags → filter Status: Paused.
2Delete these specifically identified paused tags first (they load heavy external scripts even when paused): - Facebook Pixel SDK (loads connect.facebook.net/fbevents.js) - Cookie Script (loads static.chartbeat.com/chartbeat_video.js) - Script: platform.iteratehq.com (loads platform.iteratehq.com/loader.js) - Custom HTML (dd.nytimes.com/tags.js) (loads dd.nytimes.com/tags.js) - Snapchat Pixel #1 and Snapchat Pixel #2 (load sc-static.net/scevent.min.js) - Script: cdn.brandmetrics.com (loads cdn.brandmetrics.com/nyt.js) - Script: sb.scorecardresearch.com (loads sb.scorecardresearch.com/beacon.js) - 6 × Script: c.amazon-adsystem.com (#1 through #6) (load c.amazon-adsystem.com/amzn.js) - TikTok Pixel #1 and TikTok Pixel #2 (load analytics.tiktok.com/events.js) - DataLayer Push #11 (loads apps.rokt-api.com)
3Delete the remaining grouped paused tags: 7 × Custom Image (Pixel), 8 × Floodlight Counter, 2 × Custom Tag (__cvt_2703797_1426), 2 × Custom Tag (__cvt_2703797_1514) — find via Tags → filter Paused → look for these tag types.
4Go to Variables → identify and delete unused variables. Start with Custom JavaScript type variables (e.g., "Custom JavaScript #6", "Custom JavaScript #16", "Custom JavaScript #22") as they contain executable code. Then delete unused Data Layer Variables ("viewport.scrollTop", "viewport.height", "module.name", "module.ga.eventCategory", etc.).
5Delete 53 orphaned triggers (Triggers → check which are not used by any tag).
6Publish the container.
Google Tag Manager · GTM-P528B315 tags
Facebook Pixel SDK
paused· connect.facebook.net/fbevents.js
DELETE
Cookie Script
paused· static.chartbeat.com/chartbeat_video.js
DELETE
Script: platform.iteratehq.com
paused· platform.iteratehq.com/loader.js
DELETE
Custom HTML (dd.nytimes.com/tags.js)
paused· dd.nytimes.com/tags.js
DELETE
Snapchat Pixel #1
paused· sc-static.net/scevent.min.js
DELETE
Snapchat Pixel #2
paused· sc-static.net/scevent.min.js
DELETE
Script: cdn.brandmetrics.com
paused· cdn.brandmetrics.com/nyt.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
TikTok Pixel #1
paused· analytics.tiktok.com/events.js
DELETE
TikTok Pixel #2
paused· analytics.tiktok.com/events.js
DELETE
Expected impact
Removing 49 paused tags + 566 unused variables could reduce GTM-P528B3 container size by 30-50% (~40-80KB savings), reducing parse time by ~200-400ms on throttled mobile CPU. Field p75 INP: 180ms → ~150ms.
6highlcpBoth

Enable HTML edge caching to reduce field TTFB from 412ms

The response header Cache-Control: no-cache, must-revalidate forces every request to the gunicorn origin server, bypassing any CDN edge cache. Field TTFB is 412ms (needs-improvement). For a news article that changes infrequently after publication, this is unnecessary — the article content is stable and can be served from edge cache with short TTL and stale-while-revalidate for instant updates. The server uses DataDome bot protection, which adds latency to every request (bot verification happens before origin response). Edge-caching the HTML allows DataDome-verified requests to be served from cache on subsequent visits, eliminating the origin round-trip. Steps:
1Change the Cache-Control header for article pages on the gunicorn/reverse proxy level:
2If using nginx as a reverse proxy in front of gunicorn, add a proxy_cache configuration for article URLs.
3Verify with: curl -sI https://www.nytimes.com/2026/02/24/business/... | grep -i cache-control — should show the new header.
Before
# Current response header
Cache-Control: no-cache, must-revalidate
After
# nginx reverse proxy config for article pages
location ~ ^/\d{4}/\d{2}/\d{2}/ {
    proxy_pass http://gunicorn_upstream;
    proxy_cache article_cache;
    proxy_cache_valid 200 300s;
    proxy_cache_use_stale updating error timeout;
    add_header Cache-Control "public, max-age=60, stale-while-revalidate=300";
    add_header X-Cache-Status $upstream_cache_status;
}
Expected impact
Edge caching with 60s max-age + 300s stale-while-revalidate should reduce field p75 TTFB from 412ms → ~150ms for repeat/popular articles. This improves field LCP from 2,060ms → ~1,700ms (TTFB is ~20% of LCP).
7mediumlcpBoth

Optimize font loading: 6 font files create a sequential chain adding 400ms+ render delay

Six font files from g1.nyt.com load in a chained sequence: first the font CSS (web-fonts.css at 206ms), then the browser discovers and fetches 6 individual font files (Franklin ×4 weights + Cheltenham ×2 weights) all starting at 464ms and completing around 853ms. This ~400ms font chain directly contributes to the 1,042ms Render Delay in the LCP breakdown — text cannot render with correct fonts until these files arrive. NYT already self-hosts fonts on g1.nyt.com (good — no cross-origin penalty). But loading 6 fonts for a single article page is excessive. The article primarily uses Cheltenham (headlines) and Franklin (body). Loading all 4 Franklin weights (300, 500, 600, 700) upfront is wasteful — body text likely only needs 300 and 700. 1. Preload the 2 most critical fonts (Cheltenham italic-700 for the headline, Franklin normal-300 for body text) in the HTML <head>. 2. Defer non-critical font weights (Franklin 500, 600) to load after LCP via font-display: optional. 3. Consider using font-display: optional for body text fonts to guarantee zero CLS from font swap (field CLS is 0.95 — font swaps may contribute).
Before
<!-- No preloads for fonts in <head> -->
After
<link rel="preload" as="font" type="font/woff2" crossorigin
  href="https://g1.nyt.com/fonts/family/cheltenham/cheltenham-italic-700.f99a0459024509f157a3.woff2">
<link rel="preload" as="font" type="font/woff2" crossorigin
  href="https://g1.nyt.com/fonts/family/franklin/franklin-normal-300.a6479a5200f9a6352bdb7158.woff2">
Expected impact
Preloading 2 critical fonts eliminates the ~200ms discovery delay (CSS parse → font request). Combined with deferring unused weights, this could reduce LCP Render Delay from 1,042ms → ~800ms, improving field p75 LCP from 2,060ms → ~1,850ms.
8mediuminpBoth

Delete duplicate Google Ads and TikTok Pixel trackers in GTM

GTM-P528B3 contains duplicate tracker instances that waste main thread time: Google Ads — 4 overlapping tags: - Google Ads Conversion (1008590664) (native __awct, paused) - Google Ads Linker (__gclidw, paused) - Google AdSense (Custom HTML, enabled) - Google Ads Conversion (Custom HTML, paused) The paused native conversion tag and the paused Custom HTML conversion tag are both dead weight. The Ads Linker is also paused. Only the AdSense Custom HTML is active — but it should use the native GTM AdSense tag template instead of Custom HTML for better performance. TikTok Pixel — 2 duplicate tags (both paused): - TikTok Pixel #1 and TikTok Pixel #2 both load analytics.tiktok.com/events.js. Both are paused — delete both. Steps: Open GTM-P528B3 → Tags → search for the Conversion ID 1008590664 to find the native Google Ads Conversion tag. Delete all paused Google Ads tags. Convert the enabled AdSense Custom HTML to the native GTM template.
Google Tag Manager · GTM-P528B34 tags
Google Ads Conversion (1008590664)
paused· GTM: conv: 1008590664
DELETE
Google Ads Linker
paused
DELETE
Google AdSense
html
MODIFY
Google Ads Conversion
paused
DELETE
Expected impact
Removing 4 duplicate/paused ad tags eliminates ~150ms of wasted parsing. Field p75 INP: 180ms → ~170ms (incremental, combines with other GTM cleanup).
9mediumlcpMobile

Defer non-critical GraphQL API calls that fire before LCP

The waterfall shows 6 GraphQL fetch calls to samizdat-graphql.nytimes.com between 342ms and 5,200ms. The earliest one (at 342ms) fires before LCP and competes for network bandwidth and CPU. These fetch calls serve secondary content: email capsule, storyline guide, free daily assets, subnav, and trust assets — none of which are needed for the initial article render. Additionally, nytimes.com/svc/onsite-messaging/query (at 4,428ms, 15.2KB) and the statsig config fetch (at 4,180ms, 14.4KB) are A/B testing and messaging payloads that can be deferred.
1Move the early GraphQL call (operationName at 342ms) to fire after LCP using requestIdleCallback or after the load event.
2Batch the 4 GraphQL calls at ~4,400-5,200ms into a single request if the GraphQL server supports query batching.
3Defer the onsite-messaging and statsig config fetches to after user scroll or 3 seconds post-load.
Expected impact
Deferring the pre-LCP GraphQL call frees ~50ms of network contention during the critical LCP window. Batching later calls reduces 4 requests → 1, saving ~100ms of connection overhead. Field p75 LCP improvement: 2,060ms → ~1,950ms.
10lowclsMobile

Fix catastrophic CLS from #bottom-wrapper (0.82 shift score in lab, drives field CLS to 0.95)

The field CLS is 0.95 — catastrophically poor (threshold for "poor" is >0.25). Lab CLS is only 0.0025 because Lighthouse measures initial load only, while real users scrolling encounter massive shifts. The lab data reveals the root cause: div#bottom-wrapper has a shift score of 0.82 — this single element accounts for the vast majority of CLS. Examining the screenshot, this is the "Continue reading in the app" interstitial banner that dynamically inserts at the bottom of the viewport. When this banner renders after the user begins scrolling, it pushes all content above it and triggers a catastrophic layout shift. The fix is to make this element position: fixed or position: sticky — fixed-position elements do NOT cause layout shifts per CLS specification. Alternatively, if the banner must be in document flow, reserve its exact height with CSS before the JS that renders it fires. Additionally, lab data shows article body paragraphs (section.meteredContent p.css-ac37hb) and div#after-story-ad-3 both have shift scores of 0.088 — these are likely caused by ads or dynamic content injected between paragraphs during scroll.
Before
/* Current: banner inserted dynamically in normal flow */
#bottom-wrapper {
  /* no positioning — causes CLS when injected */
}
After
/* Fix: use fixed positioning so insertion causes zero CLS */
#bottom-wrapper {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 999;
}

/* Add padding to body to prevent content overlap */
body {
  padding-bottom: 80px; /* match banner height */
}
Expected impact
Eliminating the 0.82 shift from #bottom-wrapper alone should reduce field CLS from 0.95 → ~0.10. This single fix addresses ~85% of CLS. Combined with ad slot reservation (next recommendation), field p75 CLS could reach the good zone (<0.1).

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

Analyze Your Site Free