From 63167bf379f9db28f7d4543ed0550befa9f9532b Mon Sep 17 00:00:00 2001 From: Joan Leon Date: Sun, 25 Oct 2026 12:34:46 +0100 Subject: [PATCH] feat(snippets): give the issues of the CSS, font and image snippets of Loading a stable code, and require one Content-Visibility, Critical-CSS-Detection, CSS-Media-Queries-Analysis, Find-Above-The-Fold-Lazy-Loaded-Images, Find-Images-With-Lazy-and-Fetchpriority, Find-non-Lazy-Loaded-Images-outside-of-the-viewport, Find-render-blocking-resources, Fonts-Preloaded-Loaded-and-used-above-the-fold, Inline-CSS-Info-and-Size and Webfont-Usage-Analyzer carry a code in every issue, with their tables under Issue codes in snippets/SCHEMA.md. They were the last of PENDING_ISSUE_CODES, which goes away: the contract and the catalog test hold every snippet to its codes. Webfont-Usage-Analyzer chose between two messages for one issue; it builds one issue per situation now, loaded-unused and redeclared. The issues about one font file or element name it in target. CONTRIBUTING.md describes the rule for a new snippet. No message changes. --- .../references/schema.md | 88 +++++++++++++++++++ .../webperf-interaction/references/schema.md | 88 +++++++++++++++++++ .../webperf-loading/references/schema.md | 88 +++++++++++++++++++ .../scripts/CSS-Media-Queries-Analysis.js | 4 +- .../scripts/Content-Visibility.js | 4 +- .../scripts/Critical-CSS-Detection.js | 4 +- .../Find-Above-The-Fold-Lazy-Loaded-Images.js | 4 +- ...Find-Images-With-Lazy-and-Fetchpriority.js | 4 +- ...y-Loaded-Images-outside-of-the-viewport.js | 4 +- .../scripts/Find-render-blocking-resources.js | 4 +- ...reloaded-Loaded-and-used-above-the-fold.js | 4 +- .../scripts/Inline-CSS-Info-and-Size.js | 4 +- .../scripts/Webfont-Usage-Analyzer.js | 4 +- .../skills/webperf-media/references/schema.md | 88 +++++++++++++++++++ .../webperf-resources/references/schema.md | 88 +++++++++++++++++++ CHANGELOG.md | 2 +- CONTRIBUTING.md | 6 +- cli/tests/e2e/snippet-contract.test.js | 7 +- cli/tests/e2e/webfont-usage.test.js | 7 +- cli/tests/helpers/issue-codes.js | 15 ---- cli/tests/unit/issue-codes.test.js | 7 +- .../references/schema.md | 88 +++++++++++++++++++ .../webperf-interaction/references/schema.md | 88 +++++++++++++++++++ .../webperf-loading/references/schema.md | 88 +++++++++++++++++++ .../scripts/CSS-Media-Queries-Analysis.js | 4 + .../scripts/Content-Visibility.js | 5 +- .../scripts/Critical-CSS-Detection.js | 6 ++ .../Find-Above-The-Fold-Lazy-Loaded-Images.js | 2 + ...Find-Images-With-Lazy-and-Fetchpriority.js | 1 + ...y-Loaded-Images-outside-of-the-viewport.js | 1 + .../scripts/Find-render-blocking-resources.js | 1 + ...reloaded-Loaded-and-used-above-the-fold.js | 12 ++- .../scripts/Inline-CSS-Info-and-Size.js | 5 ++ .../scripts/Webfont-Usage-Analyzer.js | 24 ++++- .../webperf-media/references/schema.md | 88 +++++++++++++++++++ .../webperf-resources/references/schema.md | 88 +++++++++++++++++++ .../scripts/CSS-Media-Queries-Analysis.js | 4 + .../scripts/Content-Visibility.js | 5 +- .../scripts/Critical-CSS-Detection.js | 6 ++ .../Find-Above-The-Fold-Lazy-Loaded-Images.js | 2 + ...Find-Images-With-Lazy-and-Fetchpriority.js | 1 + ...y-Loaded-Images-outside-of-the-viewport.js | 1 + .../scripts/Find-render-blocking-resources.js | 1 + ...reloaded-Loaded-and-used-above-the-fold.js | 12 ++- .../scripts/Inline-CSS-Info-and-Size.js | 5 ++ .../scripts/Webfont-Usage-Analyzer.js | 24 ++++- .../CSS-Media-Queries-Analysis.js | 4 + dist/webperf-loading/Content-Visibility.js | 5 +- .../webperf-loading/Critical-CSS-Detection.js | 6 ++ .../Find-Above-The-Fold-Lazy-Loaded-Images.js | 2 + ...Find-Images-With-Lazy-and-Fetchpriority.js | 1 + ...y-Loaded-Images-outside-of-the-viewport.js | 1 + .../Find-render-blocking-resources.js | 1 + ...reloaded-Loaded-and-used-above-the-fold.js | 12 ++- .../Inline-CSS-Info-and-Size.js | 5 ++ .../webperf-loading/Webfont-Usage-Analyzer.js | 24 ++++- public/llms-full.txt | 65 ++++++++------ .../references/schema.md | 88 +++++++++++++++++++ .../webperf-interaction/references/schema.md | 88 +++++++++++++++++++ skills/webperf-loading/references/schema.md | 88 +++++++++++++++++++ .../scripts/CSS-Media-Queries-Analysis.js | 4 +- .../scripts/Content-Visibility.js | 4 +- .../scripts/Critical-CSS-Detection.js | 4 +- .../Find-Above-The-Fold-Lazy-Loaded-Images.js | 4 +- ...Find-Images-With-Lazy-and-Fetchpriority.js | 4 +- ...y-Loaded-Images-outside-of-the-viewport.js | 4 +- .../scripts/Find-render-blocking-resources.js | 4 +- ...reloaded-Loaded-and-used-above-the-fold.js | 4 +- .../scripts/Inline-CSS-Info-and-Size.js | 4 +- .../scripts/Webfont-Usage-Analyzer.js | 4 +- skills/webperf-media/references/schema.md | 88 +++++++++++++++++++ skills/webperf-resources/references/schema.md | 88 +++++++++++++++++++ .../Loading/CSS-Media-Queries-Analysis.js | 6 +- snippets/Loading/Content-Visibility.js | 4 +- snippets/Loading/Critical-CSS-Detection.js | 6 ++ .../Find-Above-The-Fold-Lazy-Loaded-Images.js | 4 +- ...Find-Images-With-Lazy-and-Fetchpriority.js | 2 +- ...y-Loaded-Images-outside-of-the-viewport.js | 2 +- .../Loading/Find-render-blocking-resources.js | 2 +- ...reloaded-Loaded-and-used-above-the-fold.js | 6 +- snippets/Loading/Inline-CSS-Info-and-Size.js | 6 +- snippets/Loading/Webfont-Usage-Analyzer.js | 27 +++--- snippets/SCHEMA.md | 88 +++++++++++++++++++ 83 files changed, 1698 insertions(+), 147 deletions(-) diff --git a/.claude/skills/webperf-core-web-vitals/references/schema.md b/.claude/skills/webperf-core-web-vitals/references/schema.md index 975142c7..f53498ff 100644 --- a/.claude/skills/webperf-core-web-vitals/references/schema.md +++ b/.claude/skills/webperf-core-web-vitals/references/schema.md @@ -1232,6 +1232,94 @@ An issue about one thing among several, one issue per thing, also names it in `t | `could-defer` | info | Blocking scripts other than the last three of ``, which could try `defer` | | `unknown-size` | info | Scripts whose size is hidden by a missing `Timing-Allow-Origin`, so the total is a lower bound | +### CSS-Media-Queries-Analysis + +| Code | Severity | When | +|------|----------|------| +| `no-wide-rules` | info | No `@media` rules for viewports wider than the breakpoint (768 px by default) | +| `wide-rules` | info or warning | `@media` rules for wider viewports that narrower screens download without using, a warning above 50 KB | +| `repeated-queries` | info | Few distinct queries repeated across many `@media` blocks | +| `unreadable-stylesheets` | info | Cross-origin stylesheets whose rules cannot be read | + +### Content-Visibility + +| Code | Severity | When | +|------|----------|------| +| `missing-intrinsic-size` | warning | An element with `content-visibility: auto` and no `contain-intrinsic-size`; `target` is its selector | +| `not-used` | info | No `content-visibility` on the page | + +### Critical-CSS-Detection + +| Code | Severity | When | +|------|----------|------| +| `no-inline-critical-css` | error | Render-blocking stylesheets and no inline critical CSS | +| `render-blocking-over-budget` | error | Render-blocking CSS above the 14 KB critical budget | +| `large-not-preloaded` | warning | Render-blocking stylesheets above 10 KB without preload | +| `inline-over-budget` | error | Inline CSS above the 14 KB critical budget | +| `styles-in-body` | warning | Inline style blocks in `` | + +### Find-Above-The-Fold-Lazy-Loaded-Images + +| Code | Severity | When | +|------|----------|------| +| `lcp-lazy` | error | The LCP candidate image has `loading="lazy"` | +| `lazy-above-fold` | warning | Other images above the fold with `loading="lazy"` | + +### Find-Images-With-Lazy-and-Fetchpriority + +| Code | Severity | When | +|------|----------|------| +| `lazy-and-high-priority` | error | Elements with both `loading="lazy"` and `fetchpriority="high"` | + +### Find-non-Lazy-Loaded-Images-outside-of-the-viewport + +| Code | Severity | When | +|------|----------|------| +| `eager-off-viewport` | warning | Images outside the viewport without `loading="lazy"` | + +### Find-render-blocking-resources + +| Code | Severity | When | +|------|----------|------| +| `render-blocking` | error | Resources that block rendering | + +### Fonts-Preloaded-Loaded-and-used-above-the-fold + +Each issue names its font in `target`. + +| Code | Severity | When | +|------|----------|------| +| `preloaded-unused` | warning | A preloaded font that the content above the fold does not use | +| `used-not-preloaded` | warning | A font used above the fold without preload | +| `preload-no-crossorigin` | error | A font preload without `crossorigin`, which fetches the font twice | + +### Inline-CSS-Info-and-Size + +| Code | Severity | When | +|------|----------|------| +| `over-budget` | warning | Inline CSS above the 14 KB critical budget | +| `duplicate-blocks` | warning | Style blocks with identical content | +| `large-blocks` | warning | Style blocks above 5 KB | +| `styles-in-body` | warning | Style blocks in `` | + +### Webfont-Usage-Analyzer + +`load-failed`, `loaded-unused` and `redeclared` name the font file by its URL in `target`, or the face when it has none. + +| Code | Severity | When | +|------|----------|------| +| `load-failed` | error | A font that failed to load | +| `loaded-unused` | warning | A loaded font that no rendered element uses | +| `redeclared` | warning | A loaded font declared again later with the same descriptors, so the page uses the other file | +| `blocking-font-display` | warning | Faces with `font-display` `auto` or `block`, which can keep text invisible | +| `heavy-fonts` | warning | Web fonts above 300 KB in total | +| `never-loaded` | info | Faces declared and never loaded | +| `shadowed` | info | Faces declared more than once, whose earlier declarations never loaded | +| `unused-subsets` | info | `unicode-range` subsets the page did not need | +| `size-unknown` | info | Cross-origin font files without `Timing-Allow-Origin`, so the byte totals are a lower bound | +| `unreadable-stylesheets` | info | Cross-origin stylesheets whose font files cannot be matched to faces | +| `elements-capped` | info | Only the first 20,000 elements were scanned for text | + ### First-And-Third-Party-Script-Info | Code | Severity | When | diff --git a/.claude/skills/webperf-interaction/references/schema.md b/.claude/skills/webperf-interaction/references/schema.md index 975142c7..f53498ff 100644 --- a/.claude/skills/webperf-interaction/references/schema.md +++ b/.claude/skills/webperf-interaction/references/schema.md @@ -1232,6 +1232,94 @@ An issue about one thing among several, one issue per thing, also names it in `t | `could-defer` | info | Blocking scripts other than the last three of ``, which could try `defer` | | `unknown-size` | info | Scripts whose size is hidden by a missing `Timing-Allow-Origin`, so the total is a lower bound | +### CSS-Media-Queries-Analysis + +| Code | Severity | When | +|------|----------|------| +| `no-wide-rules` | info | No `@media` rules for viewports wider than the breakpoint (768 px by default) | +| `wide-rules` | info or warning | `@media` rules for wider viewports that narrower screens download without using, a warning above 50 KB | +| `repeated-queries` | info | Few distinct queries repeated across many `@media` blocks | +| `unreadable-stylesheets` | info | Cross-origin stylesheets whose rules cannot be read | + +### Content-Visibility + +| Code | Severity | When | +|------|----------|------| +| `missing-intrinsic-size` | warning | An element with `content-visibility: auto` and no `contain-intrinsic-size`; `target` is its selector | +| `not-used` | info | No `content-visibility` on the page | + +### Critical-CSS-Detection + +| Code | Severity | When | +|------|----------|------| +| `no-inline-critical-css` | error | Render-blocking stylesheets and no inline critical CSS | +| `render-blocking-over-budget` | error | Render-blocking CSS above the 14 KB critical budget | +| `large-not-preloaded` | warning | Render-blocking stylesheets above 10 KB without preload | +| `inline-over-budget` | error | Inline CSS above the 14 KB critical budget | +| `styles-in-body` | warning | Inline style blocks in `` | + +### Find-Above-The-Fold-Lazy-Loaded-Images + +| Code | Severity | When | +|------|----------|------| +| `lcp-lazy` | error | The LCP candidate image has `loading="lazy"` | +| `lazy-above-fold` | warning | Other images above the fold with `loading="lazy"` | + +### Find-Images-With-Lazy-and-Fetchpriority + +| Code | Severity | When | +|------|----------|------| +| `lazy-and-high-priority` | error | Elements with both `loading="lazy"` and `fetchpriority="high"` | + +### Find-non-Lazy-Loaded-Images-outside-of-the-viewport + +| Code | Severity | When | +|------|----------|------| +| `eager-off-viewport` | warning | Images outside the viewport without `loading="lazy"` | + +### Find-render-blocking-resources + +| Code | Severity | When | +|------|----------|------| +| `render-blocking` | error | Resources that block rendering | + +### Fonts-Preloaded-Loaded-and-used-above-the-fold + +Each issue names its font in `target`. + +| Code | Severity | When | +|------|----------|------| +| `preloaded-unused` | warning | A preloaded font that the content above the fold does not use | +| `used-not-preloaded` | warning | A font used above the fold without preload | +| `preload-no-crossorigin` | error | A font preload without `crossorigin`, which fetches the font twice | + +### Inline-CSS-Info-and-Size + +| Code | Severity | When | +|------|----------|------| +| `over-budget` | warning | Inline CSS above the 14 KB critical budget | +| `duplicate-blocks` | warning | Style blocks with identical content | +| `large-blocks` | warning | Style blocks above 5 KB | +| `styles-in-body` | warning | Style blocks in `` | + +### Webfont-Usage-Analyzer + +`load-failed`, `loaded-unused` and `redeclared` name the font file by its URL in `target`, or the face when it has none. + +| Code | Severity | When | +|------|----------|------| +| `load-failed` | error | A font that failed to load | +| `loaded-unused` | warning | A loaded font that no rendered element uses | +| `redeclared` | warning | A loaded font declared again later with the same descriptors, so the page uses the other file | +| `blocking-font-display` | warning | Faces with `font-display` `auto` or `block`, which can keep text invisible | +| `heavy-fonts` | warning | Web fonts above 300 KB in total | +| `never-loaded` | info | Faces declared and never loaded | +| `shadowed` | info | Faces declared more than once, whose earlier declarations never loaded | +| `unused-subsets` | info | `unicode-range` subsets the page did not need | +| `size-unknown` | info | Cross-origin font files without `Timing-Allow-Origin`, so the byte totals are a lower bound | +| `unreadable-stylesheets` | info | Cross-origin stylesheets whose font files cannot be matched to faces | +| `elements-capped` | info | Only the first 20,000 elements were scanned for text | + ### First-And-Third-Party-Script-Info | Code | Severity | When | diff --git a/.claude/skills/webperf-loading/references/schema.md b/.claude/skills/webperf-loading/references/schema.md index 975142c7..f53498ff 100644 --- a/.claude/skills/webperf-loading/references/schema.md +++ b/.claude/skills/webperf-loading/references/schema.md @@ -1232,6 +1232,94 @@ An issue about one thing among several, one issue per thing, also names it in `t | `could-defer` | info | Blocking scripts other than the last three of ``, which could try `defer` | | `unknown-size` | info | Scripts whose size is hidden by a missing `Timing-Allow-Origin`, so the total is a lower bound | +### CSS-Media-Queries-Analysis + +| Code | Severity | When | +|------|----------|------| +| `no-wide-rules` | info | No `@media` rules for viewports wider than the breakpoint (768 px by default) | +| `wide-rules` | info or warning | `@media` rules for wider viewports that narrower screens download without using, a warning above 50 KB | +| `repeated-queries` | info | Few distinct queries repeated across many `@media` blocks | +| `unreadable-stylesheets` | info | Cross-origin stylesheets whose rules cannot be read | + +### Content-Visibility + +| Code | Severity | When | +|------|----------|------| +| `missing-intrinsic-size` | warning | An element with `content-visibility: auto` and no `contain-intrinsic-size`; `target` is its selector | +| `not-used` | info | No `content-visibility` on the page | + +### Critical-CSS-Detection + +| Code | Severity | When | +|------|----------|------| +| `no-inline-critical-css` | error | Render-blocking stylesheets and no inline critical CSS | +| `render-blocking-over-budget` | error | Render-blocking CSS above the 14 KB critical budget | +| `large-not-preloaded` | warning | Render-blocking stylesheets above 10 KB without preload | +| `inline-over-budget` | error | Inline CSS above the 14 KB critical budget | +| `styles-in-body` | warning | Inline style blocks in `` | + +### Find-Above-The-Fold-Lazy-Loaded-Images + +| Code | Severity | When | +|------|----------|------| +| `lcp-lazy` | error | The LCP candidate image has `loading="lazy"` | +| `lazy-above-fold` | warning | Other images above the fold with `loading="lazy"` | + +### Find-Images-With-Lazy-and-Fetchpriority + +| Code | Severity | When | +|------|----------|------| +| `lazy-and-high-priority` | error | Elements with both `loading="lazy"` and `fetchpriority="high"` | + +### Find-non-Lazy-Loaded-Images-outside-of-the-viewport + +| Code | Severity | When | +|------|----------|------| +| `eager-off-viewport` | warning | Images outside the viewport without `loading="lazy"` | + +### Find-render-blocking-resources + +| Code | Severity | When | +|------|----------|------| +| `render-blocking` | error | Resources that block rendering | + +### Fonts-Preloaded-Loaded-and-used-above-the-fold + +Each issue names its font in `target`. + +| Code | Severity | When | +|------|----------|------| +| `preloaded-unused` | warning | A preloaded font that the content above the fold does not use | +| `used-not-preloaded` | warning | A font used above the fold without preload | +| `preload-no-crossorigin` | error | A font preload without `crossorigin`, which fetches the font twice | + +### Inline-CSS-Info-and-Size + +| Code | Severity | When | +|------|----------|------| +| `over-budget` | warning | Inline CSS above the 14 KB critical budget | +| `duplicate-blocks` | warning | Style blocks with identical content | +| `large-blocks` | warning | Style blocks above 5 KB | +| `styles-in-body` | warning | Style blocks in `` | + +### Webfont-Usage-Analyzer + +`load-failed`, `loaded-unused` and `redeclared` name the font file by its URL in `target`, or the face when it has none. + +| Code | Severity | When | +|------|----------|------| +| `load-failed` | error | A font that failed to load | +| `loaded-unused` | warning | A loaded font that no rendered element uses | +| `redeclared` | warning | A loaded font declared again later with the same descriptors, so the page uses the other file | +| `blocking-font-display` | warning | Faces with `font-display` `auto` or `block`, which can keep text invisible | +| `heavy-fonts` | warning | Web fonts above 300 KB in total | +| `never-loaded` | info | Faces declared and never loaded | +| `shadowed` | info | Faces declared more than once, whose earlier declarations never loaded | +| `unused-subsets` | info | `unicode-range` subsets the page did not need | +| `size-unknown` | info | Cross-origin font files without `Timing-Allow-Origin`, so the byte totals are a lower bound | +| `unreadable-stylesheets` | info | Cross-origin stylesheets whose font files cannot be matched to faces | +| `elements-capped` | info | Only the first 20,000 elements were scanned for text | + ### First-And-Third-Party-Script-Info | Code | Severity | When | diff --git a/.claude/skills/webperf-loading/scripts/CSS-Media-Queries-Analysis.js b/.claude/skills/webperf-loading/scripts/CSS-Media-Queries-Analysis.js index 458bf8a9..e7e35c67 100644 --- a/.claude/skills/webperf-loading/scripts/CSS-Media-Queries-Analysis.js +++ b/.claude/skills/webperf-loading/scripts/CSS-Media-Queries-Analysis.js @@ -1,2 +1,2 @@ -// snippets/Loading/CSS-Media-Queries-Analysis.js | sha256:69decca9b5432236 | https://github.com/nucliweb/webperf-snippets/blob/main/snippets/Loading/CSS-Media-Queries-Analysis.js -async function analyzeCSSMediaQueries(e=768){function s(e){try{const s=new URL(e,location.href);return/^https?:$/.test(s.protocol)?(s.hash="webperf-snippets",s.href):e}catch{return e}}const t=[...document.styleSheets],r=[];let n=0;function o(e){const s=e.match(/\.-?[_a-zA-Z][\w-]*/g)||[],t=e.match(/[a-z-]+\s*:/g)||[];return{classes:s.length,properties:t.length}}function i(s,t,n,i,a){var c;(function(s){if(!s)return!1;const t=s.match(/min-width:\s*(\d+)(px|em|rem)/i);if(t){const s=parseInt(t[1]),r=t[2].toLowerCase();if("px"===r&&s>e)return!0;if("em"===r&&s>e/16)return!0;if("rem"===r&&s>e/16)return!0}return!1})(n)&&r.push({source:s,inline:t,mediaQuery:n,...o(a),bytes:(c=i,new Blob([c]).size)})}function a(e,s){const t=/@media\s*([^{]+)\{((?:[^{}]|\{[^{}]*\})*)\}/g;let r;for(;null!==(r=t.exec(e));)i(s,!1,r[1].trim(),r[0],r[2])}for(let e=0;e tag #${e}`;try{const e=r.cssRules||r.rules;e&&[...e].forEach(e=>{if(e.type===CSSRule.MEDIA_RULE){const s=[...e.cssRules].map(e=>e.cssText).join("");i(c,o,e.media.mediaText,e.cssText,s)}})}catch{if(r.href)try{const e=await fetch(s(r.href));a(await e.text(),r.href)}catch{n++}}}const c=new Map;for(const e of r){const s=c.get(e.mediaQuery)??{mediaQuery:e.mediaQuery,rules:0,sources:new Set,classes:0,properties:0,bytes:0};s.rules++,s.sources.add(e.source),s.classes+=e.classes,s.properties+=e.properties,s.bytes+=e.bytes,c.set(e.mediaQuery,s)}const u=[...c.values()].map(({sources:e,...s})=>({mediaQuery:s.mediaQuery,rules:s.rules,files:e.size,classes:s.classes,properties:s.properties,bytes:s.bytes})).sort((e,s)=>s.bytes-e.bytes),l=new Map;for(const e of r){const s=l.get(e.source)??{source:e.source,inline:e.inline,rules:0,bytes:0};s.rules++,s.bytes+=e.bytes,l.set(e.source,s)}const h=[...l.values()],d=r.reduce((e,s)=>e+s.bytes,0),p=r.length?Math.round(u.length/r.length*1e3)/1e3:0,f=[];return 0===r.length?f.push({severity:"info",message:`No @media rules for viewports wider than ${e}px`}):(f.push({severity:d>51200?"warning":"info",message:`${r.length} @media rules for viewports wider than ${e}px add ${function(e){if(null==e||Number.isNaN(e))return"-";if(0===e)return"0 B";const s=["B","KB","MB","GB"],t=Math.max(0,Math.min(Math.floor(Math.log(e)/Math.log(1024)),s.length-1));return(e/Math.pow(1024,t)).toFixed(1)+" "+s[t]}(d)} of CSS that narrower screens download and parse without using. Move them to a stylesheet with a media attribute, such as , which narrower screens fetch at low priority without blocking rendering`}),r.length>=20&&p<.1&&f.push({severity:"info",message:`${u.length} distinct queries repeat across ${r.length} @media blocks, the pattern of component-scoped or compiled CSS. Grouping the rules of each query under one block shortens the stylesheets`})),n>0&&f.push({severity:"info",message:`${n} cross-origin stylesheet(s) could not be read, so their @media rules are not counted`}),f.forEach(e=>{}),{script:"CSS-Media-Queries-Analysis",status:"ok",count:u.length,details:{minWidth:e,rules:r.length,totalUnique:u.length,uniquenessRatio:p,classes:r.reduce((e,s)=>e+s.classes,0),properties:r.reduce((e,s)=>e+s.properties,0),bytes:d,bySource:h.slice(0,50),corsBlockedCount:n},items:u.slice(0,50),issues:f}}analyzeCSSMediaQueries(); \ No newline at end of file +// snippets/Loading/CSS-Media-Queries-Analysis.js | sha256:0b5fe535e72f38fc | https://github.com/nucliweb/webperf-snippets/blob/main/snippets/Loading/CSS-Media-Queries-Analysis.js +async function analyzeCSSMediaQueries(e=768){function s(e){try{const s=new URL(e,location.href);return/^https?:$/.test(s.protocol)?(s.hash="webperf-snippets",s.href):e}catch{return e}}const t=[...document.styleSheets],r=[];let n=0;function o(e){const s=e.match(/\.-?[_a-zA-Z][\w-]*/g)||[],t=e.match(/[a-z-]+\s*:/g)||[];return{classes:s.length,properties:t.length}}function i(s,t,n,i,a){var c;(function(s){if(!s)return!1;const t=s.match(/min-width:\s*(\d+)(px|em|rem)/i);if(t){const s=parseInt(t[1]),r=t[2].toLowerCase();if("px"===r&&s>e)return!0;if("em"===r&&s>e/16)return!0;if("rem"===r&&s>e/16)return!0}return!1})(n)&&r.push({source:s,inline:t,mediaQuery:n,...o(a),bytes:(c=i,new Blob([c]).size)})}function a(e,s){const t=/@media\s*([^{]+)\{((?:[^{}]|\{[^{}]*\})*)\}/g;let r;for(;null!==(r=t.exec(e));)i(s,!1,r[1].trim(),r[0],r[2])}for(let e=0;e tag #${e}`;try{const e=r.cssRules||r.rules;e&&[...e].forEach(e=>{if(e.type===CSSRule.MEDIA_RULE){const s=[...e.cssRules].map(e=>e.cssText).join("");i(c,o,e.media.mediaText,e.cssText,s)}})}catch{if(r.href)try{const e=await fetch(s(r.href));a(await e.text(),r.href)}catch{n++}}}const c=new Map;for(const e of r){const s=c.get(e.mediaQuery)??{mediaQuery:e.mediaQuery,rules:0,sources:new Set,classes:0,properties:0,bytes:0};s.rules++,s.sources.add(e.source),s.classes+=e.classes,s.properties+=e.properties,s.bytes+=e.bytes,c.set(e.mediaQuery,s)}const u=[...c.values()].map(({sources:e,...s})=>({mediaQuery:s.mediaQuery,rules:s.rules,files:e.size,classes:s.classes,properties:s.properties,bytes:s.bytes})).sort((e,s)=>s.bytes-e.bytes),l=new Map;for(const e of r){const s=l.get(e.source)??{source:e.source,inline:e.inline,rules:0,bytes:0};s.rules++,s.bytes+=e.bytes,l.set(e.source,s)}const h=[...l.values()],d=r.reduce((e,s)=>e+s.bytes,0),p=r.length?Math.round(u.length/r.length*1e3)/1e3:0,f=[];return 0===r.length?f.push({code:"no-wide-rules",severity:"info",message:`No @media rules for viewports wider than ${e}px`}):(f.push({code:"wide-rules",severity:d>51200?"warning":"info",message:`${r.length} @media rules for viewports wider than ${e}px add ${function(e){if(null==e||Number.isNaN(e))return"-";if(0===e)return"0 B";const s=["B","KB","MB","GB"],t=Math.max(0,Math.min(Math.floor(Math.log(e)/Math.log(1024)),s.length-1));return(e/Math.pow(1024,t)).toFixed(1)+" "+s[t]}(d)} of CSS that narrower screens download and parse without using. Move them to a stylesheet with a media attribute, such as , which narrower screens fetch at low priority without blocking rendering`}),r.length>=20&&p<.1&&f.push({code:"repeated-queries",severity:"info",message:`${u.length} distinct queries repeat across ${r.length} @media blocks, the pattern of component-scoped or compiled CSS. Grouping the rules of each query under one block shortens the stylesheets`})),n>0&&f.push({code:"unreadable-stylesheets",severity:"info",message:`${n} cross-origin stylesheet(s) could not be read, so their @media rules are not counted`}),f.forEach(e=>{}),{script:"CSS-Media-Queries-Analysis",status:"ok",count:u.length,details:{minWidth:e,rules:r.length,totalUnique:u.length,uniquenessRatio:p,classes:r.reduce((e,s)=>e+s.classes,0),properties:r.reduce((e,s)=>e+s.properties,0),bytes:d,bySource:h.slice(0,50),corsBlockedCount:n},items:u.slice(0,50),issues:f}}analyzeCSSMediaQueries(); \ No newline at end of file diff --git a/.claude/skills/webperf-loading/scripts/Content-Visibility.js b/.claude/skills/webperf-loading/scripts/Content-Visibility.js index 1bd48fa8..67f7940d 100644 --- a/.claude/skills/webperf-loading/scripts/Content-Visibility.js +++ b/.claude/skills/webperf-loading/scripts/Content-Visibility.js @@ -1,2 +1,2 @@ -// snippets/Loading/Content-Visibility.js | sha256:5c928b4610848da7 | https://github.com/nucliweb/webperf-snippets/blob/main/snippets/Loading/Content-Visibility.js -function detectContentVisibility(){const t={autoElements:[],hiddenElements:[],visibleElements:[],nodeArray:[]};function e(t){const e=t.nodeName;return 1===t.nodeType?e.toLowerCase():e.toUpperCase().replace(/^#/,"")}function n(t){let n="";try{for(;t&&9!==t.nodeType;){const i=t,s=i.id?"#"+i.id:e(i)+(i.classList&&i.classList.value&&i.classList.value.trim()&&i.classList.value.trim().length?"."+i.classList.value.trim().replace(/\s+/g,"."):"");if(n.length+s.length>99)return n||s;if(n=n?s+">"+n:s,i.id)break;t=i.parentNode}}catch{}return n}function i(t){const e=t.getBoundingClientRect();return e.top0&&e.left0}return function e(s){const o=window.getComputedStyle(s)["content-visibility"];if(o&&"visible"!==o){const e=function(t){const e=t.getBoundingClientRect(),s=window.getComputedStyle(t);return{selector:n(t),contentVisibility:s["content-visibility"],containIntrinsicSize:s["contain-intrinsic-size"]||"not set",width:Math.round(e.width),height:Math.round(e.height),top:Math.round(e.top+window.scrollY),inViewport:i(t)}}(s);"auto"===o?(t.autoElements.push(e),t.nodeArray.push(s)):"hidden"===o&&t.hiddenElements.push(e)}for(let t=0;t"not set"===t.containIntrinsicSize||"none"===t.containIntrinsicSize),t.nodeArray.forEach((t,e)=>{})),t}function analyzeContentVisibilityOpportunities(t={}){const{threshold:e=0,minHeight:n=100,minChildren:i=5}=t,s=window.innerHeight,o=[],l=new Set;function r(t){let e="";try{for(;t&&9!==t.nodeType;){const n=t,i=n.nodeName.toLowerCase(),s=n.id?"#"+n.id:i+(n.classList&&n.classList.value&&n.classList.value.trim()?"."+n.classList.value.trim().split(/\s+/).slice(0,2).join("."):"");if(e.length+s.length>80)return e||s;if(e=e?s+">"+e:s,n.id)break;t=n.parentNode}}catch{}return e}function c(t){const e=.2*t;return e<5?"Low (~"+e.toFixed(1)+"ms)":e<20?"Medium (~"+e.toFixed(1)+"ms)":"High (~"+e.toFixed(1)+"ms)"}function a(t){if(l.has(t)||function(t){let e=t.parentElement;for(;e;){if(l.has(e))return!0;e=e.parentElement}return!1}(t))return;const a=t.getBoundingClientRect(),d=window.getComputedStyle(t);if(d["content-visibility"]&&"visible"!==d["content-visibility"])return;if(a.heighte.childElements-t.childElements),0===o.length||(o.slice(0,20).map(t=>({selector:t.selector,height:t.height,distanceFromViewport:t.distanceFromViewport,childElements:t.childElements,estimatedSavings:t.estimatedSavings})),o.slice(0,10).forEach((t,e)=>{})),{opportunities:o.map(t=>({selector:t.selector,height:t.height,distanceFromViewport:t.distanceFromViewport,childElements:t.childElements,estimatedSavings:t.estimatedSavings})),totalElements:o.length,highImpact:o.filter(t=>t.estimatedSavings.startsWith("High")).length,elements:o.map(t=>t.element)}}window.analyzeContentVisibilityOpportunities=analyzeContentVisibilityOpportunities,(()=>{const t=detectContentVisibility();return{script:"Content-Visibility",status:"ok",count:t.autoElements.length+t.hiddenElements.length,details:{autoCount:t.autoElements.length,hiddenCount:t.hiddenElements.length},items:[...t.autoElements.map(t=>({...t,type:"auto"})),...t.hiddenElements.map(t=>({...t,type:"hidden"}))],issues:[...t.autoElements.filter(t=>"not set"===t.containIntrinsicSize||"none"===t.containIntrinsicSize).map(t=>({severity:"warning",message:`${t.selector}: missing contain-intrinsic-size (CLS risk)`})),...t.autoElements.length+t.hiddenElements.length===0?[{severity:"info",message:"No content-visibility usage found. Consider content-visibility: auto on below-the-fold sections to reduce the initial render cost."}]:[]]}})(); \ No newline at end of file +// snippets/Loading/Content-Visibility.js | sha256:d27a70bf806fe510 | https://github.com/nucliweb/webperf-snippets/blob/main/snippets/Loading/Content-Visibility.js +function detectContentVisibility(){const t={autoElements:[],hiddenElements:[],visibleElements:[],nodeArray:[]};function e(t){const e=t.nodeName;return 1===t.nodeType?e.toLowerCase():e.toUpperCase().replace(/^#/,"")}function n(t){let n="";try{for(;t&&9!==t.nodeType;){const i=t,s=i.id?"#"+i.id:e(i)+(i.classList&&i.classList.value&&i.classList.value.trim()&&i.classList.value.trim().length?"."+i.classList.value.trim().replace(/\s+/g,"."):"");if(n.length+s.length>99)return n||s;if(n=n?s+">"+n:s,i.id)break;t=i.parentNode}}catch{}return n}function i(t){const e=t.getBoundingClientRect();return e.top0&&e.left0}return function e(s){const o=window.getComputedStyle(s)["content-visibility"];if(o&&"visible"!==o){const e=function(t){const e=t.getBoundingClientRect(),s=window.getComputedStyle(t);return{selector:n(t),contentVisibility:s["content-visibility"],containIntrinsicSize:s["contain-intrinsic-size"]||"not set",width:Math.round(e.width),height:Math.round(e.height),top:Math.round(e.top+window.scrollY),inViewport:i(t)}}(s);"auto"===o?(t.autoElements.push(e),t.nodeArray.push(s)):"hidden"===o&&t.hiddenElements.push(e)}for(let t=0;t"not set"===t.containIntrinsicSize||"none"===t.containIntrinsicSize),t.nodeArray.forEach((t,e)=>{})),t}function analyzeContentVisibilityOpportunities(t={}){const{threshold:e=0,minHeight:n=100,minChildren:i=5}=t,s=window.innerHeight,o=[],l=new Set;function r(t){let e="";try{for(;t&&9!==t.nodeType;){const n=t,i=n.nodeName.toLowerCase(),s=n.id?"#"+n.id:i+(n.classList&&n.classList.value&&n.classList.value.trim()?"."+n.classList.value.trim().split(/\s+/).slice(0,2).join("."):"");if(e.length+s.length>80)return e||s;if(e=e?s+">"+e:s,n.id)break;t=n.parentNode}}catch{}return e}function c(t){const e=.2*t;return e<5?"Low (~"+e.toFixed(1)+"ms)":e<20?"Medium (~"+e.toFixed(1)+"ms)":"High (~"+e.toFixed(1)+"ms)"}function a(t){if(l.has(t)||function(t){let e=t.parentElement;for(;e;){if(l.has(e))return!0;e=e.parentElement}return!1}(t))return;const a=t.getBoundingClientRect(),d=window.getComputedStyle(t);if(d["content-visibility"]&&"visible"!==d["content-visibility"])return;if(a.heighte.childElements-t.childElements),0===o.length||(o.slice(0,20).map(t=>({selector:t.selector,height:t.height,distanceFromViewport:t.distanceFromViewport,childElements:t.childElements,estimatedSavings:t.estimatedSavings})),o.slice(0,10).forEach((t,e)=>{})),{opportunities:o.map(t=>({selector:t.selector,height:t.height,distanceFromViewport:t.distanceFromViewport,childElements:t.childElements,estimatedSavings:t.estimatedSavings})),totalElements:o.length,highImpact:o.filter(t=>t.estimatedSavings.startsWith("High")).length,elements:o.map(t=>t.element)}}window.analyzeContentVisibilityOpportunities=analyzeContentVisibilityOpportunities,(()=>{const t=detectContentVisibility();return{script:"Content-Visibility",status:"ok",count:t.autoElements.length+t.hiddenElements.length,details:{autoCount:t.autoElements.length,hiddenCount:t.hiddenElements.length},items:[...t.autoElements.map(t=>({...t,type:"auto"})),...t.hiddenElements.map(t=>({...t,type:"hidden"}))],issues:[...t.autoElements.filter(t=>"not set"===t.containIntrinsicSize||"none"===t.containIntrinsicSize).map(t=>({code:"missing-intrinsic-size",severity:"warning",message:`${t.selector}: missing contain-intrinsic-size (CLS risk)`,target:t.selector})),...t.autoElements.length+t.hiddenElements.length===0?[{code:"not-used",severity:"info",message:"No content-visibility usage found. Consider content-visibility: auto on below-the-fold sections to reduce the initial render cost."}]:[]]}})(); \ No newline at end of file diff --git a/.claude/skills/webperf-loading/scripts/Critical-CSS-Detection.js b/.claude/skills/webperf-loading/scripts/Critical-CSS-Detection.js index ea9dfb02..8403f2fd 100644 --- a/.claude/skills/webperf-loading/scripts/Critical-CSS-Detection.js +++ b/.claude/skills/webperf-loading/scripts/Critical-CSS-Detection.js @@ -1,2 +1,2 @@ -// snippets/Loading/Critical-CSS-Detection.js | sha256:02c1a8edc362c602 | https://github.com/nucliweb/webperf-snippets/blob/main/snippets/Loading/Critical-CSS-Detection.js -(()=>{function e(e){if(null==e||Number.isNaN(e))return"-";if(0===e)return"0 B";const t=["B","KB","MB","GB"],n=Math.max(0,Math.min(Math.floor(Math.log(e)/Math.log(1024)),t.length-1));return(e/Math.pow(1024,n)).toFixed(1)+" "+t[n]}const t=14336,n=Array.from(document.querySelectorAll('link[rel="stylesheet"]')),r=performance.getEntriesByType("resource").filter(e=>!e.name.endsWith("#webperf-snippets")),i=n.map(e=>{const t=e.href,n=e.getAttribute("media")||"all",i=r.find(e=>e.name===t);let s;s=i&&void 0!==i.renderBlockingStatus?"blocking"===i.renderBlockingStatus:"all"===n||""===n||"screen"===n||n.toLowerCase().includes("screen")&&!n.toLowerCase().includes("print");const l=i?i.transferSize:0,o=i?i.decodedBodySize:0,a=document.querySelector(`link[rel="preload"][as="style"][href="${t}"]`);let d=0,c=!1;try{const e=Array.from(document.styleSheets).find(e=>e.href===t);e&&e.cssRules&&(d=e.cssRules.length)}catch{c=!0}return{filename:t.split("/").pop().split("?")[0]||t,href:t,media:n,isRenderBlocking:s,transferSize:l,decodedSize:o,ruleCount:d,corsBlocked:c,preloaded:null!==a}}),s=Array.from(document.querySelectorAll("style")).map((e,t)=>{const n=e.innerHTML;return{index:t+1,size:new Blob([n]).size,ruleCount:(n.match(/\{[^}]*\}/g)||[]).length,inHead:null!==e.closest("head")}}),l=i.filter(e=>e.isRenderBlocking),o=i.filter(e=>!e.isRenderBlocking),a=s.reduce((e,t)=>e+t.size,0),d=l.reduce((e,t)=>e+t.decodedSize,0),c=i.reduce((e,t)=>e+t.transferSize,0);if(l.length>0){const e=Math.min(d/t*100,200),n=Math.min(Math.round(e/10),20);"█".repeat(n),"░".repeat(20-n)}s.length>0&&(a/t*100).toFixed(0);const g=[];l.length>0&&0===s.length&&g.push({severity:"high",message:"No critical CSS inlined: all CSS loads via render-blocking requests",suggestion:"Extract above-the-fold styles into a