Skip to content

feat(validate): add performance validation rules#691

Merged
harlan-zw merged 1 commit intomainfrom
feat/validate-perf-rules
Mar 11, 2026
Merged

feat(validate): add performance validation rules#691
harlan-zw merged 1 commit intomainfrom
feat/validate-perf-rules

Conversation

@harlan-zw
Copy link
Collaborator

@harlan-zw harlan-zw commented Mar 11, 2026

🔗 Linked issue

N/A

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking change

📚 Description

Adds 8 performance validation rules to the ValidatePlugin, inspired by webperf-snippets. These catch common performance anti-patterns in head tags:

  • preload-fetchpriority-conflict — contradictory preload + low fetchpriority
  • too-many-preloads — more than 6 preloads competing for bandwidth
  • too-many-preconnects — more than 4 preconnects saturating connections
  • redundant-dns-prefetch — dns-prefetch redundant when preconnect exists
  • preload-async-defer-conflict — preloaded script with async/defer
  • prefetch-preload-conflict — same resource both preloaded and prefetched
  • inline-style-size — inline style exceeds 14KB critical CSS budget
  • inline-script-size — inline script exceeds 2KB cacheability threshold

Also introduces ESLint-style type-safe rule configuration. Rules with options accept [severity, options] tuples with per-rule typed options:

ValidatePlugin({
  rules: {
    'too-many-preloads': ['warn', { max: 10 }],
    'inline-style-size': ['info', { maxKB: 20 }],
    'missing-description': 'off',
  }
})

Summary by CodeRabbit

  • New Features

    • 8 new performance validation rules detecting oversized inline styles and scripts, excessive resource counts, and conflicting performance tags.
    • Enhanced rule configuration supporting per-rule options for customizable size limits and resource thresholds.
  • Documentation

    • Added comprehensive "Performance Hints" documentation explaining all 9 validation rules with configuration examples and per-rule option settings.

…nfig

Add 8 performance hint rules inspired by webperf-snippets: preload-fetchpriority-conflict,
too-many-preloads, too-many-preconnects, redundant-dns-prefetch, preload-async-defer-conflict,
prefetch-preload-conflict, inline-style-size, inline-script-size.

Add type-safe ESLint-style rule configuration with per-rule options support via
discriminated union types (e.g. `['warn', { max: 10 }]` tuples).
@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

📝 Walkthrough

Walkthrough

This PR extends the head validation plugin with eight new performance-related validation rules, adds type-safe per-rule configuration options, introduces helper functions to resolve configurations, and includes comprehensive test coverage for all new rules.

Changes

Cohort / File(s) Summary
Type System & Exports
packages/unhead/src/plugins/validate.ts, packages/unhead/src/plugins/index.ts
Introduces ValidationRuleOptions interface defining per-rule option shapes (e.g., maxKB for inline-script-size/inline-style-size, max for preload/preconnect thresholds). Adds generic RuleConfig<Id> and RulesConfig types enabling ESLint-style configurations `RuleSeverity
Validation Rules & Runtime Logic
packages/unhead/src/plugins/validate.ts
Extends ValidationRuleId with eight new rules: inline-script-size, inline-style-size, prefetch-preload-conflict, preload-async-defer-conflict, preload-fetchpriority-conflict, redundant-dns-prefetch, too-many-preconnects, too-many-preloads. Implements corresponding runtime checks with configurable thresholds. Adds resolveSeverity and resolveOptions helper functions to parse ESLint-style configurations.
Test Coverage
packages/unhead/test/unit/plugins/validate.test.ts
Adds comprehensive "performance hints" test suite covering all eight new rules with various configuration options, threshold behaviors, edge cases, and custom max/maxKB settings.
Documentation
docs/head/1.guides/plugins/validate.md
Adds "Performance Hints" section documenting nine validation rules with IDs, severities, and descriptions. Includes ESLint-style configuration examples demonstrating how to pass severity and per-rule options. (Configuration examples appear in two locations within the document.)

Sequence Diagram

sequenceDiagram
    participant Config as Configuration Input
    participant Resolver as Option Resolver
    participant Checks as Validation Checks
    participant Report as Report Output

    Config->>Resolver: RulesConfig with [Severity, Options]?
    Resolver->>Resolver: resolveSeverity()
    Resolver->>Resolver: resolveOptions() with defaults
    Resolver->>Checks: Severity + Resolved Options
    Checks->>Checks: Execute check with options<br/>(e.g., maxKB, max threshold)
    alt Rule Violation Detected
        Checks->>Report: Report with severity
    else Rule Passes
        Checks->>Report: No report
    end
    Report->>Report: Return validation results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Eight new rules hop into the fold,
With options that flow and thresholds to hold,
Performance hints whisper what's right,
ESLint-style configs, type-safe and bright,
Hop-skip-jump — validation takes flight! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The description covers all required sections from the template, including linked issue (N/A), type of change (New feature selected), and a comprehensive description of the eight performance rules with configuration examples.
Title check ✅ Passed The pull request title 'feat(validate): add performance validation rules' clearly and concisely summarizes the main change: adding eight new performance-related validation rules to the ValidatePlugin.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/validate-perf-rules

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

Bundle Size Analysis

Bundle Size Gzipped
Client (Minimal) 10.5 kB 4.3 kB
Server (Minimal) 10.2 kB 4.2 kB
Vue Client (Minimal) 11.4 kB 4.7 kB
Vue Server (Minimal) 11.1 kB 4.6 kB

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/head/1.guides/plugins/validate.md (1)

62-64: ⚠️ Potential issue | 🟡 Minor

Documentation shows outdated type signature.

The interface definition shows rules?: Partial<Record<string, 'warn' | 'info' | 'off'>> but the actual implementation uses RulesConfig which supports the ESLint-style [severity, options] tuple for rules with configurable options.

📝 Suggested fix to align with implementation
   /**
-   * Configure rule severity. Set to 'off' to disable, or 'warn'/'info' to override.
+   * Configure rule severity and options. Accepts a severity string or an ESLint-style
+   * `[severity, options]` tuple for rules that support configuration.
    */
-  rules?: Partial<Record<string, 'warn' | 'info' | 'off'>>
+  rules?: RulesConfig
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/head/1.guides/plugins/validate.md` around lines 62 - 64, The docs show
an outdated type for the rules property; update the documentation to reflect the
actual implementation by replacing the current signature "rules?:
Partial<Record<string, 'warn' | 'info' | 'off'>>" with the correct type that
uses RulesConfig (i.e., allow ESLint-style rule entries like [severity, options]
and tuples), and mention that the configuration accepts severity strings or
[severity, options] tuples consistent with the RulesConfig used in the codebase.
🧹 Nitpick comments (1)
packages/unhead/src/plugins/validate.ts (1)

502-516: Consider normalizing origin URLs for comparison.

The redundant dns-prefetch check uses exact string matching. Origins with subtle differences (e.g., trailing slash, different casing) won't match. This could lead to false negatives in edge cases.

🔧 Optional: Normalize origins before comparison
+function normalizeOrigin(href: string): string {
+  try {
+    const url = new URL(href)
+    return url.origin
+  } catch {
+    return href.toLowerCase().replace(/\/$/, '')
+  }
+}
+
 // Redundant dns-prefetch when preconnect exists for same origin
 const preconnectOrigins = new Set<string>()
 const dnsPrefetchTags: HeadTag[] = []
 for (const tag of tags) {
   if (tag.tag === 'link' && tag.props.href) {
     if (tag.props.rel === 'preconnect')
-      preconnectOrigins.add(tag.props.href)
+      preconnectOrigins.add(normalizeOrigin(tag.props.href))
     else if (tag.props.rel === 'dns-prefetch')
       dnsPrefetchTags.push(tag)
   }
 }
 for (const tag of dnsPrefetchTags) {
-  if (preconnectOrigins.has(tag.props.href))
+  if (preconnectOrigins.has(normalizeOrigin(tag.props.href)))
     report('redundant-dns-prefetch', `dns-prefetch for "${tag.props.href}" is redundant — preconnect already includes DNS resolution.`, 'info', tag)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/unhead/src/plugins/validate.ts` around lines 502 - 516, The
redundancy check currently compares link href strings verbatim (using
preconnectOrigins.has(tag.props.href)), which misses semantically equal origins
(e.g., differing case or trailing slashes); update the logic in the validate
plugin around the preconnectOrigins set and dnsPrefetchTags loop to normalize
origins before storing/checking: for each link href (in the block that populates
preconnectOrigins and later when iterating dnsPrefetchTags) compute a canonical
origin (e.g., use the URL constructor to extract URL.origin or otherwise
lower-case and trim trailing slashes, with a try/catch for invalid/relative
hrefs and a sensible fallback) and use that normalized origin as the Set key and
for the has() check so report('redundant-dns-prefetch', ...) fires correctly for
equivalent origins.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@docs/head/1.guides/plugins/validate.md`:
- Around line 62-64: The docs show an outdated type for the rules property;
update the documentation to reflect the actual implementation by replacing the
current signature "rules?: Partial<Record<string, 'warn' | 'info' | 'off'>>"
with the correct type that uses RulesConfig (i.e., allow ESLint-style rule
entries like [severity, options] and tuples), and mention that the configuration
accepts severity strings or [severity, options] tuples consistent with the
RulesConfig used in the codebase.

---

Nitpick comments:
In `@packages/unhead/src/plugins/validate.ts`:
- Around line 502-516: The redundancy check currently compares link href strings
verbatim (using preconnectOrigins.has(tag.props.href)), which misses
semantically equal origins (e.g., differing case or trailing slashes); update
the logic in the validate plugin around the preconnectOrigins set and
dnsPrefetchTags loop to normalize origins before storing/checking: for each link
href (in the block that populates preconnectOrigins and later when iterating
dnsPrefetchTags) compute a canonical origin (e.g., use the URL constructor to
extract URL.origin or otherwise lower-case and trim trailing slashes, with a
try/catch for invalid/relative hrefs and a sensible fallback) and use that
normalized origin as the Set key and for the has() check so
report('redundant-dns-prefetch', ...) fires correctly for equivalent origins.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 51ebfff3-5f6b-4357-af10-1363f0bbf582

📥 Commits

Reviewing files that changed from the base of the PR and between 3fa0cc5 and 8b3feac.

📒 Files selected for processing (4)
  • docs/head/1.guides/plugins/validate.md
  • packages/unhead/src/plugins/index.ts
  • packages/unhead/src/plugins/validate.ts
  • packages/unhead/test/unit/plugins/validate.test.ts

@harlan-zw harlan-zw changed the title feat(validate): add performance validation rules with ESLint-style config feat(validate): add performance validation rules Mar 11, 2026
@harlan-zw harlan-zw merged commit 0abb6a7 into main Mar 11, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant