Skip to content

Image Optimization (Optional) ​

Use @sveltejs/enhanced-img for build-time AVIF, WebP, and fallback images. Source files must be available to Vite at build time; this does not cover runtime uploads or remote URLs.

Installation ​

bash
npm install --save-dev @sveltejs/enhanced-img

Vite Configuration ​

Import enhancedImages and place enhancedImages() before svelte(). Keep the existing plugins, formatting settings, and svelte-check environment guard.

File: vite.config.ts

ts
import inertia from '@inertiajs/vite';
import { wayfinder } from '@laravel/vite-plugin-wayfinder';
import { enhancedImages } from '@sveltejs/enhanced-img';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import tailwindcss from '@tailwindcss/vite';
import laravel from 'laravel-vite-plugin';
import { bunny } from 'laravel-vite-plugin/fonts';
import { defineConfig, lazyPlugins } from 'vite-plus';

const isSvelteCheck = process.argv.some((argument) =>
    argument.includes('svelte-check'),
);

if (isSvelteCheck) {
    process.env.LARAVEL_BYPASS_ENV_CHECK ??= '1';
}

export default defineConfig({
    plugins: lazyPlugins(() => [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.ts'],
            refresh: true,
            fonts: [
                bunny('Instrument Sans', {
                    weights: [400, 500, 600],
                }),
            ],
        }),
        inertia(),
        tailwindcss(),
        enhancedImages(),
        svelte(),
        wayfinder({
            formVariants: true,
        }),
    ]),
    server: {
        watch: {
            ignored: [
                '**/.agents/**',
                '**/.claude/**',
                '**/.cursor/**',
                '**/.junie/**',
                '**/vendor/**',
            ],
        },
    },
    lint: {
        ignorePatterns: [
            'vendor/**',
            'node_modules/**',
            'public/**',
            'bootstrap/ssr/**',
            'tailwind.config.js',
            'resources/js/actions/**',
            'resources/js/components/ui/*',
            'resources/js/routes/**',
            'resources/js/wayfinder/**',
        ],
        options: {
            denyWarnings: true,
            typeAware: true,
        },
    },
    fmt: {
        printWidth: 80,
        tabWidth: 4,
        singleQuote: true,
        semi: true,
        singleAttributePerLine: false,
        htmlWhitespaceSensitivity: 'css',
        ignorePatterns: [
            '.agents/skills/**',
            '.claude/skills/**',
            '.github/**',
            '.mcp.json',
            '.pi/skills/**',
            'AGENTS.md',
            'CLAUDE.md',
            'boost.json',
            'composer.json',
            'resources/js/components/ui/*',
            'resources/views/mail/*',
        ],
        sortTailwindcss: {
            functions: ['clsx', 'cn', 'cva'],
            entryPoint: 'resources/css/app.css',
        },
    },
});

Restart the development server after changing the configuration.

TypeScript Declarations ​

Global Types ​

Add the @sveltejs/enhanced-img import; keep the existing environment and Inertia declarations.

File: resources/js/types/global.d.ts

ts
import type { Auth } from '@/types/auth';
import '@sveltejs/enhanced-img';

// Extend ImportMeta interface for Vite...
declare module 'vite/client' {
    interface ImportMetaEnv {
        readonly VITE_APP_NAME: string;
        [key: string]: string | boolean | undefined;
    }

    interface ImportMeta {
        readonly env: ImportMetaEnv;
        readonly glob: <T>(
            pattern: string,
            options?: { eager?: boolean },
        ) => Record<string, T>;
    }
}

declare module '@inertiajs/core' {
    export interface InertiaConfig {
        sharedPageProps: {
            name: string;
            auth: Auth;
            sidebarOpen: boolean;
            [key: string]: unknown;
        };
    }
}

Image Imports ​

Add the *&enhanced declaration for imports with width queries; the package's *?enhanced declaration does not cover that suffix.

File: resources/js/types/images.d.ts

ts
declare module '*&enhanced' {
    import type { Picture } from '@sveltejs/enhanced-img';

    const image: Picture;
    export default image;
}

Source Image ​

bash
mkdir -p resources/images

Save the original image as resources/images/matrix-rain.jpg. The test expects this 2400 × 1200 fixture and its 2:1 aspect ratio; do not substitute a resized preview.

File: resources/images/matrix-rain.jpg

Green digital code on a dark background

Import source images through Vite, not a /public URL.

Home Page ​

Request widths 480, 768, 1280, 1920, and 2400. Keep sizes in sync with the content width and fetchpriority="high" on this hero; do not lazy-load it.

File: resources/js/pages/Home.svelte

svelte
<script module lang="ts">
    import { home } from '@/routes';

    export const layout = {
        breadcrumbs: [
            {
                title: 'Home',
                href: home(),
            },
        ],
    };
</script>

<script lang="ts">
    import matrixRain from '../../images/matrix-rain.jpg?w=480;768;1280;1920;2400&enhanced';
    import AppHead from '@/components/AppHead.svelte';
</script>

<AppHead title="Home" />

<div class="mx-auto w-full max-w-7xl flex-1 px-4 py-20 sm:px-6 lg:px-8">
    <h1 class="bg-linear-to-r from-sky-500 to-indigo-500 bg-clip-text text-4xl font-bold tracking-tight text-transparent">
        Home
    </h1>
    <p class="mt-4 max-w-2xl text-lg leading-relaxed text-muted-foreground">
        {import.meta.env.VITE_APP_NAME || 'Laravel'} welcomes you.
    </p>
    <enhanced:img
        src={matrixRain}
        alt="Glowing green streams of digital code cascading across a dark background"
        sizes="(min-width: 1280px) 1216px, (min-width: 1024px) calc(100vw - 64px), (min-width: 640px) calc(100vw - 48px), calc(100vw - 32px)"
        fetchpriority="high"
        class="mt-10 block h-auto w-full rounded-xl ring-1 ring-emerald-500/20"
    />
</div>

Image Optimization Tests ​

Check output formats, responsive markup, and Laravel dev asset URLs with the real Vite config. Keep the test directly under tests/; it resolves the project root from there.

File: tests/image-optimization.test.mjs

js
import assert from 'node:assert/strict';
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { test } from 'node:test';
import { build, createServer } from 'vite-plus';

const root = path.resolve(import.meta.dirname, '..');

await test('builds responsive pictures with the application Vite configuration', async (t) => {
    const directory = await mkdtemp(path.join(root, 'tests/.images-'));
    t.after(() => rm(directory, { recursive: true, force: true }));
    const original = await readFile(
        path.join(root, 'resources/images/matrix-rain.jpg'),
    );
    await writeFile(path.join(directory, 'photo.jpg'), original);
    await writeFile(
        path.join(directory, 'Image.svelte'),
        `<script>
            import photo from './photo.jpg?w=800;400&enhanced';
        </script>
        <enhanced:img src="./photo.jpg?w=800;400" sizes="100vw" alt="Static image" />
        <enhanced:img src={photo} sizes="100vw" alt="Imported image" />`,
    );
    const entry = path.join(directory, 'entry.js');
    await writeFile(
        entry,
        `import { mount } from 'svelte';
        import Image from './Image.svelte';
        mount(Image, { target: document.body });`,
    );

    const result = await build({
        mode: 'development',
        root,
        configFile: path.join(root, 'vite.config.ts'),
        logLevel: 'error',
        build: {
            write: false,
            minify: false,
            assetsInlineLimit: 0,
            rolldownOptions: { input: entry },
        },
    });

    const output = result.output;
    const code = output
        .filter((file) => file.type === 'chunk')
        .map((file) => file.code)
        .join('\n');
    assert.ok(code.includes('<picture>'));
    assert.ok(!code.includes('<enhanced:img'));
    assert.match(code, /width=.?800/);
    assert.match(code, /height=.?400/);
    assert.ok(code.includes('400w'));
    assert.ok(code.includes('800w'));
    assert.ok(code.includes('100vw'));
    for (const format of ['avif', 'webp', 'jpg']) {
        const images = output.filter(
            (file) =>
                file.type === 'asset' &&
                (file.fileName.endsWith(`.${format}`) ||
                    (format === 'jpg' && file.fileName.endsWith('.jpeg'))),
        );
        assert.equal(images.length, 2, `Expected two ${format} sizes`);
        for (const image of images) {
            assert.ok(image.source.byteLength > 0);
            assert.ok(image.source.byteLength < original.byteLength);
        }
    }
});

await test('transforms the home image with Laravel development asset URLs', async (t) => {
    const server = await createServer({
        root,
        mode: 'development',
        configFile: path.join(root, 'vite.config.ts'),
        logLevel: 'error',
        optimizeDeps: { noDiscovery: true, include: [] },
        plugins: [
            {
                name: 'image-test-no-hot-file',
                configResolved(config) {
                    /** Transforms only: do not register cleanup of the real public/hot file. */
                    config.plugins.find(
                        (plugin) => plugin.name === 'laravel',
                    ).configureServer = undefined;
                },
            },
        ],
        server: {
            middlewareMode: true,
            watch: null,
            hmr: false,
            origin: 'http://localhost:5173',
        },
    });
    t.after(() => server.close());

    const page = await server.transformRequest(
        '/resources/js/pages/Home.svelte',
    );
    const image = await server.transformRequest(
        '/resources/images/matrix-rain.jpg?w=480;768;1280;1920;2400&enhanced',
    );

    assert.ok(page.code.includes('picture'));
    assert.ok(image.code.includes('http://localhost:5173/@imagetools/'));
    assert.ok(image.code.includes('480w'));
    assert.ok(image.code.includes('2400w'));
});

Run the Node test explicitly; Pest and composer ci:check do not pick it up:

bash
node --test tests/image-optimization.test.mjs
npm run types:check
npm run build

Check the home image at mobile and desktop widths, both with the dev server and with built assets. The first build is slower; later builds use node_modules/.cache/imagetools.

Commit ​

bash
git add -A
git commit -m '07 - Image Optimization'

References ​