DEV Community

Matthias Andrasch
Matthias Andrasch

Posted on

Optimize external / dynamic images on-the-fly in SvelteKit

If all your images are known at build time, @sveltejs/enhanced-img is all you need for optimizing and delivering responsive images to your visitors.

But what about external / dynamically loaded images?

Paid CDNs

The easiest way to optimize external images is using a cloud service (CDN) for image optimization such as cloudinary, imgix, etc. Here is a list of providers which can be used via @unpic/svelte. But these services cost money (if you exceed their free tiers). See SvelteKit docs as well for more information.

Selfhost an image optimizer

Fortunately for us, there are selfhosting alternatives. 🥳

Btw: Frameworks like NuxtJS already offer packages like NuxtImage where you can easily add ipx as selfhosted image optimizer to your project:

Nuxt/Image comes with a preconfigured instance of unjs/ipx. An open source, self-hosted image optimizer based on lovell/sharp.

For SvelteKit, this is discussed here:

But as long as there is no officially supported plugin, you can integrate an image optimizer like ipx easily by yourself.

Selfhost ipx (within SvelteKit)

ipx is an easy-to-use image optimizer powered by sharp and svgo, written in NodeJS. It can be installed as package to your SvelteKit project:

npm i ipx
Enter fullscreen mode Exit fullscreen mode

Afterwards, we need to integrate ipx into a custom express server route in SvelteKit. The whole approach requires using adapter-node in our project.

Here is a demo repository with SvelteKit v2 and unpic v1:

We switch to adapter-node and add the express package to our project:

npm i express
Enter fullscreen mode Exit fullscreen mode

We then add the express server configuration in a file like my-server.js:

// my-server.js
import { handler } from './build/handler.js';
import express from 'express';
import {
    createIPX,
    ipxHttpStorage,
    createIPXNodeServer
} from "ipx";


const app = express();

// add a route that lives separately from the SvelteKit app
app.get('/healthcheck', (req, res) => {
    res.end('ok');
});

// ipx image optimizer
const ipx = createIPX({
    // Allowed domains for optimization
    // With httpStorage: ipxHttpStorage({ domains: ["your-domain.com", "second-domain.com"] }), ipx will optimize images coming for a given domain.
    httpStorage: ipxHttpStorage({ domains: ["picsum.photos"] }),
});
app.use("/_ipx", createIPXNodeServer(ipx));

// let SvelteKit handle everything else, including serving prerendered pages and static assets
app.use(handler);

app.listen(3000, () => {
    console.log('listening on port 3000');
});
Enter fullscreen mode Exit fullscreen mode

In a nutshell, we implemented three server routes: One /healthcheck route, one /_ipx-route which handles image optimization requests and serves optimized images via ipx's server and one serves your SvelteKit app as usual.

We also configured the allowed domains for ipx - you can add your external domains from where you want to load images.

In package.json we add the following to the scripts section:

"start": "node ./my-server.js"
Enter fullscreen mode Exit fullscreen mode

This is the command which will be used when deploying and starting your app on production.

The last step is to add @unpic/svelte to our project, which supports ipx:

npm i @unpic/svelte
Enter fullscreen mode Exit fullscreen mode

Now we can use an external image from https://picsum.photos/ and retrieve the large 4K version (3840 x 2160 pixel) of it. We import the ipx transformer and tell unpic that ipx should handle the image optimization:

<script lang="ts">
  // Important: Use base component which has transformer support
  import { Image } from '@unpic/svelte/base'; 
  // Import the transformer for the provider you are using
  import { transform } from "unpic/providers/ipx";
</script>

<Image
    src="https://picsum.photos/id/29/3840/2160"
    layout="constrained"
    width={800}
    height={600}
    alt="winter mountain landscape"
    transformer={transform}
/>
Enter fullscreen mode Exit fullscreen mode

If you run this app via npm run build && npm run start, the responsive HTML output of this will be the following:

<img alt="winter mountain landscape" style="object-fit: cover; max-width: 800px; max-height: 600px; aspect-ratio: 1.33333 / 1; width: 100%;" loading="lazy" decoding="async" sizes="(min-width: 800px) 800px, 100vw" srcset="/_ipx/s_640x480/https://picsum.photos/id/29/3840/2160 640w,
/_ipx/s_750x563/https://picsum.photos/id/29/3840/2160 750w,
/_ipx/s_800x600/https://picsum.photos/id/29/3840/2160 800w,
/_ipx/s_828x621/https://picsum.photos/id/29/3840/2160 828w,
/_ipx/s_960x720/https://picsum.photos/id/29/3840/2160 960w,
/_ipx/s_1080x810/https://picsum.photos/id/29/3840/2160 1080w,
/_ipx/s_1280x960/https://picsum.photos/id/29/3840/2160 1280w,
/_ipx/s_1600x1200/https://picsum.photos/id/29/3840/2160 1600w" src="/_ipx/s_800x600/https://picsum.photos/id/29/3840/2160">
Enter fullscreen mode Exit fullscreen mode

As you can see, optimized image variants are served by the custom /_ipx-route 🎉

Learn more about all unpic options: https://unpic.pics/learn/

If you use the regular npm run dev, you won't see the optimized images since the Vite devserver doesn't know about ipx. If you wan't, you can use a Vite plugin as described in the tutorial by Kazuumi Nishimura. Beware: The tutorial was written for an earlier version of unpic, not the latest unpic v1.

Selfhost imgproxy

Another alternative for selfhosting is imgproxy. This works more like an external CDN.

You could selfhost it for instance with Coolify, the one click service "Next image transformation" ships the docker image darthsim/imgproxy:

Coolify add new service screenshot

Beware: The official imgproxy Docker repository was moved to ghcr.io/imgproxy/imgproxy⁠. The repository on Docker Hub will be kept available, but we may stop updating images in it eventually. So you might want to install it as Docker image from there via Coolify (or other services)

I didn't find specific tutorials for SvelteKit for the usage of imgproxy, but the usage should be most likely similiar to these tutorials:

I assume you would need to write a custom image loading function, since imgproxy seems not yet be supported by @unpic/svelte. But there is an ongoing discussion: Feature Request: Support for Self Hosted image manipulation server (Thumbor, Imgproxy, Imaginary) #37.

If you're interested in selfhosting and Coolify, make sure to check out Deploy SvelteKit with SSR on Coolify (Hetzner VPS) as well.

Other alternatives?

In the GitHub discussion about image processing in SvelteKit, developer mrxbox98 posted about his experiment of using sharp directly in SvelteKit.

There are also other alternatives for selfhosting such as https://www.thumbor.org/.

Let's hope that this will be a bit easier in future with an image library similiar to NuxtImage.

If you know about other tutorials or alternatives, please let me know in the comments - thanks! 🙏

Acknowledgements:

  • This article section about ipx is based on the wonderful (japanese) tutorial by Kazuumi Nishimura showing all the necessary steps: SvelteKit + ipx + unpic dynamic and optimal image delivery. Beware: It was written for an earlier version of unpic, not the latest unpic v1.
  • Thanks very much to tobimori for giving some helpful hints about image processing within JS frameworks (ipx/sharp) and thanks to Jens for the idea to write an article about this and reviewing it.
  • Thanks to Martin Grubinger for the info about imgproxy via Coolify.

Top comments (0)