From 4b95153b9e74045ad10cdef5784216745ed3dba4 Mon Sep 17 00:00:00 2001 From: ManuelPauloAfonso Date: Sat, 9 May 2026 00:19:28 +0100 Subject: [PATCH] feat: auto-capture snippet console outputs at build time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add scripts/capture-outputs.js — runs each snippet in a headless Playwright browser and saves console output to snippets/outputs.json - Add scripts/inject-outputs.js — injects output prop into all MDX Snippet components from outputs.json - Update components/Snippet.jsx — renders Expected Output section - Wire both scripts into prebuild so outputs stay in sync automatically Closes #75 --- components/Snippet.jsx | 117 +++++----- package-lock.json | 48 ++++ package.json | 5 +- pages/CoreWebVitals/CLS.mdx | 2 +- pages/CoreWebVitals/INP.mdx | 5 +- pages/CoreWebVitals/LCP-Image-Entropy.mdx | 6 +- pages/CoreWebVitals/LCP-Subparts.mdx | 3 +- pages/CoreWebVitals/LCP-Trail.mdx | 4 +- pages/CoreWebVitals/LCP-Video-Candidate.mdx | 3 +- pages/CoreWebVitals/LCP.mdx | 3 +- .../DevTools-Overrides/Fetch-XHR-Timeline.mdx | 4 +- .../Interaction/Forced-Synchronous-Layout.mdx | 4 +- pages/Interaction/Input-Latency-Breakdown.mdx | 4 +- pages/Interaction/Interactions.mdx | 4 +- .../Layout-Shift-Loading-and-Interaction.mdx | 8 +- .../Long-Animation-Frames-Helpers.mdx | 5 +- ...ng-Animation-Frames-Script-Attribution.mdx | 2 +- pages/Interaction/Long-Animation-Frames.mdx | 4 +- pages/Interaction/LongTask.mdx | 5 +- pages/Interaction/Scroll-Performance.mdx | 4 +- pages/Loading/Back-Forward-Cache.mdx | 21 +- pages/Loading/CSS-Media-Queries-Analysis.mdx | 23 +- pages/Loading/Cache-Strategy-Analysis.mdx | 19 +- .../Client-Side-Redirect-Detection.mdx | 11 +- pages/Loading/Content-Visibility.mdx | 11 +- pages/Loading/Critical-CSS-Detection.mdx | 19 +- pages/Loading/Event-Processing-Time.mdx | 14 +- pages/Loading/FCP.mdx | 2 +- ...Find-Above-The-Fold-Lazy-Loaded-Images.mdx | 4 +- ...ind-Images-With-Lazy-and-Fetchpriority.mdx | 4 +- ...-Loaded-Images-outside-of-the-viewport.mdx | 4 +- .../Find-render-blocking-resources.mdx | 9 +- .../First-And-Third-Party-Script-Info.mdx | 11 +- .../First-And-Third-Party-Script-Timings.mdx | 18 +- ...eloaded-Loaded-and-used-above-the-fold.mdx | 23 +- pages/Loading/Inline-CSS-Info-and-Size.mdx | 5 +- pages/Loading/Inline-Script-Info-and-Size.mdx | 5 +- pages/Loading/JS-Execution-Time-Breakdown.mdx | 23 +- .../Loading/Prefetch-Resource-Validation.mdx | 3 +- pages/Loading/Priority-Hints-Audit.mdx | 9 +- pages/Loading/Resource-Hints-Validation.mdx | 39 +++- pages/Loading/Resource-Hints.mdx | 21 +- pages/Loading/SSR-Hydration-Data-Analysis.mdx | 9 +- pages/Loading/Script-Loading.mdx | 21 +- pages/Loading/Service-Worker-Analysis.mdx | 2 +- pages/Loading/TTFB.mdx | 9 +- .../Validate-Preload-Async-Defer-Scripts.mdx | 3 +- pages/Media/Image-Element-Audit.mdx | 2 +- pages/Media/SVG-Embedded-Bitmap-Analysis.mdx | 2 +- pages/Media/Video-Element-Audit.mdx | 2 +- .../Network-Bandwidth-Connection-Quality.mdx | 6 +- scripts/capture-outputs.js | 132 +++++++++++ scripts/inject-outputs.js | 105 +++++++++ snippets/outputs.json | 206 ++++++++++++++++++ 54 files changed, 935 insertions(+), 102 deletions(-) create mode 100644 scripts/capture-outputs.js create mode 100644 scripts/inject-outputs.js create mode 100644 snippets/outputs.json diff --git a/components/Snippet.jsx b/components/Snippet.jsx index 85683de..f0bab92 100644 --- a/components/Snippet.jsx +++ b/components/Snippet.jsx @@ -1,14 +1,17 @@ +import javascript from "highlight.js/lib/languages/javascript"; import { useState, useMemo } from "react"; import hljs from "highlight.js/lib/core"; -import javascript from "highlight.js/lib/languages/javascript"; hljs.registerLanguage("javascript", javascript); -export function Snippet({ code }) { +export function Snippet({ code, output }) { const [copied, setCopied] = useState(false); const [hovered, setHovered] = useState(false); - const highlighted = useMemo(() => hljs.highlight(code, { language: "javascript" }).value, [code]); + const highlighted = useMemo( + () => hljs.highlight(code, { language: "javascript" }).value, + [code], + ); async function handleCopy() { try { @@ -27,58 +30,72 @@ export function Snippet({ code }) { } return ( -
setHovered(true)} - onMouseLeave={() => setHovered(false)} - > -
+      {/* Code block */}
+      
setHovered(true)} + onMouseLeave={() => setHovered(false)} + className="nx-relative" > - -
-
- + + +
+ +
+ + {output && ( +
+

+ Expected Output +

+
+            {output}
+          
+
+ )}
); } diff --git a/package-lock.json b/package-lock.json index e8d41dc..8e15799 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ }, "devDependencies": { "eslint": "^10.0.2", + "playwright": "^1.59.1", "terser": "^5.36.0", "vercel": "^32.4.1" } @@ -14067,6 +14068,53 @@ "node": ">=4" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/package.json b/package.json index aecf70f..f28f900 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,11 @@ "dev": "next dev", "build": "next build", "postbuild": "next-sitemap", - "prebuild": "rm -rf .next && node scripts/generate-llms.js", + "prebuild": "rm -rf .next && node scripts/generate-llms.js && node scripts/capture-outputs.js && node scripts/inject-outputs.js", "check:consistency": "node scripts/check-consistency.js", "generate-skills": "node scripts/generate-skills.js", + "inject-outputs": "node scripts/inject-outputs.js", + "capture-outputs": "node scripts/capture-outputs.js", "generate-skills:check": "node scripts/generate-skills.js && git diff --exit-code -- skills dist", "install-skills": "node scripts/install-skills.js", "install-global": "node scripts/install-global.js", @@ -38,6 +40,7 @@ }, "devDependencies": { "eslint": "^10.0.2", + "playwright": "^1.59.1", "terser": "^5.36.0", "vercel": "^32.4.1" }, diff --git a/pages/CoreWebVitals/CLS.mdx b/pages/CoreWebVitals/CLS.mdx index 8258978..d3c44ab 100644 --- a/pages/CoreWebVitals/CLS.mdx +++ b/pages/CoreWebVitals/CLS.mdx @@ -23,7 +23,7 @@ Unexpected layout shifts are frustrating and can cause users to click the wrong ### Snippet - + ### Understanding CLS diff --git a/pages/CoreWebVitals/INP.mdx b/pages/CoreWebVitals/INP.mdx index 7c99af7..a2268f3 100644 --- a/pages/CoreWebVitals/INP.mdx +++ b/pages/CoreWebVitals/INP.mdx @@ -58,7 +58,10 @@ flowchart TD ### Snippet - + 16ms will be tracked. +Call getINP() to see current INP value. +Call getINPDetails() for full interaction list.`} /> ### Understanding INP diff --git a/pages/CoreWebVitals/LCP-Image-Entropy.mdx b/pages/CoreWebVitals/LCP-Image-Entropy.mdx index 4ff3d76..1f7540a 100644 --- a/pages/CoreWebVitals/LCP-Image-Entropy.mdx +++ b/pages/CoreWebVitals/LCP-Image-Entropy.mdx @@ -27,7 +27,11 @@ BPP = (file size in bits) / (width × height) ### Snippet - + ### Understanding the Results diff --git a/pages/CoreWebVitals/LCP-Subparts.mdx b/pages/CoreWebVitals/LCP-Subparts.mdx index d54534d..64af282 100644 --- a/pages/CoreWebVitals/LCP-Subparts.mdx +++ b/pages/CoreWebVitals/LCP-Subparts.mdx @@ -48,7 +48,8 @@ flowchart LR ### Snippet - + ### Understanding the Results diff --git a/pages/CoreWebVitals/LCP-Trail.mdx b/pages/CoreWebVitals/LCP-Trail.mdx index 0e03fd2..ca660ab 100644 --- a/pages/CoreWebVitals/LCP-Trail.mdx +++ b/pages/CoreWebVitals/LCP-Trail.mdx @@ -13,7 +13,9 @@ Each time the browser promotes a larger element as the new LCP, the snippet assi ### Snippet - + ### How It Works diff --git a/pages/CoreWebVitals/LCP-Video-Candidate.mdx b/pages/CoreWebVitals/LCP-Video-Candidate.mdx index a4bec5b..71fd103 100644 --- a/pages/CoreWebVitals/LCP-Video-Candidate.mdx +++ b/pages/CoreWebVitals/LCP-Video-Candidate.mdx @@ -51,7 +51,8 @@ A `` in `` shortcuts this chain, allowing the browser ### Snippet - + ### Understanding the Results diff --git a/pages/CoreWebVitals/LCP.mdx b/pages/CoreWebVitals/LCP.mdx index e7fb823..14b375e 100644 --- a/pages/CoreWebVitals/LCP.mdx +++ b/pages/CoreWebVitals/LCP.mdx @@ -17,7 +17,8 @@ Quick check for [Largest Contentful Paint](https://web.dev/articles/lcp), a Core ### Snippet - + ### Understanding LCP diff --git a/pages/DevTools-Overrides/Fetch-XHR-Timeline.mdx b/pages/DevTools-Overrides/Fetch-XHR-Timeline.mdx index 2503399..7585d4d 100644 --- a/pages/DevTools-Overrides/Fetch-XHR-Timeline.mdx +++ b/pages/DevTools-Overrides/Fetch-XHR-Timeline.mdx @@ -25,7 +25,7 @@ Console snippets can only see requests that have already completed and are visib Add this inside a ` +For independent scripts (analytics, ads): + +For ES + +For critical inline + +console.groupEnd +console.groupEnd`} /> ### Understanding the Results diff --git a/pages/Loading/Service-Worker-Analysis.mdx b/pages/Loading/Service-Worker-Analysis.mdx index 8b6a57a..d98731f 100644 --- a/pages/Loading/Service-Worker-Analysis.mdx +++ b/pages/Loading/Service-Worker-Analysis.mdx @@ -67,7 +67,7 @@ sequenceDiagram ### Snippet - + ## Interpreting the Results diff --git a/pages/Loading/TTFB.mdx b/pages/Loading/TTFB.mdx index a7d836b..6dea505 100644 --- a/pages/Loading/TTFB.mdx +++ b/pages/Loading/TTFB.mdx @@ -26,7 +26,7 @@ Time to First Byte (TTFB) measures the time from when the user starts navigating ### Snippet - + ## Measure TTFB sub-parts @@ -66,7 +66,10 @@ sequenceDiagram ### Snippet - + ## Measure TTFB for all resources @@ -76,7 +79,7 @@ Analyzes TTFB for every resource loaded on the page (scripts, stylesheets, image ### Snippet - + ## Browser Support diff --git a/pages/Loading/Validate-Preload-Async-Defer-Scripts.mdx b/pages/Loading/Validate-Preload-Async-Defer-Scripts.mdx index 49aba70..9292bca 100644 --- a/pages/Loading/Validate-Preload-Async-Defer-Scripts.mdx +++ b/pages/Loading/Validate-Preload-Async-Defer-Scripts.mdx @@ -64,7 +64,8 @@ With preload on async script: ### Snippet - + ### Understanding the Results diff --git a/pages/Media/Image-Element-Audit.mdx b/pages/Media/Image-Element-Audit.mdx index db8edd2..2ac395f 100644 --- a/pages/Media/Image-Element-Audit.mdx +++ b/pages/Media/Image-Element-Audit.mdx @@ -70,7 +70,7 @@ For maximum format coverage, wrap images in a `` element: ### Snippet - + elements found on this page.`} /> ### Understanding the Results diff --git a/pages/Media/SVG-Embedded-Bitmap-Analysis.mdx b/pages/Media/SVG-Embedded-Bitmap-Analysis.mdx index 3966de7..54fa0c4 100644 --- a/pages/Media/SVG-Embedded-Bitmap-Analysis.mdx +++ b/pages/Media/SVG-Embedded-Bitmap-Analysis.mdx @@ -37,7 +37,7 @@ The correct pattern is to reference bitmap assets as separate files: ### Snippet - + ### Understanding the Results diff --git a/pages/Media/Video-Element-Audit.mdx b/pages/Media/Video-Element-Audit.mdx index df4ed68..2f5abcb 100644 --- a/pages/Media/Video-Element-Audit.mdx +++ b/pages/Media/Video-Element-Audit.mdx @@ -58,7 +58,7 @@ Every video on a page falls into one of three roles, each with its own optimal c ### Snippet - + elements found on this page.`} /> ### Understanding the Results diff --git a/pages/Resources/Network-Bandwidth-Connection-Quality.mdx b/pages/Resources/Network-Bandwidth-Connection-Quality.mdx index 77c459e..37f2ecc 100644 --- a/pages/Resources/Network-Bandwidth-Connection-Quality.mdx +++ b/pages/Resources/Network-Bandwidth-Connection-Quality.mdx @@ -23,7 +23,11 @@ Network quality directly affects web performance. Segmenting metrics by connecti ### Snippet - + ### Understanding the Results diff --git a/scripts/capture-outputs.js b/scripts/capture-outputs.js new file mode 100644 index 0000000..ca5ba67 --- /dev/null +++ b/scripts/capture-outputs.js @@ -0,0 +1,132 @@ +#!/usr/bin/env node +// Runs each snippet in a headless browser and captures console output. +// Writes results to snippets/outputs.json + +const { chromium } = require("playwright"); +const fs = require("fs"); +const path = require("path"); + +const ROOT = path.join(__dirname, ".."); +const SNIPPETS_DIR = path.join(ROOT, "snippets"); +const OUTPUT_FILE = path.join(SNIPPETS_DIR, "outputs.json"); + +// Collect all .js files recursively from snippets/ +function getSnippetFiles(dir) { + const files = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.isDirectory()) { + files.push(...getSnippetFiles(path.join(dir, entry.name))); + } else if (entry.name.endsWith(".js")) { + files.push(path.join(dir, entry.name)); + } + } + return files; +} + +// Convert absolute path to a relative key like "CoreWebVitals/LCP" +function fileToKey(filePath) { + const rel = path.relative(SNIPPETS_DIR, filePath); + return rel.replace(/\.js$/, ""); +} + +async function captureOutput(browser, code) { + const context = await browser.newContext(); + const page = await context.newPage(); + const logs = []; + + // Capture all console messages + page.on("console", (msg) => { + const type = msg.type(); // log, warn, error, info, etc. + const text = msg.text(); + logs.push({ type, text }); + }); + + // Navigate to a blank page (snippets use browser APIs like performance) + await page.goto("about:blank"); + + try { + // Execute the snippet code in the page context + await page.evaluate(code); + // Give async snippets time to finish + await page.waitForTimeout(200); + } catch (err) { + logs.push({ type: "error", text: `Execution error: ${err.message}` }); + } + + await context.close(); + return logs; +} + +// Remove %c directives and their associated CSS style arguments +// e.g. "%cLCP Active font-weight: bold" → "LCP Active" +function cleanConsoleText(text) { + // Split on CSS style args that follow %c (they look like CSS property strings) + // Strategy: remove %c, then remove the CSS strings that follow as separate tokens + let result = text; + + // Remove %c placeholders + result = result.replace(/%c/g, ""); + + // Remove CSS style strings — they appear as standalone segments containing + // CSS properties like "font-weight: bold; font-size: 14px;" + result = result + .split("\n") + .map((line) => + // A line is a CSS style arg if it matches "prop: value;" pattern and + // has no normal words/sentences (no spaces before the first colon) + /^[\w-]+\s*:/.test(line.trim()) && !/^(https?|file):/.test(line.trim()) + ? null + : line, + ) + .filter((line) => line !== null) + .join("\n"); + + // Also remove inline CSS blobs that ended up on same line after %c removal + // e.g. "LCP Active font-weight: bold; font-size: 14px;" + result = result.replace(/\s+([\w-]+\s*:[^;]+;(\s*[\w-]+\s*:[^;]+;)*)/g, ""); + + return result.trim(); +} + +function formatLogs(logs) { + return logs + .filter((l) => l.text && l.text.trim() !== "") + .map((l) => cleanConsoleText(l.text)) + .filter((t) => t !== "") + .join("\n"); +} + +async function main() { + const snippetFiles = getSnippetFiles(SNIPPETS_DIR); + console.log(`Found ${snippetFiles.length} snippets\n`); + + const browser = await chromium.launch(); + const results = {}; + + for (const file of snippetFiles) { + const key = fileToKey(file); + const code = fs.readFileSync(file, "utf-8"); + + process.stdout.write(` Running: ${key} ... `); + + try { + const logs = await captureOutput(browser, code); + const output = formatLogs(logs); + results[key] = { output: output || "(no output)", error: null }; + console.log("✓"); + } catch (err) { + results[key] = { output: null, error: err.message }; + console.log(`✗ ${err.message}`); + } + } + + await browser.close(); + + fs.writeFileSync(OUTPUT_FILE, JSON.stringify(results, null, 2)); + console.log(`\nOutputs saved to snippets/outputs.json`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/inject-outputs.js b/scripts/inject-outputs.js new file mode 100644 index 0000000..6f5b8f6 --- /dev/null +++ b/scripts/inject-outputs.js @@ -0,0 +1,105 @@ +const fs = require("fs"); +const path = require("path"); + +const ROOT = path.join(__dirname, ".."); +const PAGES_DIR = path.join(ROOT, "pages"); +const SNIPPETS_DIR = path.join(ROOT, "snippets"); +const OUTPUT_FILE = path.join(SNIPPETS_DIR, "outputs.json"); + +if (!fs.existsSync(OUTPUT_FILE)) { + console.error( + "snippets/outputs.json not found. Run capture-outputs.js first.", + ); + process.exit(1); +} + +const outputs = JSON.parse(fs.readFileSync(OUTPUT_FILE, "utf-8")); + +function getMdxFiles(dir) { + const files = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.isDirectory()) { + files.push(...getMdxFiles(path.join(dir, entry.name))); + } else if (entry.name.endsWith(".mdx")) { + files.push(path.join(dir, entry.name)); + } + } + return files; +} + +function extractKeyFromImport(importLine) { + const match = importLine.match(/snippets\/(.+?)\.js\?raw/); + return match ? match[1] : null; +} + +function getSnippetImports(content) { + const map = {}; + const lines = content.split("\n"); + for (const line of lines) { + if (!line.includes("snippets/") || !line.includes("?raw")) continue; + const varMatch = line.match(/import\s+(\w+)\s+from/); + const key = extractKeyFromImport(line); + if (varMatch && key) { + map[varMatch[1]] = key; + } + } + return map; +} + +function processFile(mdxPath) { + let content = fs.readFileSync(mdxPath, "utf-8"); + const rel = path.relative(ROOT, mdxPath); + + const snippetImports = getSnippetImports(content); + if (Object.keys(snippetImports).length === 0) return false; + + let changed = false; + + for (const [varName, outputKey] of Object.entries(snippetImports)) { + const outputData = outputs[outputKey]; + if (!outputData || !outputData.output) continue; + + const outputText = outputData.output; + + const withoutOutput = new RegExp( + ``, + "g", + ); + const withOutput = new RegExp( + ``, + "g", + ); + + const escaped = outputText + .replace(/\\/g, "\\\\") + .replace(/`/g, "\\`") + .replace(/\$/g, "\\$"); + + const replacement = ``; + + if (withoutOutput.test(content)) { + content = content.replace(withoutOutput, replacement); + changed = true; + } else if (withOutput.test(content)) { + content = content.replace(withOutput, replacement); + changed = true; + } + } + + if (changed) { + fs.writeFileSync(mdxPath, content); + console.log(` updated: ${rel}`); + } + + return changed; +} + +const mdxFiles = getMdxFiles(PAGES_DIR); +console.log(`Processing ${mdxFiles.length} MDX files...\n`); + +let updatedCount = 0; +for (const file of mdxFiles) { + if (processFile(file)) updatedCount++; +} + +console.log(`\nDone. Updated ${updatedCount} files.`); diff --git a/snippets/outputs.json b/snippets/outputs.json new file mode 100644 index 0000000..26a8f4e --- /dev/null +++ b/snippets/outputs.json @@ -0,0 +1,206 @@ +{ + "CoreWebVitals/CLS": { + "output": "Call getCLS() anytime to check current value.", + "error": null + }, + "CoreWebVitals/INP": { + "output": "⚡ INP Tracking Active\nInteractions with duration > 16ms will be tracked.\nCall getINP() to see current INP value.\nCall getINPDetails() for full interaction list.", + "error": null + }, + "CoreWebVitals/LCP-Image-Entropy": { + "output": "Deprecated API for given entry type.\n🖼️ Image Entropy Analysis\nNo images with measurable entropy found.\n(Data URLs and cross-origin images without CORS are excluded)\nconsole.groupEnd", + "error": null + }, + "CoreWebVitals/LCP-Subparts": { + "output": "📊 LCP Subparts Analysis Active\nWaiting for LCP...", + "error": null + }, + "CoreWebVitals/LCP-Trail": { + "output": "⏱️ LCP Trail Active\nHighlights all LCP candidate elements with distinct colors.\nDeprecated API for given entry type.", + "error": null + }, + "CoreWebVitals/LCP-Video-Candidate": { + "output": "Deprecated API for given entry type.\n⚠️ No LCP entries found. Run this snippet before interacting with the page, or reload and run it immediately.", + "error": null + }, + "CoreWebVitals/LCP": { + "output": "⏱️ LCP Tracking Active\nLCP may update as larger elements load.", + "error": null + }, + "DevTools-Overrides/Fetch-XHR-Timeline-inject": { + "output": "(no output)", + "error": null + }, + "DevTools-Overrides/Fetch-XHR-Timeline-read": { + "output": "No data found. Make sure the inject snippet is active via DevTools Overrides and reload the page.", + "error": null + }, + "Interaction/Forced-Synchronous-Layout": { + "output": "⚡ FSL Detector Active\nMonitoring class/style mutations and geometric property access.\nCall getFSLSummary() for the report or stopFSLDetector() to stop.", + "error": null + }, + "Interaction/Input-Latency-Breakdown": { + "output": "⌨️ Input Latency Breakdown Active\nInteract with the page (click, type, etc.).\nCall getInputLatencyBreakdown() for the aggregated report.", + "error": null + }, + "Interaction/Interactions": { + "output": "👆 Interaction Tracking Active\nInteract with the page to see interaction details.\nCall getInteractionSummary() for a summary.", + "error": null + }, + "Interaction/Layout-Shift-Loading-and-Interaction": { + "output": "📐 Layout Shift Tracking Active\nCurrent CLS: 🟢 0.0000\nInteract with the page to see new shifts.\nCall getLayoutShiftSummary() for full analysis.\nDeprecated API for given entry type.\nDeprecated API for given entry type.\nDeprecated API for given entry type.", + "error": null + }, + "Interaction/Long-Animation-Frames-Helpers": { + "output": "🔧 LoAF Helpers Loaded\nObserving long animation frames (>50ms)...\nQuick\nAll", + "error": null + }, + "Interaction/Long-Animation-Frames-Script-Attribution": { + "output": "✅ No long frames detected", + "error": null + }, + "Interaction/Long-Animation-Frames": { + "output": "🎬 Long Animation Frames Tracking Active\nFrames with blocking duration will be logged.\nCall getLoAFSummary() for full analysis.", + "error": null + }, + "Interaction/LongTask": { + "output": "⏱️ Long Tasks Tracking Active\nTasks blocking the main thread for >50ms will be logged.\nCall getLongTaskSummary() for statistics.\nDeprecated API for given entry type.", + "error": null + }, + "Interaction/Scroll-Performance": { + "output": "📜 Scroll Performance Analysis Active\nScroll to measure FPS. Non-passive listener registrations will be warned.\nCall getScrollSummary() for the full report.", + "error": null + }, + "Loading/Back-Forward-Cache": { + "output": "🚀 bfcache Analysis Running...\nResults will appear shortly.\nCall checkBfcache() anytime to re-run analysis.\n🟡 bfcache\n📊\nℹ️ This page was NOT restored from bfcache\n(Either first visit or bfcache was blocked on previous navigation)\n🕐 Navigation\n🔍 Potential\n[Object, Object]\n💡\n1. Close WebSocket connections before page hide.\n🧪 How to\n1. Run this snippet (you already did this ✓)\n2. Navigate to another page\n3. Click browser Back button\n4. Check console for restoration message\nOr use Chrome DevTools → Application → Back/forward cache\nconsole.groupEnd\nℹ️ For detailed reasons, use Chrome 123+ and navigate back to this page.", + "error": null + }, + "Loading/CSS-Media-Queries-Analysis": { + "output": "📊 CSS Media Queries Analysis (min-width > 768px)\nTotal @media rules found: 0\nTotal classes: 0\nTotal properties: 0\nconsole.groupEnd\n💾 POTENTIAL MOBILE SAVINGS\nEstimated CSS bytes that mobile doesn't\n└─ From inline CSS: 0 Bytes\n└─ From external files: 0 Bytes\n💡 By splitting these styles into separate files with media queries,\nmobile users won't need to download, parse, or process this CSS.\nconsole.groupEnd\n🔷 INLINE CSS (