Skip to content

network-priority/resource-hint-lint

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

resource-hint-lint

Lint the resource hints in your built HTML — catch broken preload, prefetch, preconnect, dns-prefetch, modulepreload and fetchpriority before they ship.

node dependencies license tests

resource-hint-lint crawls your built/static HTML, parses every <link> hint and fetchpriority attribute, and reports the mistakes that quietly waste bandwidth, open idle connections, or defeat the whole point of an early hint. It prints a grouped, colorized report with a 0–100 score and exits non-zero when errors are found, so it drops straight into CI.

Zero runtime dependencies. Pure Node (>=18). One small, well-tested HTML tokenizer, one rules engine, two reporters.


What it does

  • Parses each HTML file with a dependency-free tag/attribute scanner.
  • Runs 13 rules over the resource hints and fetchpriority usage it finds.
  • Groups findings by file, computes a score, and prints a summary.
  • Emits machine-readable JSON for pipelines with --json.
  • Exits 1 when any error-severity finding exists (or on warnings too with --strict).

Why hint mistakes hurt performance

Resource hints are a sharp tool: used well they shave real milliseconds off Largest Contentful Paint; used carelessly they do the opposite.

  • A preload with no as (or an invalid one) is ignored — you pay the bytes for nothing.
  • A font preload without crossorigin is fetched twice, because fonts always load in CORS mode.
  • An unused preload competes with the resources the browser actually needs right now.
  • Too many preconnect hints open idle sockets that fight for bandwidth.
  • The same URL marked both preload and prefetch sends the browser conflicting priorities.
  • Marking many images fetchpriority="high" dilutes prioritization and creates LCP contention.

For the background on getting these right, see the resource hint implementation overview.

Install

This is a standalone repository. It is not published to npm — clone it and run it directly.

git clone https://github.com/network-priority/resource-hint-lint.git
cd resource-hint-lint

# Run straight from the checkout (zero dependencies, nothing to install):
node bin/resource-hint-lint "dist/**/*.html"

To get a global resource-hint-lint command on your PATH, link it:

npm link
resource-hint-lint "dist/**/*.html"

Usage

resource-hint-lint <glob-or-paths...> [options]

Real examples:

# Lint an entire build directory (glob support is built in — no shell expansion needed).
resource-hint-lint "dist/**/*.html"

# A single file, plus JSON for a CI artifact.
resource-hint-lint public/index.html --json

# A directory (recursed for .html), with a tighter preconnect budget, failing on warnings too.
resource-hint-lint build --max-preconnect 4 --strict

# Exclude vendored output.
resource-hint-lint "build/**/*.html" --ignore "build/vendor/**" --ignore "build/legacy/**"

Options

Option Description
--json Machine-readable JSON output (for CI).
--quiet Only show error-severity findings.
--strict Exit non-zero on warnings too, not just errors.
--max-preconnect <n> Threshold for the preconnect-too-many rule (default 6).
--config <path> Path to a JSON config file (see below).
--ignore <glob> Glob to exclude; repeatable.
--no-color Disable ANSI colors (also honors the NO_COLOR env var).
--list-rules Print every rule id and default severity, then exit.
--version Print the version and exit.
--help Show help and exit.

Exit codes: 0 clean (warnings allowed unless --strict), 1 errors present, 2 usage/config error.

Rule reference

Rule id Severity What it catches Why it matters
preload-missing-as error <link rel=preload> with no as Without as the browser can't set priority or request headers, and may double-fetch.
preload-invalid-as error as outside the allowed set An unrecognized as value is ignored, so the preload is wasted.
preload-font-missing-crossorigin error as=font preload without crossorigin Fonts load in CORS mode; without it the preload is discarded and the font is fetched twice.
preload-missing-type warn as=font/as=image preload without type A type lets the browser skip preloads for formats it can't use.
unused-preload warn A preloaded URL never referenced in the doc An unused preload wastes bandwidth and delays other downloads.
preconnect-unused warn A preconnect origin no subresource uses An unused preconnect holds an idle connection open for nothing.
preconnect-too-many warn More than --max-preconnect connection hints Too many early connections cause bandwidth/connection contention.
preconnect-missing-crossorigin-font warn preconnect to a known font origin without crossorigin Font files need a CORS connection; otherwise a second connection is opened anyway.
prefetch-same-as-preload warn Same URL both preloaded and prefetched preload (this navigation) and prefetch (the next) send conflicting priority signals.
duplicate-hint warn Identical rel+href+as declared twice Duplicate hints add parser work and clutter with no benefit.
fetchpriority-invalid error fetchpriority outside {high, low, auto} An invalid value is ignored, so the intended prioritization never happens.
preload-not-in-head warn A preload/preconnect in <body> Hints in the body are discovered late, defeating the point of an early hint.
multiple-fetchpriority-high-images warn More than N images with fetchpriority=high Marking many images high dilutes prioritization and creates LCP contention.

Deep dives on the rules above:

Config file

Pass a JSON file with --config. Everything is optional; provided keys override the defaults. Rules can be turned off ("off" or false) or have their severity changed ("warn" / "error").

{
  "maxPreconnect": 4,
  "maxHighPriorityImages": 1,
  "fontOrigins": ["https://fonts.example.com"],
  "rules": {
    "unused-preload": "off",
    "preload-missing-type": "error",
    "duplicate-hint": "warn"
  }
}
  • maxPreconnect — threshold for preconnect-too-many (CLI --max-preconnect wins over this).
  • maxHighPriorityImages — threshold for multiple-fetchpriority-high-images.
  • fontOrigins — extra origins treated as font hosts (merged with the built-in list) for the font crossorigin checks.
  • rules — per-rule severity overrides.

CI / GitHub Action usage

Because the linter exits non-zero on errors, wiring it into CI is a one-liner. Add a step to your workflow that runs it against your build output:

# .github/workflows/perf-hints.yml
name: Resource hints

on: [push, pull_request]

jobs:
  hint-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20

      # Build your site first (adjust to your toolchain):
      # - run: npm ci && npm run build

      - name: Clone resource-hint-lint
        run: git clone --depth 1 https://github.com/network-priority/resource-hint-lint.git .tools/rhl

      - name: Audit resource hints
        run: node .tools/rhl/bin/resource-hint-lint "dist/**/*.html" --strict

This repository's own .github/workflows/ci.yml runs the test suite on Node 18, 20 and 22.

Output examples

Pretty (terminal) output for a page with problems:

examples/messy.html
     8: error  <link rel="preload" href="/css/late.css"> is missing an "as" attribute. preload-missing-as
    11: error  Invalid as="styles" on preload (allowed: style, script, image, font, ...). preload-invalid-as
    14: error  preload as="font" href="/fonts/inter.woff2" is missing "crossorigin" ... preload-font-missing-crossorigin
    39: warn   7 preconnect/dns-prefetch hints exceed the limit of 6. preconnect-too-many
    51: warn   3 images use fetchpriority="high" (limit 2); this dilutes prioritization. multiple-fetchpriority-high-images
  5 error(s), 15 warning(s) — score 0/100

Summary
  files: 1   errors: 5   warnings: 15
  overall score: 0/100

JSON output (--json):

{
  "tool": "resource-hint-lint",
  "summary": { "files": 1, "errors": 5, "warnings": 15, "score": 0 },
  "results": [
    {
      "file": "examples/messy.html",
      "score": 0,
      "errors": 5,
      "warnings": 15,
      "findings": [
        {
          "rule": "preload-missing-as",
          "severity": "error",
          "message": "<link rel=\"preload\" href=\"/css/late.css\"> is missing an \"as\" attribute.",
          "line": 8
        }
      ]
    }
  ]
}

Try it yourself against the bundled fixtures in examples/:

node bin/resource-hint-lint examples/clean.html   # exits 0, score 100/100
node bin/resource-hint-lint examples/messy.html   # exits 1, nearly every rule fires

How it works

Three small pieces, all dependency-free:

  1. The parser (src/parser.js) is a tag/attribute tokenizer — not a full DOM. It walks the HTML once, extracting <link>, <script>, <img>, <source>, <a>, <video> and <audio> tags with their attributes. It handles single/ double/unquoted attribute values, boolean attributes, self-closing tags and case-insensitivity, skips comments and the raw text inside <script>/<style>, and tracks whether each tag lives in <head> or <body> (plus a line number).

  2. The rules engine (src/rules.js) collects the hints and the subresources the document actually references, then runs each enabled rule. Cross-cutting rules (unused preloads, unused preconnects, prefetch-vs-preload conflicts) work by comparing the hinted URLs/origins against the referenced ones. Severities come from each rule's default, overridable per-rule via config.

  3. The reporters (src/reporters.js) render either the colorized terminal report (respecting NO_COLOR / --no-color) or JSON, and compute the score (errors weigh more than warnings, clamped to 0–100).

Globbing and file discovery (src/glob.js) are hand-rolled too: a recursive directory walker plus a minimatch-lite matcher supporting **, *, ? and character classes.

Further reading

If you want to go deeper than the linter's messages, these guides cover the "why" behind each rule:

Contributing

Issues and pull requests are welcome. To work on the tool:

git clone https://github.com/network-priority/resource-hint-lint.git
cd resource-hint-lint
npm test        # runs node --test

Adding a rule is small and self-contained: register its metadata in RULES (src/rules.js), emit findings from runRules, and add a fixture-backed test under test/. Please keep the project dependency-free.

License

MIT © 2026 Network Priority.


Built by the team behind network-priority.com.

About

Lint browser resource hints (preload/prefetch/preconnect) and fetchpriority in built HTML — scored, CI-ready, zero dependencies.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors