From cf172963b2c5b9a771abf55693e4f511ea5d965c Mon Sep 17 00:00:00 2001 From: Joan Leon Date: Sat, 18 Apr 2026 19:37:03 +0200 Subject: [PATCH 1/3] feat: add Fetch & XHR Timeline DevTools Override snippet Adds a two-part DevTools Override snippet that intercepts all fetch() and XHR calls from page initialization and correlates them with LCP timing. - Inject snippet patches window.fetch and XHR.prototype early, captures all calls including cross-origin, and records LCP via PerformanceObserver - Read snippet reports four diagnostic sections: calls before LCP (critical path candidates), bootstrap errors (failed calls before LCP), auth-related calls (detected by URL pattern), and complete network inventory - Auth detection covers /auth, /login, /token, /session, /oauth, /me, /whoami, /identity path segments and authIndexType query params --- .../DevTools-Overrides/Fetch-XHR-Timeline.mdx | 98 +++++++++ pages/DevTools-Overrides/_meta.json | 8 + pages/DevTools-Overrides/index.mdx | 67 ++++++ pages/_meta.json | 3 + .../Fetch-XHR-Timeline-inject.js | 75 +++++++ .../Fetch-XHR-Timeline-read.js | 191 ++++++++++++++++++ 6 files changed, 442 insertions(+) create mode 100644 pages/DevTools-Overrides/Fetch-XHR-Timeline.mdx create mode 100644 pages/DevTools-Overrides/_meta.json create mode 100644 pages/DevTools-Overrides/index.mdx create mode 100644 snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js create mode 100644 snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js diff --git a/pages/DevTools-Overrides/Fetch-XHR-Timeline.mdx b/pages/DevTools-Overrides/Fetch-XHR-Timeline.mdx new file mode 100644 index 0000000..5c8f335 --- /dev/null +++ b/pages/DevTools-Overrides/Fetch-XHR-Timeline.mdx @@ -0,0 +1,98 @@ +import injectSnippet from '../../snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js?raw' +import readSnippet from '../../snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js?raw' +import { Snippet } from '../../components/Snippet' + +# Fetch & XHR Timeline + +### Overview + +Intercepts every `fetch()` and `XMLHttpRequest` call made during the page lifecycle and correlates them with the [Largest Contentful Paint (LCP)](https://web.dev/articles/lcp) timing. + +Console snippets can only see requests that have already completed and are visible in the Resource Timing API. That excludes cross-origin requests without `Timing-Allow-Origin`, requests made by service workers, and calls that fail before a response is received. This snippet patches `window.fetch` and `XMLHttpRequest.prototype` at page initialization, so every call is captured regardless of origin or outcome. + +**What it helps diagnose:** + +- API calls that complete *before* LCP and may be blocking the critical rendering path +- Failed or errored requests during framework bootstrap (Angular, React, Vue resolvers and guards) +- Unnecessary authentication checks on pages where the user is known to be unauthenticated +- The full request inventory of a page, including calls invisible to the Network panel timing data + +> This snippet requires [DevTools Overrides](/DevTools-Overrides). Read [What are DevTools Overrides?](/DevTools-Overrides) for setup instructions. + +--- + +### Part 1 — Inject snippet + +Add this inside a ` +``` + +**Step 4 — Reload and read** + +Reload the page. The override fires before any other script. Once the page has loaded, run the companion **read snippet** from the console to see the captured data. + +--- + +### Workflow + +``` +Setup (once) Every page load Console (on demand) +───────────── ──────────────── ─────────────────── +Enable Overrides → Inject snippet runs → Paste read snippet +Save HTML file Captures data into See results correlated +Add diff --git a/snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js b/snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js new file mode 100644 index 0000000..247a3b9 --- /dev/null +++ b/snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js @@ -0,0 +1,191 @@ +// Fetch & XHR Timeline — Run in console after page load +// https://webperf-snippets.nucliweb.net + +(() => { + const calls = window.__perfCalls; + + if (!calls) { + console.warn( + "No data found. Make sure the inject snippet is active via DevTools Overrides and reload the page." + ); + return; + } + + const lcpEntries = performance.getEntriesByType("largest-contentful-paint"); + const lcpTime = lcpEntries.length + ? Math.round(lcpEntries[lcpEntries.length - 1].startTime) + : window.__lcpTime ?? null; + + const isAuthCall = (url = "") => { + try { + const { pathname, searchParams } = new URL(url, location.href); + const path = pathname.toLowerCase(); + if (/\/(auth|login|logout|token|session|whoami|identity|oauth)/.test(path)) return true; + if (/(^|\/)me(\/|$)/.test(path)) return true; + if (searchParams.has("authIndexType") || searchParams.has("authIndexValue")) return true; + return false; + } catch { + return false; + } + }; + + const isError = (c) => + c.status === "ERROR" || (typeof c.status === "number" && c.status >= 400); + + const beforeLCP = lcpTime ? calls.filter((c) => c.end <= lcpTime) : []; + const afterLCP = lcpTime ? calls.filter((c) => c.end > lcpTime) : calls; + const errors = calls.filter(isError); + const bootstrapErrors = errors.filter((c) => !lcpTime || c.end <= lcpTime); + const postLCPErrors = errors.filter((c) => lcpTime && c.end > lcpTime); + const authCalls = calls.filter((c) => isAuthCall(c.url)); + const authBeforeLCP = lcpTime ? authCalls.filter((c) => c.end <= lcpTime) : []; + + const row = (c) => ({ + Type: c.type, + Status: c.status, + "Start (ms)": c.start, + "End (ms)": c.end, + "Duration (ms)": c.duration, + ...(lcpTime ? { "Before LCP": c.end <= lcpTime ? "⚠️ yes" : "no" } : {}), + URL: c.url?.length > 80 ? "..." + c.url.slice(-77) : c.url, + }); + + console.group( + "%c📡 Fetch & XHR Timeline", + "font-weight: bold; font-size: 14px;" + ); + console.log(""); + + if (lcpTime) { + console.log(`%cLCP: ${lcpTime}ms`, "font-weight: bold;"); + console.log(""); + } else { + console.warn("LCP timing not available — before/after LCP analysis skipped. Make sure the inject snippet is active and reload the page."); + console.log(""); + } + + if (calls.length === 0) { + console.log( + "%c✅ No fetch or XHR calls detected during page load", + "color: #22c55e; font-weight: bold;" + ); + console.groupEnd(); + return; + } + + // Summary + console.log("%cSummary:", "font-weight: bold;"); + console.log(` Total calls: ${calls.length}`); + if (lcpTime) { + console.log( + ` ${beforeLCP.length > 0 ? "⚠️" : "✅"} Before LCP (critical path candidates): ${beforeLCP.length}` + ); + console.log( + ` ✅ After LCP (confirmed non-blocking): ${afterLCP.length}` + ); + } + if (bootstrapErrors.length > 0) { + console.log(` 🔴 Bootstrap errors (before LCP): ${bootstrapErrors.length}`); + } + if (postLCPErrors.length > 0) { + console.log(` 🔴 Errors after LCP: ${postLCPErrors.length}`); + } + if (authCalls.length > 0) { + console.log( + ` 🔑 Auth-related calls: ${authCalls.length}` + + (authBeforeLCP.length > 0 ? ` — ${authBeforeLCP.length} before LCP ⚠️` : "") + ); + } + + // Case 1 — API calls blocking LCP + if (beforeLCP.length > 0) { + console.log(""); + console.group( + "%c⚠️ Calls completing before LCP — investigate as critical path blockers", + "color: #ef4444; font-weight: bold;" + ); + console.log( + "%cCalls completing before LCP are candidates for deferral, aggressive caching, or parallelization. If a framework waits for their response before rendering, each one adds directly to LCP.", + "color: #6b7280;" + ); + console.table(beforeLCP.map(row)); + console.groupEnd(); + } + + // Case 2 — Bootstrap errors (before LCP) + if (bootstrapErrors.length > 0) { + console.log(""); + console.group( + "%c🔴 Bootstrap errors — failed calls before LCP", + "color: #ef4444; font-weight: bold;" + ); + console.log( + "%cA failed auth check or config endpoint during bootstrap can add hundreds of ms before first paint — even when the error is caught silently.", + "color: #6b7280;" + ); + console.table( + bootstrapErrors.map((c) => ({ + Type: c.type, + Status: c.status, + Error: c.error || "-", + "Start (ms)": c.start, + "Duration (ms)": c.duration, + URL: c.url?.length > 80 ? "..." + c.url.slice(-77) : c.url, + })) + ); + console.groupEnd(); + } + + if (postLCPErrors.length > 0) { + console.log(""); + console.group( + "%c🔴 Failed calls after LCP", + "color: #ef4444; font-weight: bold;" + ); + console.table( + postLCPErrors.map((c) => ({ + Type: c.type, + Status: c.status, + Error: c.error || "-", + "Start (ms)": c.start, + "Duration (ms)": c.duration, + URL: c.url?.length > 80 ? "..." + c.url.slice(-77) : c.url, + })) + ); + console.groupEnd(); + } + + // Case 4 — Unnecessary auth checks + if (authCalls.length > 0) { + console.log(""); + console.group( + "%c🔑 Auth-related calls — verify these are necessary", + "color: #f59e0b; font-weight: bold;" + ); + console.log( + "%cAuth calls on pages where the user is known to be unauthenticated add latency without value. Cross-reference with your routing guards.", + "color: #6b7280;" + ); + console.table(authCalls.map(row)); + console.groupEnd(); + } + + // Case 3 — Complete network inventory + console.log(""); + console.group("%c📋 Complete network inventory", "font-weight: bold;"); + console.log( + "%cIncludes cross-origin calls invisible to the Network panel timing data (no Timing-Allow-Origin header).", + "color: #6b7280;" + ); + console.table( + calls + .sort((a, b) => a.start - b.start) + .map((c) => ({ + ...row(c), + Auth: isAuthCall(c.url) ? "🔑" : "-", + })) + ); + console.groupEnd(); + + console.groupEnd(); +})(); From 71ac357410670ca64bef90712f21a0ca582e55f5 Mon Sep 17 00:00:00 2001 From: Joan Leon Date: Mon, 20 Apr 2026 17:09:50 +0200 Subject: [PATCH 2/3] Fix linter error --- snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js b/snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js index f94bdbb..fbd5c01 100644 --- a/snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js +++ b/snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js @@ -1,4 +1,3 @@ - From 77d47e3f81eecfc8de0f416f2c17d18ef9d7035f Mon Sep 17 00:00:00 2001 From: Joan Leon Date: Mon, 20 Apr 2026 17:17:39 +0200 Subject: [PATCH 3/3] Update the number of scripts --- README.md | 6 +++--- SKILLS.md | 6 +++--- scripts/check-consistency.js | 2 +- skills/webperf/SKILL.md | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index df109d7..16f9bda 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A curated collection of JavaScript snippets to measure and debug Web Performance [![CI](https://github.com/nucliweb/webperf-snippets/actions/workflows/ci.yml/badge.svg)](https://github.com/nucliweb/webperf-snippets/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/nucliweb/webperf-snippets)](https://github.com/nucliweb/webperf-snippets/releases) -[![Snippets](https://img.shields.io/badge/snippets-47-0f766e)](https://webperf-snippets.nucliweb.net) +[![Snippets](https://img.shields.io/badge/snippets-48-0f766e)](https://webperf-snippets.nucliweb.net) [![License](https://img.shields.io/github/license/nucliweb/webperf-snippets)](./LICENSE) [![Star History](https://img.shields.io/github/stars/nucliweb/webperf-snippets?style=social)](https://star-history.com/#nucliweb/webperf-snippets&Date) @@ -87,9 +87,9 @@ This installs skills to `~/.claude/skills/` for use across any project. | Skill | Snippets | Description | | ------------------------- | -------- | ---------------------------------------------------------------- | -| `webperf` | 47 | Main entry point for all web performance analysis | +| `webperf` | 48 | Main entry point for all web performance analysis | | `webperf-core-web-vitals` | 7 | LCP, CLS, INP measurements with detailed breakdowns | -| `webperf-loading` | 28 | TTFB, FCP, script/font analysis, resource hints, service workers | +| `webperf-loading` | 29 | TTFB, FCP, script/font analysis, resource hints, service workers | | `webperf-interaction` | 8 | Long tasks, animation frames, scroll jank, INP debugging | | `webperf-media` | 3 | Image/video audits, lazy loading validation, SVG analysis | | `webperf-resources` | 1 | Network bandwidth, connection quality, adaptive loading | diff --git a/SKILLS.md b/SKILLS.md index 97fafbe..53071bb 100644 --- a/SKILLS.md +++ b/SKILLS.md @@ -6,7 +6,7 @@ A collection of [Agent Skills](https://agentskills.io/) for measuring and debugg ## Why WebPerf Skills? -These skills transform 47 battle-tested JavaScript snippets into agent capabilities for any skills-compatible AI coding assistant: +These skills transform 48 battle-tested JavaScript snippets into agent capabilities for any skills-compatible AI coding assistant: - **Browser Console Integration**: Run performance measurements directly in Chrome DevTools - **Real-time Analysis**: Measure actual user experience on live pages @@ -22,7 +22,7 @@ These skills transform 47 battle-tested JavaScript snippets into agent capabilit | ------------------------------------------------------- | ---------- | ------------------------------------------------------------ | | **[webperf](#webperf)** | Meta-skill | "Audit performance", "check web vitals", "analyze this page" | | **[webperf-core-web-vitals](#webperf-core-web-vitals)** | 7 | "Debug LCP", "check CLS", "measure INP" | -| **[webperf-loading](#webperf-loading)** | 28 | "Analyze TTFB", "check render-blocking", "audit scripts" | +| **[webperf-loading](#webperf-loading)** | 29 | "Analyze TTFB", "check render-blocking", "audit scripts" | | **[webperf-interaction](#webperf-interaction)** | 8 | "Debug jank", "long tasks", "animation frames" | | **[webperf-media](#webperf-media)** | 3 | "Audit images", "optimize video", "lazy loading" | | **[webperf-resources](#webperf-resources)** | 1 | "Check bandwidth", "network quality" | @@ -105,7 +105,7 @@ The main entry point that helps identify the right skill for your performance qu **What it does:** - Routes to the appropriate specialized skill -- Provides overview of all 47 available snippets +- Provides overview of all 48 available snippets - Suggests which skill to use based on your question ### webperf-core-web-vitals diff --git a/scripts/check-consistency.js b/scripts/check-consistency.js index d59ab3b..56d99e2 100644 --- a/scripts/check-consistency.js +++ b/scripts/check-consistency.js @@ -203,7 +203,7 @@ function verifyPublishedCounts(errors) { ok: skillsDoc.includes(`These skills transform ${total} battle-tested JavaScript snippets`) && skillsDoc.includes(`| **[webperf-loading](#webperf-loading)** | ${categoryCounts.Loading}`) && - skillsDoc.includes('Provides overview of all 47 available snippets'), + skillsDoc.includes(`Provides overview of all ${total} available snippets`), }, { file: 'skills/webperf/SKILL.md', diff --git a/skills/webperf/SKILL.md b/skills/webperf/SKILL.md index c051ef4..78fc1cf 100644 --- a/skills/webperf/SKILL.md +++ b/skills/webperf/SKILL.md @@ -13,14 +13,14 @@ metadata: # WebPerf Snippets Toolkit -A collection of 47 JavaScript snippets for measuring and debugging web performance in Chrome DevTools. Each snippet runs in the browser console and outputs structured, color-coded results. +A collection of 48 JavaScript snippets for measuring and debugging web performance in Chrome DevTools. Each snippet runs in the browser console and outputs structured, color-coded results. ## Quick Reference | Skill | Snippets | Trigger phrases | |-------|----------|-----------------| | webperf-core-web-vitals | 7 | "debug LCP", "slow LCP", "CLS", "layout shifts", "INP", "interaction latency", "responsiveness" | -| webperf-loading | 28 | "TTFB", "slow server", "FCP", "render blocking", "font loading", "script loading", "resource hints", "service worker" | +| webperf-loading | 29 | "TTFB", "slow server", "FCP", "render blocking", "font loading", "script loading", "resource hints", "service worker" | | webperf-interaction | 8 | "jank", "scroll performance", "long tasks", "animation frames", "INP debug" | | webperf-media | 3 | "image audit", "lazy loading", "image optimization", "video audit" | | webperf-resources | 1 | "network quality", "bandwidth", "connection type", "save-data" |