Skip to content

Commit 49db83a

Browse files
Add priority-hints-codemod: apply fetchpriority and resource hints via codemod
Adds fetchpriority/priority, lazy loading, and head hints (preload/preconnect) to hero and below-the-fold images across HTML, Next.js, Nuxt, and Astro, with correct per-framework attribute casing. Dry-run by default, idempotent, backups, respects author intent. Zero dependencies, 37 tests.
0 parents  commit 49db83a

35 files changed

Lines changed: 2444 additions & 0 deletions

‎.github/workflows/ci.yml‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
node-version: [18, 20, 22]
15+
steps:
16+
- uses: actions/checkout@v4
17+
- name: Use Node.js ${{ matrix.node-version }}
18+
uses: actions/setup-node@v4
19+
with:
20+
node-version: ${{ matrix.node-version }}
21+
- name: Run tests (zero dependencies, nothing to install)
22+
run: node --test

‎.gitignore‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
node_modules/
2+
*.bak
3+
*.log
4+
.DS_Store
5+
coverage/
6+
.idea/
7+
.vscode/

‎LICENSE‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Network Priority
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎README.md‎

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
# priority-hints-codemod
2+
3+
A dependency-free Node.js codemod that scans your framework templates and
4+
applies **resource-priority best practices** automatically — the same hints
5+
that move your [Largest Contentful Paint](https://www.network-priority.com/core-browser-loading-mechanics-priority-queues/fetchpriority-attribute-priority-hints/)
6+
image to the front of the browser's fetch queue and let the rest of the page
7+
load lazily.
8+
9+
It edits files in place (with a dry-run diff preview and optional backups) to:
10+
11+
- Add `fetchpriority="high"` to the likely LCP / above-the-fold hero image
12+
(and the Next.js `<Image priority>` / Nuxt / Astro equivalents).
13+
- Add `loading="lazy"` and `decoding="async"` to non-critical images, plus
14+
`fetchpriority="low"` to clearly below-the-fold images.
15+
- Add `<link rel="preload">` for the hero image where a template exposes a
16+
document `<head>`.
17+
- Add `<link rel="preconnect">` for detected third-party font origins
18+
(Google Fonts).
19+
- For Next.js: set `priority` on the hero `next/image`, and annotate
20+
`next/script` tags that are missing a `strategy`.
21+
22+
Zero runtime dependencies. Pure Node (>= 18).
23+
24+
## What it does
25+
26+
Modern browsers assign every resource a fetch priority. Left to the defaults,
27+
the hero image often loads *after* less important assets, hurting LCP, while
28+
below-the-fold images compete for bandwidth they don't need yet. The right
29+
[Priority Hints](https://www.network-priority.com/core-browser-loading-mechanics-priority-queues/fetchpriority-attribute-priority-hints/)
30+
fix this — but applying them by hand across a large codebase is tedious and
31+
error-prone. This codemod does it mechanically and consistently, using the
32+
heuristics described [below](#the-heuristics-explained), and always shows you a
33+
diff first.
34+
35+
## Safety model
36+
37+
This tool is designed to never surprise you:
38+
39+
- **Dry-run by default.** Without `--write`, it changes nothing — it prints a
40+
unified diff and a summary so you can review every edit.
41+
- **Backups.** Pass `--backup` to write a `.bak` copy of each file before it is
42+
modified.
43+
- **Idempotent.** Running it twice produces no further changes. It detects the
44+
hints it has already added and leaves them alone.
45+
- **Respects author intent.** If an image already has an explicit
46+
`fetchpriority`, `loading`, or `decoding`, the codemod does not overwrite it
47+
unless you pass `--force`. Contradictions it can't silently resolve (for
48+
example `loading="lazy"` on a hero it is marking high priority) are surfaced
49+
as warnings.
50+
- **Formatting-preserving.** A custom tag tokenizer rewrites only the attributes
51+
it needs to; your quote style, whitespace, and surrounding code are left
52+
untouched. Everything it does not intend to change is preserved byte-for-byte.
53+
54+
## Install
55+
56+
This is a standalone repository, **not published to npm**. Clone it and run the
57+
bin directly, or link it globally:
58+
59+
```sh
60+
git clone https://github.com/network-priority/priority-hints-codemod.git
61+
cd priority-hints-codemod
62+
63+
# run it directly
64+
node bin/priority-hints-codemod --help
65+
66+
# or link it onto your PATH as `priority-hints-codemod`
67+
npm link
68+
```
69+
70+
There is nothing to `npm install` — the tool has zero dependencies.
71+
72+
## Usage & options
73+
74+
```sh
75+
priority-hints-codemod <paths/globs...> [options]
76+
```
77+
78+
| Option | Description |
79+
| --- | --- |
80+
| `--dry-run` | Preview a unified diff, change nothing (**this is the default**). |
81+
| `--write` | Apply the changes to disk. |
82+
| `--framework <name>` | `auto` \| `html` \| `next` \| `nuxt` \| `astro` (default `auto`). |
83+
| `--max-hero <n>` | Max hero images to mark high priority per file (default `1`). |
84+
| `--no-lazy` | Do not add `loading="lazy"` to below-the-fold images. |
85+
| `--no-annotate` | Do not insert explanatory code comments. |
86+
| `--backup` | Write a `.bak` copy before modifying each file. |
87+
| `--force` | Overwrite existing priority attributes (author intent is respected otherwise). |
88+
| `--ignore <glob>` | Ignore matching files (repeatable). |
89+
| `--json` | Emit a machine-readable JSON report. |
90+
| `--no-color` | Disable ANSI colors. |
91+
| `--version` | Print the version. |
92+
| `--help` | Print help. |
93+
94+
The tool ships its own glob matcher and recursive walker (no dependencies)
95+
supporting `**`, `*`, and `?`. Pass files, directories (walked recursively for
96+
known extensions), or glob patterns.
97+
98+
```sh
99+
# Preview across a Next.js app
100+
priority-hints-codemod "src/**/*.{jsx,tsx}"
101+
102+
# Apply to one HTML file
103+
priority-hints-codemod public/index.html --write
104+
105+
# A whole Nuxt components directory, with backups
106+
priority-hints-codemod components --framework nuxt --write --backup
107+
108+
# Ignore vendored and minified files
109+
priority-hints-codemod src --ignore "**/vendor/**" --ignore "*.min.*"
110+
```
111+
112+
Exit code is `0` on success and non-zero on a usage or runtime error.
113+
114+
## Framework support matrix
115+
116+
Each framework needs the *correct* attribute casing and mechanism —
117+
[getting these details right](https://www.network-priority.com/framework-specific-loading-strategies/)
118+
is most of the value here. React uses camelCase `fetchPriority`; HTML, Vue and
119+
Astro use lowercase `fetchpriority`; `loading` and `decoding` are lowercase
120+
everywhere; and `next/image` uses a `priority` boolean prop instead of
121+
`fetchpriority` altogether.
122+
123+
| Framework | Image tag(s) handled | High-priority mechanism | Lazy mechanism | Attribute casing |
124+
| --- | --- | --- | --- | --- |
125+
| HTML (`.html`) | `<img>` | `fetchpriority="high"` + `<link rel="preload">` | `loading="lazy"` `decoding="async"` | lowercase |
126+
| Next.js (`.jsx` / `.tsx`) | `next/image` `<Image>`, `<img>`, `next/script` | `<Image priority>` (component); `fetchPriority="high"` (`<img>`) | `next/image` is lazy by default; `<img>` gets `loading="lazy"` | React camelCase `fetchPriority`; `loading`/`decoding` lowercase |
127+
| Nuxt / Vue (`.vue`) | `<NuxtImg>` / `<nuxt-img>`, `<img>` | `fetchpriority="high"` | `loading="lazy"` `decoding="async"` | lowercase |
128+
| Astro (`.astro`) | `astro:assets` `<Image />`, `<img>` | `fetchpriority="high"` + `loading="eager"` (overrides Astro's lazy default) | `loading="lazy"` `decoding="async"` | lowercase |
129+
130+
Framework-specific deep dives:
131+
132+
- [Fixing Next.js LCP image priority](https://www.network-priority.com/framework-specific-loading-strategies/nextjs-resource-loading-optimization/fixing-nextjs-lcp-image-priority/)
133+
- [Choosing a next/script strategy](https://www.network-priority.com/framework-specific-loading-strategies/nextjs-resource-loading-optimization/choosing-nextjs-script-strategy/)
134+
- [Tuning NuxtLink prefetch behavior](https://www.network-priority.com/framework-specific-loading-strategies/nuxt-resource-loading-optimization/tuning-nuxt-link-prefetch-behavior/)
135+
- [Sequencing Astro client directives](https://www.network-priority.com/framework-specific-loading-strategies/astro-islands-loading-optimization/sequencing-astro-client-directives/)
136+
137+
## The heuristics explained
138+
139+
Because full framework AST parsing is heavy, the codemod uses robust,
140+
well-tested **tag-level transforms** driven by these heuristics:
141+
142+
- **Above the fold / hero.** An image qualifies as the hero if it is the first
143+
significant image in the file, **or** its `class` / `id` / `alt` / `src` (or
144+
its nearest un-closed section) matches
145+
`hero|banner|masthead|cover|jumbotron|splash`. The first qualifying image
146+
(up to `--max-hero`, default `1`) is marked high priority. If a hero also has
147+
`loading="lazy"`, that is contradictory — the codemod removes the lazy hint
148+
and **flags it** as a warning.
149+
- **Below the fold.** Images whose class matches
150+
`thumb|thumbnail|avatar|icon|sprite|badge|footer`, or which live inside a
151+
`<footer>` / `<aside>`, get `loading="lazy"`, `decoding="async"` and
152+
`fetchpriority="low"`.
153+
- **Neutral.** Any other non-hero image (typically further down the page) gets
154+
`loading="lazy"` and `decoding="async"`, but no `fetchpriority` — the tool
155+
won't demote what it can't prove is off-screen.
156+
- **One hero per file.** It never marks more than `--max-hero` images high
157+
priority (default `1`).
158+
- **Idempotent and intent-respecting.** Existing explicit attributes are left
159+
alone unless you pass `--force`, and a second run makes no changes.
160+
161+
`<head>` injection (preload + preconnect) only happens when a literal `<head>`
162+
is present, and is skipped for Next.js — there, `next/image` and the metadata
163+
API own the document head.
164+
165+
## Examples
166+
167+
Full before/after files for every framework live in [`examples/`](./examples).
168+
A few highlights:
169+
170+
### HTML — before
171+
172+
```html
173+
<header class="hero">
174+
<img src="/img/hero.jpg" alt="A sweeping landscape" width="1600" height="900" />
175+
</header>
176+
<footer>
177+
<img class="avatar" src="/img/author.png" alt="Author portrait" width="64" height="64" />
178+
</footer>
179+
```
180+
181+
### HTML — after
182+
183+
```html
184+
<!-- (in <head>) -->
185+
<link rel="preload" as="image" href="/img/hero.jpg" fetchpriority="high">
186+
187+
<header class="hero">
188+
<!-- priority hint added by priority-hints-codemod — see network-priority.com -->
189+
<img src="/img/hero.jpg" alt="A sweeping landscape" width="1600" height="900" fetchpriority="high" decoding="async" />
190+
</header>
191+
<footer>
192+
<img class="avatar" src="/img/author.png" alt="Author portrait" width="64" height="64" loading="lazy" decoding="async" fetchpriority="low" />
193+
</footer>
194+
```
195+
196+
### Next.js — before / after
197+
198+
```jsx
199+
// before
200+
<Image src={heroPic} alt="Product hero" width={1280} height={720} loading="lazy" />
201+
202+
// after — priority prop, contradictory lazy removed
203+
<Image src={heroPic} alt="Product hero" width={1280} height={720} priority />
204+
```
205+
206+
### Nuxt — before / after
207+
208+
```html
209+
<!-- before -->
210+
<NuxtImg src="/img/hero.jpg" alt="Marketing hero" width="1600" height="900" />
211+
212+
<!-- after -->
213+
<NuxtImg src="/img/hero.jpg" alt="Marketing hero" width="1600" height="900" fetchpriority="high" decoding="async" />
214+
```
215+
216+
### Astro — before / after
217+
218+
```jsx
219+
// before
220+
<Image src={heroImage} alt="Hero illustration" width={1440} height={720} />
221+
222+
// after — fetchpriority plus explicit eager to beat Astro's lazy default
223+
<Image src={heroImage} alt="Hero illustration" width={1440} height={720} fetchpriority="high" loading="eager" decoding="async" />
224+
```
225+
226+
## What it will NOT do
227+
228+
This is a **heuristic assist, not an oracle** — always review the diff.
229+
230+
- It cannot truly know which image is your LCP element; it *guesses* the hero
231+
from structure and naming. Confirm with a real measurement tool such as
232+
Lighthouse or the [web-performance workflow](https://www.network-priority.com/).
233+
- It does not add a hint to more than `--max-hero` images per file.
234+
- It does not overwrite attributes you set yourself (without `--force`).
235+
- It does not rewrite your image `src`, generate responsive `srcset`, resize or
236+
optimize images, or add width/height to prevent layout shift.
237+
- It does not preload web fonts (it only *preconnects* to detected font
238+
origins); the exact font file URL usually isn't available in the template.
239+
- For `next/script`, it only annotates a suggestion — it does not pick a
240+
`strategy` for you. See
241+
[Choosing a next/script strategy](https://www.network-priority.com/framework-specific-loading-strategies/nextjs-resource-loading-optimization/choosing-nextjs-script-strategy/).
242+
- It only auto-inserts `preconnect` for well-known font CDNs (Google Fonts), to
243+
avoid guessing wrong about arbitrary third parties.
244+
245+
## Further reading
246+
247+
- [Framework-specific loading strategies (overview)](https://www.network-priority.com/framework-specific-loading-strategies/)
248+
- [The fetchpriority attribute & Priority Hints](https://www.network-priority.com/core-browser-loading-mechanics-priority-queues/fetchpriority-attribute-priority-hints/)
249+
- [Fixing Next.js LCP image priority](https://www.network-priority.com/framework-specific-loading-strategies/nextjs-resource-loading-optimization/fixing-nextjs-lcp-image-priority/)
250+
- [Choosing a next/script strategy](https://www.network-priority.com/framework-specific-loading-strategies/nextjs-resource-loading-optimization/choosing-nextjs-script-strategy/)
251+
- [Tuning NuxtLink prefetch behavior](https://www.network-priority.com/framework-specific-loading-strategies/nuxt-resource-loading-optimization/tuning-nuxt-link-prefetch-behavior/)
252+
- [Sequencing Astro client directives](https://www.network-priority.com/framework-specific-loading-strategies/astro-islands-loading-optimization/sequencing-astro-client-directives/)
253+
254+
## Contributing
255+
256+
Contributions are welcome. The codebase is small, dependency-free, and fully
257+
covered by `node --test`:
258+
259+
```sh
260+
node --test
261+
```
262+
263+
Please add a fixture under [`test/fixtures/`](./test/fixtures) (an `input.*` and
264+
an `expected.*` pair) for any new transform, and make sure the idempotency tests
265+
still pass. Keep the tool dependency-free and the transforms
266+
formatting-preserving.
267+
268+
## License
269+
270+
[MIT](./LICENSE) © 2026 Network Priority

‎bin/priority-hints-codemod‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
4+
const { run } = require('../src/cli.js');
5+
6+
try {
7+
process.exitCode = run(process.argv.slice(2));
8+
} catch (err) {
9+
process.stderr.write('Unexpected error: ' + (err && err.stack ? err.stack : err) + '\n');
10+
process.exitCode = 1;
11+
}

0 commit comments

Comments
 (0)