LCP candidate
the image with fetchpriority="high", else a preloaded image, else the first content image (icons, logos in nav/header, tracking pixels and <noscript> are skipped).
GitHub Packages · MIT · Node ≥ 18
Lazy-load what is below the fold, never the LCP image: a static lazy-loading audit for your built site.
Lazy-loading audit for static sites: below-the-fold images and iframes missing loading=lazy, and LCP candidates (hero images, first images) wrongly lazy-loaded or deprioritised. SARIF, Action. Zero deps.
1 Point the @mrlouiswright scope at GitHub Packages in .npmrc
@mrlouiswright:registry=https://npm.pkg.github.com2 Install
npm i -D @mrlouiswright/lazy-pulsePublished to GitHub Packages, not npmjs.com. GitHub Packages needs a GitHub token with read:packages to install, even for public packages: add //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} to .npmrc or run npm login --registry=https://npm.pkg.github.com.
$ npx lazy-pulse test/fixtures/badlazy-pulse — test/fixtures/bad1 page(s), 7 image(s)/iframe(s), 4 lazy-loaded HIGH index.html:6 LCP candidate hero.jpg on / has loading="lazy" [lazy-lcp-lazy]HIGH index.html:6 /img/hero.jpg is preloaded on / but its <img> is loading="lazy" [lazy-preload-conflict]MEDIUM index.html:9 Below-the-fold image a.jpg on / has no loading="lazy" [lazy-img-missing]MEDIUM index.html:12 Below-the-fold image d.jpg on / has no loading="lazy" [lazy-img-missing]MEDIUM index.html:13 iframe https://www.google.com/maps/embed?pb=1 on / is not lazy-loaded [lazy-iframe-missing]LOW index.html:7 second.jpg is among the first 1 content images on / but has loading="lazy" [lazy-above-fold-lazy]LOW index.html:10 Lazy image team.jpg on / has no width/height [lazy-no-dimensions]LOW index.html:11 c.jpg on / is lazy-loaded with data-src [lazy-js-library] 2 high, 3 medium, 3 low✔ Passing (fail-on: none).
Native “loading="lazy"” is one attribute, and it is easy to get wrong in both directions: galleries and embeds below the fold download eagerly, while a template applies “loading="lazy"” (or a data-src library) to the hero image and pushes back Largest Contentful Paint. lazy-pulse reads your built HTML, picks the likely LCP image on each page, and checks every image and iframe against where it sits in the document.
CLI, library and GitHub Action. Node.js 18+, zero runtime dependencies. Works on any static build output (Astro, Next.js “output: 'export'”, Eleventy, Hugo, Jekyll, Docusaurus, VitePress, plain HTML) and runs offline: point it at the folder your build writes. By Louis Wright.
the image with fetchpriority="high", else a preloaded image, else the first content image (icons, logos in nav/header, tracking pixels and <noscript> are skipped).
loading="lazy" (high), lazy-loaded by JavaScript via data-src (high), fetchpriority="low" (medium), a <link rel=preload as=image> for an image that is lazy (high), and no fetchpriority at all (info).
images and iframes without loading="lazy" after the first foldImages content images (default 1 after the LCP image); iframes (video, maps, widgets) are always expected to be lazy unless allowlisted.
content images near the top that are lazy (low).
lazy images without width/height or aspect-ratio (layout shift), and JS lazy-load libraries where native lazy loading would do.
# Example
npx lazy-pulse dist# Example
npx lazy-pulse _site --fold-images 2# Example
npx lazy-pulse public --fail-on medium -f sarif > lazy.sarifpermissions:
contents: read
security-events: write # only needed for the SARIF upload
jobs:
lazy-pulse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with: { persist-credentials: false }
- run: npm ci && npm run build # produce the static site first
- uses: MrLouisWright/lazy-pulse@v1
with:
path: dist
fail-on: high
sarif: lazy-pulse.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with: { sarif_file: lazy-pulse.sarif }
| Flag | Description |
|---|---|
| Site | |
--base-url <url> | Public URL of the site, e.g. https://example.com (default: inferred from canonical/og:url) |
--include <globs> | Only check these pages (file or URL path globs, comma-separated) |
--exclude <globs> | Skip these pages |
--clean-urls | Pages are served without .html (about.html -> /about) |
| Lazy loading | |
--fold-images <n> | Content images treated as above the fold, after the LCP candidate (default 1) |
| Common | |
-c, --config <file> | JSON config (default: .lazy-pulse.json or "lazyPulse" in package.json) |
-f, --format <fmt> | text (default) | json | sarif | markdown |
--json <file> | Also write a JSON report |
--sarif <file> | Also write SARIF 2.1.0 |
--markdown <file> | Also write Markdown (e.g. for a job summary) |
--fail-on <sev> | critical | high | medium | low | info | none (default: high) |
--list-rules | Print rules and default severities |
--no-fixes | Hide fix suggestions in text output |
--no-color | Disable colours |
Full reference, config file format and API in the README.
Support this project
I build free, open-source tools that help developers ship safer websites. Every coffee helps keep them maintained, documented and free for everyone.
Buy me a coffee (opens Stripe in a new tab)Secure checkout by Stripe