|
| 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 |
0 commit comments