Granular image optimization for Next.js with separate quality control for AVIF/WebP
npm install next-granular-imagesNextGranularImage with art direction props | Wraps next/image component |
next/image API
bash
npm
npm install next-granular-images
pnpm
pnpm add next-granular-images
yarn
yarn add next-granular-images
`
$3
Ensure you have these installed:
`bash
npm install sharp@^0.33.0
`
> Note: sharp is required for image processing. Next.js 13+ and React 18+ are also required as peer dependencies.
---
Quick Start
$3
`bash
npx next-granular-images init
`
This creates next-granular-images.config.ts with sensible defaults.
$3
Place your images in the configured input directory (default: src/assets).
$3
`bash
npx next-granular-images optimize
`
This generates:
- Optimized AVIF/WebP variants in public/next-granular-images/
- TypeScript types in src/generated/next-granular-images/
$3
`tsx
import { NextGranularImage } from 'next-granular-images';
import { heroImage } from '@/generated/next-granular-images/hero';
export function Hero() {
return ;
}
`
$3
In your root layout:
`tsx
import { GranularBlurFix } from 'next-granular-images';
export default function RootLayout({ children }) {
return (
{children}
);
}
`
---
Configuration
$3
Create next-granular-images.config.ts in your project root:
`typescript
import { GranularImagesConfig } from 'next-granular-images';
const config: GranularImagesConfig = {
// Image quality settings (1-100)
qualities: {
avif: 60, // AVIF quality (optional)
webp: 85, // WebP quality (optional)
},
// Encoding effort (higher = slower but better compression)
effort: {
avif: 9, // AVIF effort: 1-9 (required if avif quality is set)
webp: 6, // WebP effort: 1-6 (required if webp quality is set)
},
// Responsive breakpoints (generates variants for each)
breakpoints: {
sm: 640,
md: 768,
lg: 1024,
xl: 1280,
},
// Device widths for srcset generation
deviceSizes: [400, 500, 640, 750, 828, 1080, 1200, 1920, 2048, 3840],
// Image widths for smaller images (icons, thumbnails)
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
// Parallel processing (higher = faster but more memory)
concurrency: 4,
// Skip Sharp processing for files smaller than this (KB)
minSizeToOptimize: 0,
// Blur placeholder settings
blurSize: 10, // 4-64 pixels
blurQuality: 100, // 1-100
// Path configuration
paths: {
input: 'src/assets',
output: 'public/next-granular-images',
types: 'src/generated/next-granular-images',
},
// Files to exclude from processing
exclusions: ['.ico', '.xml', '.webmanifest', '.svg', '.webp', '.avif'],
};
export default config;
`
$3
#### Qualities
At least one format (AVIF or WebP) must be configured:
`typescript
// AVIF only
qualities: { avif: 60 }
// WebP only
qualities: { webp: 85 }
// Both formats
qualities: { avif: 60, webp: 85 }
`
#### Effort
If a quality is set for a format, the corresponding effort must also be set:
| Format | Effort Range | Description |
| ------ | ------------ | --------------------------------------------------- |
| AVIF | 1-9 | Higher values = slower encoding, better compression |
| WebP | 1-6 | Higher values = slower encoding, better compression |
`typescript
// Valid: Both quality and effort set
qualities: { avif: 60 },
effort: { avif: 9 }
// Invalid: Quality without effort
qualities: { avif: 60 },
effort: {} // โ Error
`
$3
| Path | Default | Description |
| -------- | ------------------------------------ | -------------------------- |
| input | src/assets | Source images directory |
| output | public/next-granular-images | Optimized images output |
| types | src/generated/next-granular-images | Generated TypeScript types |
> โ ๏ธ Important: The output path should be inside public/ for Next.js to serve the files. The path should contain next-granular-images for safety checks during cleanup.
$3
Breakpoints define art direction boundaries:
`typescript
breakpoints: {
sm: 640, // Mobile
md: 768, // Tablet
lg: 1024, // Desktop
xl: 1280, // Large screens
}
`
Device Sizes define the actual widths for generated variants:
`typescript
deviceSizes: [400, 500, 640, 750, 828, 1080, 1200, 1920, 2048, 3840];
`
Both arrays must be sorted in ascending order.
$3
| Option | Default | Description |
| ------------------- | ------- | ------------------------------------- |
| concurrency | 4 | Images processed in parallel |
| minSizeToOptimize | 0 | Skip Sharp for files < this size (KB) |
`typescript
// For machines with lots of RAM
concurrency: 8,
// Skip processing for tiny files (copied as-is)
minSizeToOptimize: 10, // Files < 10KB are just copied
`
---
CLI Commands
All commands are run via npx next-granular-images or configured as npm scripts.
$3
Initialize the library in your project.
`bash
npx next-granular-images init [options]
`
Options:
| Flag | Description |
| -------------- | -------------------------------------------------- |
| --build | Run optimize after init (default: production mode) |
| --build fast | Run optimize in fast mode after init |
| --build dev | Run optimize in dev mode after init |
Examples:
`bash
Create config file only
npx next-granular-images init
Create config and immediately process images
npx next-granular-images init --build
Create config and process in fast mode
npx next-granular-images init --build fast
`
Behavior:
- Creates next-granular-images.config.ts with default settings
- Skips creation if config already exists
- Optionally runs optimize with specified mode
---
$3
Process all images according to configuration.
`bash
npx next-granular-images optimize [options]
`
Options:
| Flag | Description |
| ---------- | ------------------------------------------------ |
| --fast | WebP only, Q:15, E:1 (fastest, lowest quality) |
| --dev | Half quality, effort 1 (fast development builds) |
| --report | Show detailed savings report per breakpoint |
Examples:
`bash
Production build (full quality)
npx next-granular-images optimize
Quick preview during development
npx next-granular-images optimize --fast
Development build with reasonable quality
npx next-granular-images optimize --dev
Production build with savings report
npx next-granular-images optimize --report
`
Process:
1. Scans input directory for images
2. Validates for duplicate content and filename collisions
3. Generates responsive variants for each breakpoint
4. Creates blur placeholders
5. Outputs optimized files with content-based hashes
6. Generates TypeScript type definitions
7. Detects orphaned files from previous builds
Caching:
Images are cached based on a composite hash of:
- File content hash
- Configuration hash
If neither changes, the image is skipped (cache hit).
Sample Output:
`
๐ Starting Next Granular Images (Optimize)...
Found 24 images.
Processing: hero/desktop.png
Processing: hero/mobile.png
Processing: products/item-1.jpg
...
๐ Savings Report:
โโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโฌโโโโโโโ
โ โ Original โ Optimized โ Saved โ % โ
โโโโโโโผโโโโโโโโโโโโผโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโค
โ sm โ 12.50 MB โ 2.30 MB โ 10.20 MBโ 81.6%โ
โ md โ 12.50 MB โ 3.10 MB โ 9.40 MB โ 75.2%โ
โ lg โ 12.50 MB โ 4.50 MB โ 8.00 MB โ 64.0%โ
โ xl โ 12.50 MB โ 5.80 MB โ 6.70 MB โ 53.6%โ
โโโโโโโดโโโโโโโโโโโโดโโโโโโโโโโโโดโโโโโโโโโโดโโโโโโโ
โจ Done in 45.32s
Processed: 24
Cached: 0
`
---
$3
Regenerate TypeScript types without reprocessing images.
`bash
npx next-granular-images generate [options]
`
Options:
| Flag | Description |
| --------------- | ------------------------------------------------ |
| (none) | Regenerate all types (breakpoints + images) |
| --breakpoints | Regenerate only breakpoint types (config.d.ts) |
| --images | Regenerate only image types |
Examples:
`bash
Regenerate all types from existing optimized images
npx next-granular-images generate
Regenerate only breakpoint types (after config change)
npx next-granular-images generate --breakpoints
Regenerate only image types
npx next-granular-images generate --images
`
Use cases:
- TypeScript files were accidentally deleted
- You manually modified the output directory
- You changed breakpoints in config and need to update types
- Types need updating without full reprocessing
---
$3
Remove generated files and artifacts.
`bash
npx next-granular-images clean [options]
`
Options:
| Flag | Description |
| --------------- | -------------------------------------------------- |
| (none) | Clean output directory and types directory |
| --image | Clean only image artifacts (keeps config types) |
| --breakpoints | Clean only config.d.ts (breakpoint types) |
| --all | Full reset: removes output, types, and config file |
Examples:
`bash
Standard cleanup (output + types)
npx next-granular-images clean
Remove only optimized images
npx next-granular-images clean --image
Remove only breakpoint config types
npx next-granular-images clean --breakpoints
Complete reset (restore to pre-init state)
npx next-granular-images clean --all
`
Safety:
The clean command includes a safety check that requires paths to contain next-granular-images to prevent accidental deletion of unrelated directories.
---
React Component
$3
`tsx
import { NextGranularImage } from 'next-granular-images';
import { productImage } from '@/generated/next-granular-images/products';
export function ProductCard() {
return (
src={productImage}
alt="Product name"
sizes="(max-width: 768px) 100vw, 50vw"
/>
);
}
`
$3
| Prop | Type | Default | Description |
| ------------------- | ----------------------------------- | ------------ | ---------------------------------------------- |
| src | GeneratedImage \| ArtDirectionSrc | required | Generated image object or art direction object |
| alt | string | required | Alt text for accessibility |
| sizes | string | - | Responsive sizes attribute |
| className | string | - | CSS class names |
| style | CSSProperties | - | Inline styles |
| loading | 'lazy' \| 'eager' | 'lazy' | Loading strategy |
| decoding | 'async' \| 'sync' \| 'auto' | 'async' | Decoding hint |
| fetchPriority | 'high' \| 'low' \| 'auto' | 'auto' | Fetch priority hint |
| placeholder | string \| null | - | Blur placeholder URL |
| customBreakpoints | Record | - | Override default breakpoints |
$3
Serve different images at different breakpoints:
`tsx
import { NextGranularImage } from 'next-granular-images';
import { heroDesktop } from '@/generated/next-granular-images/hero/desktop';
import { heroMobile } from '@/generated/next-granular-images/hero/mobile';
export function Hero() {
return (
src={{
default: heroMobile, // Used for smallest screens + fallback
md: heroDesktop, // Used for screens >= 768px
}}
alt="Hero banner"
sizes="100vw"
/>
);
}
`
The breakpoint keys (sm, md, lg, xl) correspond to your config's breakpoints and are type-checked via the generated config.d.ts.
$3
Enable blur-up effect with the generated placeholder:
`tsx
import { NextGranularImage } from 'next-granular-images';
import {
productImage,
productImageBlur,
} from '@/generated/next-granular-images/products';
export function ProductCard() {
return (
src={productImage}
alt="Product"
placeholder={productImageBlur}
/>
);
}
`
The blur placeholder is a tiny base64-encoded image that displays while the full image loads.
$3
For the blur-to-clear transition to work, add GranularBlurFix once in your root layout:
`tsx
// app/layout.tsx
import { GranularBlurFix } from 'next-granular-images';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
`
This component:
- Attaches a global load event listener
- Fades out blur placeholders when images load
- Handles already-cached images on initial render
---
Generated Types
After running optimize, TypeScript types are generated in your configured types directory.
$3
`
src/generated/next-granular-images/
โโโ config.d.ts # Breakpoint type definitions
โโโ hero/
โ โโโ index.ts # Exports all images in hero/
โ โโโ ...
โโโ products/
โ โโโ index.ts
โ โโโ ...
โโโ index.ts # Root exports
`
$3
Each image generates:
`typescript
// GeneratedImage type
export const heroImage: GeneratedImage = {
src: '/next-granular-images/hero/desktop-abc123.jpg',
width: 1920,
height: 1080,
variants: {
avif: '/next-granular-images/hero/desktop-abc123/...',
webp: '/next-granular-images/hero/desktop-abc123/...',
},
};
// Blur placeholder (base64)
export const heroImageBlur: string = 'data:image/webp;base64,...';
`
$3
The generated config.d.ts augments the component's breakpoint types:
`typescript
// config.d.ts (auto-generated)
declare module 'next-granular-images' {
interface GranularBreakpointOverrides {
sm: true;
md: true;
lg: true;
xl: true;
}
}
`
This provides type checking for art direction keys:
`tsx
src={{
default: mobileImage,
md: desktopImage,
invalid: otherImage, // โ TypeScript error
}}
alt="..."
/>
`
---
Workflow Integration
$3
`json
{
"scripts": {
"images": "next-granular-images optimize",
"images:fast": "next-granular-images optimize --fast",
"images:dev": "next-granular-images optimize --dev",
"images:report": "next-granular-images optimize --report",
"images:clean": "next-granular-images clean",
"images:reset": "next-granular-images clean --all",
"prebuild": "next-granular-images optimize"
}
}
`
$3
`yaml
GitHub Actions example
- name: Install dependencies
run: pnpm install
- name: Optimize images
run: pnpm images
- name: Build Next.js
run: pnpm build
`
$3
1. Initial setup: npx next-granular-images init --build
2. During development: npm run images:fast for quick previews
3. Before commit: npm run images:dev for reasonable quality
4. Production build: npm run images (or let prebuild handle it)
$3
Add to .gitignore:
`gitignore
Optimized images (regenerated on build)
public/next-granular-images/
Generated types (regenerated on build)
src/generated/next-granular-images/
`
> Note: Consider whether to commit generated files. For faster CI builds, you may want to commit them. For smaller repo size, exclude them.
---
Troubleshooting
$3
#### "Input directory not found"
`
Error: Input directory not found: /path/to/src/assets
`
Solution: Create the input directory or update paths.input in your config.
#### "Duplicate image content found"
`
Error: Duplicate image content found.
The following files are identical:
- Hash abc123:
hero/image.png
backup/image.png
`
Solution: Remove duplicate files. The library requires unique content to prevent redundant processing.
#### "Duplicate image names found"
`
Error: Duplicate image names found.
The library generates variables based on filenames (ignoring extensions).
- products/item.png matches products/item.jpg
`
Solution: Rename files to have unique base names, as TypeScript exports are generated from filenames.
#### "Output directory path does not contain 'next-granular-images'"
`
SKIPPED: Output directory path '/public/images' does not contain 'next-granular-images'. Safety check failed.
`
Solution: Update paths.output to include next-granular-images in the path (e.g., public/next-granular-images).
#### Images not updating after changes
Solution: The library uses content hashing. If you only renamed a file without changing content, delete the old output and re-run optimize, or run clean first.
$3
For more verbose output, check the processing logs:
`bash
npx next-granular-images optimize 2>&1 | tee optimize.log
`
---
License
MIT ยฉ Santiago Puertas
`
MIT License
Copyright (c) 2025 Santiago Puertas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
``