DocumentationFramework guide

Image optimization

@briansunter/nib-images is an optional build-time package. It creates static responsive image markup and files; it does not add a client runtime.

Install it:

npm install @briansunter/nib-images

Register the plugin in nib.config.ts:

import { defineConfig } from '@briansunter/nib'
import { images } from '@briansunter/nib-images/plugin'

export default defineConfig({
  plugins: [images({ formats: ['avif', 'webp'] })],
})

Import a local image with the explicit query and render it with Image:

import { Image } from '@briansunter/nib-images'
import hero from './hero.jpg?nib-image'

export default function Page() {
  return <Image src={hero} alt="Hikers on a ridge" layout="full" priority />
}

Image reads width and height from the import, including EXIF orientation. It emits AVIF and WebP <source> elements plus JPEG (or PNG for alpha) fallback markup. Standard images use loading="lazy" and decoding="async"; priority uses eager loading and a high fetch priority.

quality applies to AVIF, WebP, and JPEG. PNG fallback output stays lossless and uses a fixed compression setting, so a misleading PNG quality control is not exposed.

Use useImage() when a component needs the resolved URLs without delegating markup to Image, such as a gallery that serializes candidates for a scoped client enhancement:

import { useImage } from '@briansunter/nib-images'
import photo from './photo.jpg?nib-image'

export function GalleryItem() {
  const getImage = useImage()
  const image = getImage({
    src: photo,
    layout: 'constrained',
    widths: [320, 640, 960],
    sizes: '(min-width: 48rem) 50vw, 100vw',
  })
  return <a href={image.src} data-sources={JSON.stringify(image.sources)}>Open</a>
}

Call the hook once during a server-rendered component, then call its stable getImage function freely inside maps or loops. It registers the same build transforms as Image and returns src, optional srcSet/sizes, intrinsic width/height, modern-format sources, and passthrough. It does not encode bytes immediately and is not a browser image service; the images plugin must wrap the Nib renderer and finalizes every registered candidate into static output.

Resolve paths stored in content

When a JSON collection or Markdown field already stores a configured public path, use the server-only content resolver instead of maintaining an eager import.meta.glob lookup in the application:

import { Image } from '@briansunter/nib-images'
import { resolveContentImage } from '@briansunter/nib-images/content'

export function GalleryImage({ src, alt }: { src: string; alt: string }) {
  const source = resolveContentImage(src)
  if (!source) return <img src={src} alt={alt} />
  return <Image src={source} alt={alt} layout="constrained" width={640} />
}

The resolver catalog comes directly from every images({ content: [...] }) publicPath and directory pair. It returns undefined for unknown or unsafe paths. Overlapping content roots may coexist, but the build rejects two files that claim the same public path rather than silently choosing one. This import belongs only in server-rendered pages, layouts, and components; browser-target enhancements and islands reject it. Corrupt candidates warn and resolve as undefined, matching the content optimizer's unoptimized fallback behavior.

Layouts and sizes

Layout Use Default sizes
constrained A responsive image with a known maximum width (max-width: Wpx) 100vw, Wpx
fixed An icon or fixed-size image Density descriptors, no sizes
full An image spanning its container 100vw

Use an explicit sizes value when CSS places an image in a multi-column layout. Nib cannot infer arbitrary container widths. widths replaces the configured responsive ladder, while fixed uses densities instead.

Cache and limits

Transformed files are content-addressed in .nib/cache/images and linked or copied to dist/client/assets/nib. Deleting dist is safe; a warm build reuses checksum-validated cache entries. concurrency bounds all active Sharp transforms, while memoryLimitMb can lower that bound using a conservative per-transform estimate for CI. The automatic value respects both available processors and Node's libuv image-task concurrency. Nib maintainers can run bun run benchmark:images to compare cold, warm, concurrency, memory, and WebP-only versus AVIF-plus-WebP behavior on the current machine.

In development, every imported image is watched explicitly. Changed bytes are re-inspected without restarting the server and receive a new ETag; touching or byte-identically rewriting a file keeps the same key and returns 304 without re-encoding. Short editor overwrite windows are retried instead of discarding the last valid source.

The root component entry is processor-free. Import images() from the /plugin entry as shown above. Image is a static rendering primitive and cannot be imported into a client enhancement or React island; build and development report that mistake instead of shipping build-only image processing code to the browser.

For imported content or Markdown that already uses stable public image URLs, configure an opt-in source root. Nib copies the originals to that public prefix for lightboxes and fallbacks, then rewrites matching rendered <img> elements to the same responsive <picture> pipeline:

images({
  content: [{
    publicPath: '/site-assets/',
    directory: 'src/assets/site-assets',
    widths: [320, 640, 1280],
    sizes: '(min-width: 900px) 860px, 100vw',
  }],
})

This is deliberately reference-driven rather than a scan of every file in the project. Animated, SVG, or unsupported/corrupt sources remain available at their original URL and do not abort the rest of the build.

Only local JPEG, PNG, WebP, and AVIF sources are transformed in this release. SVG and animated sources are passed through without rasterization or animation conversion. Remote URLs and automatic relative-path Markdown image resolution are not yet supported.