Image processing
Rich-Harris opened this issue Β· 70 comments
Updated Aug 2023 by @benmccann - this thread has gotten quite long, so I've summarized some key points here. We have some docs on the site (https://kit.svelte.dev/docs/assets) that share some helpful basics. This issue discusses a potential future implementation within SvelteKit
Static optimization with an Image
component
Vite's build pipeline will handle assets that you import
: https://vitejs.dev/guide/assets.html
You can use vite-imagetools
to transform those images as part of Vite's pipeline. E.g. the most common transform might be to generate avif and webp versions of an image. You can use the defaultDirectives
option to set a project-wide default or you can add query parameters to handle images individually. You can also import a directory of images using Vite's import.meta.glob with its query option.
You could use something like bluwy/svelte-preprocess-import-assets to let users simply write img
tags that get converted to import statements for Vite to process.
A discussion of how to set this up is included at #241 (comment) and further demonstrated in #241 (comment).
Static optimization powered by a preprocessor
A problem with using an Image
component in Svelte is that it requires the usage of :global
to style it and it's difficult to handle events on it. It's possible some of these issues could be addressed in Svelte itself (e.g. there was a community proposal regarding event forwarding sveltejs/rfcs#60), but at the current time there are no concrete plans around this.
One solution to this would be to use a preprocessor to convert:
<img alt="delete" src="$lib/icons/trashcan.png" />
Into something like:
<script>
import __image1 from '$lib/icons/trashcan.png';
import __image1_avif from '$lib/icons/trashcan.png?format=avif';
import __image1_webp from '$lib/icons/trashcan.png?format=webp';
</script>
<picture>
<source type="image/avif" src={__image1_avif} />
<source type="image/webp" src={__image1_webp} />
<img alt="delete" src={__image1} width="24" height="24" />
</picture>
This actually scales very well since sveltejs/svelte#8948.
However, this approach doesn't work as well for something like the SvelteKit showcase or import.meta.glob
because it requires the presence of an img
tag.
Dynamic optimization
You could also implement a function to alter a URL and output the CDN's URL (e.g. #10323). Including this manually may become cumbersome, but as with the static optimization case, you could use something like bluwy/svelte-preprocess-import-assets to let users simply write img
tags that get converted to use this function. The unpic library is an example of this approach. This approach works really well when users have a CDN available and some hosts like Vercel have CDNs as included offerings. For users that want to deploy to somewhere like GitHub pages the static approach might work better and so it may make sense to offer dynamic optimzation alongside one of the static optimization approaches.
Snowpack has some image optimization plugins (https://www.snowpack.dev/plugins) and he said that one off them looked to fit his needs. Would plugins only be used in dev mode because Rollup is used in production mode?
Plugins are used in both cases. Rollup just bundles/analyses the resulting vanilla JS
Ok, it sounds like this is already addressed then, so we could probably close this
Maybe β need to take a look and see if there's anything missing from this: https://twitter.com/rchrdnsh/status/1336386125552779264
That description isn't very helpful, does the plugin transform the sourcecode as well as optimising the image? If it only does the image optimisation, it would be pretty limiting in terms of lazy-loading, generating srcsets, etc. Likewise some way to set the priority of the image (by adding preload tags to the head) would also be good. next/image
does all of this.
Image components aren't always the best solution but they offer a lot of flexibility depending on your needs.
I think the most comprehensive implementations will need to be framework specific, although I haven't thought about it a great deal.
With regards to this, I think a rollup/vite plugin for https://www.npmjs.com/package/sharp would likely be able to fulfil these requirements.
Hi all, thank you for considering this feature...
Just found this new (2 months old, it seems) plugin for Vite...starting to mess around with it with no success yet...seems potentially promising, however :-)
Hi all, thank you for considering this feature...
Just found this new (2 months old, it seems) plugin for Vite...starting to mess around with it with no success yet...seems potentially promising, however :-)
I'm in a close contact with the author and he's rewriting the whole library into core library and different bundler adapters including vite.
You can easily just do this to generate 6 different images together with their width and height info:
import {width, height, src} from "./image.jpg?format=webp;jpg&width=480;1024;1920&metadata"
Hey guys! I'm a little late to the party, but whatever ππ»
With regards to this, I think a rollup/vite plugin for https://www.npmjs.com/package/sharp would likely be able to fulfil these requirements.
vite-imagetools (or the core library more specifically) is basically just a wrapper around sharp, that let's you "execute" sharps image function in the import statement.
The basic idea though is to provide a "common api" that lets you specify how you want your image to be transformed and all the rest (wether the transformation is done by sharp or libvips directly, what buildtools you use, etc.) are completely irrelevant to the developer as all information is encoded in the url and not some config file.
It's not super reliable in vite yet(because vite is sometimes a bit quirky with static assets)
and the docs are horrible,
but I'm working on improvements and other buildtool integrations so if you have any questions, bug, feature requests just let me know ππ»
@JonasKruckenberg really appreciate your work on the vite-imagetools library. I was able to get vite-imagetools integrated with the current release of sveltekit and the node adapter!
- Install vite-imagetools@next as a devDependency
- In your svelte.config.cjs, ensure you have imagetools() set as a vite plugin:
const imagetools = require('vite-imagetools')
vite: {
plugins: [imagetools({force: true})]
}
- wherever you want to import the transformed image (example shown below is in src/routes/$layout.svelte) :
<script>
import Logo1 from '../../static/title.png?webp&meta'
</script>
<img src={Logo1.src} alt="alt description">
Edit: With srcset:
<script>
import Logo1 from '../../static/title.png?w=300;400;500&format=webp&srcset'
</script>
<img srcset={Logo1} type="image/webp" alt="testattribute"/>
import Logo1 from '../../static/title.png?w=300;400;500&format=webp&srcset'
Is it a better idea to put your images like this in the src
folder instead of importing from static
which requires weird ..
directory traversal?
Is it a better idea to put your images like this in the
src
folder instead of importing fromstatic
which requires weird..
directory traversal?
Definitely; I will likely end up moving them to src/static and updating svelte.config.cjs to reflect the updated location of the static assets.
hmmmm...
might it be worth considering adding some sort of official Svelte/Kit support for some sort of assets
folder in the src
folder for images that will be processed by plugins such as these in the future?
hmmmm...
might it be worth considering adding some sort of official Svelte/Kit support for some sort of
assets
folder in thesrc
folder for images that will be processed by plugins such as these in the future?
What more is SvelteKit supposed to do than is already possible per the comment above?
well, i don't have to make a lib
folder myself, as it's already there, with a nice and ergonomic alias as well, which would be super awesome to have for an images
folder...but maybe it's easy to make as alias, as I simply don't know how...but to be more specific, making image processing part of the default configuration out of the box, as it's such an important consideration for performant sites and apps...or maybe even a svelte-add
for image processing in the future...
Example integration of vite-imagetools and sveltekit with $static alias for src/static below:
- Install vite-imagetools@next as a devDependency
- Your svelte.config.cjs should be modified to include the following:
const node = require('@sveltejs/adapter-node');
const pkg = require('./package.json');
const imagetools = require('vite-imagetools')
const path = require('path')
/** @type {import('@sveltejs/kit').Config} */
module.exports = {
kit: {
files: {
assets: 'src/static',
},
vite: {
resolve: {
alias: {
$static: path.resolve('src/static')
}
},
plugins: [imagetools({force: true})]
}
},
};
- Example importing png and converting to webp ($static alias to src/static for convenience)
<script>
import Logo1 from '$static/title.png?webp&meta'
</script>
<img src={Logo1.src} alt="alt description">
- Example importing png in 3 sizes, convert to webp and render as srcset. ($static alias to src/static for convenience)
<script>
import Logo1 from '$static/title.png?w=300;400;500&format=webp&srcset'
</script>
<img srcset={Logo1} type="image/webp" alt="testattribute"/>
very nice :-) We should probably move this kind of stuff to discord, but thank you!
There is so much boilerplate to write, even more if you need backward compatibility with WebP in a picture tag. But since svelte is a compiler, can't we just have an image transformation attribute right on the img tag that can create whatever we want - including lazy loading or fade-in transitions and more?
Well I went down the rabbit hole of trying to use vite-imagetools with SvelteKit yesterday. I have a site using Netlify CMS with local JSON files and wanted to dynamically load and resize images at build time along the lines of
<script lang="ts" context="module">
import page from "$data/pages/home.json";
export async function load() {
const { default: hero } = await import(`../static/${page.hero}.jpg?height=550&format=webp`)
return {
props: {
hero
}
}
}
</script>
It looks like it may be a limitation related to dynamic-import-vars, but when digging through that project I couldn't tell if the query parameters are expected to break. Based on the examples in vite-imagetools
I would have assumed query params alone wouldn't break production builds, but who knows.
I made sure to follow the workarounds for known limitations of the rollup plugin, like having the import starts with ./ or ../
and the file extension is included for the glob serach. It works fine in dev and actually works in production without the query params, I was hoping the dynamic-import-vars logic would have been able to strip off the query params and find the original image file.
Repro
I made a barebones repro of this at https://github.com/tonyfsullivan/sveltekit-imagetools-repro. Not sure if my use case here is just an outlier, it works flawlessly when the image file names are basic strings rather than dynamic variables.
Does anyone know if this should have worked for me, or where the bug/feature request would land? At the moment I have no idea if this would be related to vite
, vite-imagetools
, @rollup/plugin-dynamic-import-vars
, or if it's specific to SvelteKit somehow.
I don't know enough about svelte-kit to really debug this, but I can confirm that it is not an issue with vite-imagetools
as the plugin never gets told about the image in build mode. Seems to be related to vite-imagetools#34 so a problem with with the resolution of the import glob pattern
I wrote a little preprocessor to make using vite-imagetools even easier. Modifiers can be directly appended to the img's src or srcset prop and the imports will be added by the preprocessor.
const svelte = require("svelte/compiler")
/**
* @typedef Options
* @property {{ [tag: string]: string[] }} tags
*/
/**
* @typedef {import('svelte/types/compiler/interfaces').TemplateNode} TemplateNode
*/
/**
* @param {Options} [options]
* @returns {import('svelte/types/compiler/preprocess/types').PreprocessorGroup}
*/
function imagePreprocess(options = {}) {
const imports = {}
const tags = options.tags || {
img: ["src", "srcset", "data-src", "data-srcset"],
source: ["src", "srcset", "data-src", "data-srcset"],
}
return {
markup: ({ content, filename }) => {
const preparedContent = content
.replace(/<style[^>]*>[\w\W]+<\/style>/g, (match) => " ".repeat(match.length))
.replace(/<script[^>]*>[\w\W]+<\/script>/g, (match) => " ".repeat(match.length))
let ast
try {
ast = svelte.parse(preparedContent)
} catch (e) {
console.error(e, "Error parsing content")
return
}
/** @type {TemplateNode[]} */
const matches = []
svelte.walk(ast, {
enter: (node) => {
if (!["Element", "Fragment", "InlineComponent"].includes(node.type)) {
return
}
if (tags[node.name]) {
matches.push({ node, attributes: tags[node.name] })
}
},
})
const dependencies = []
const code = matches.reduce(
/**
* @param {{content: string, offset: number}} processed
* @param {{node: TemplateNode, attributes: string[]} match
* @param {number} index
*/
(processed, match, index) => {
const attributes = (match.node.attributes || []).filter(
(a) => a.type === "Attribute" && match.attributes.includes(a.name)
)
if (
attributes.length === 0 ||
(match.node.attributes || []).find((a) => a.name === "rel" && a.value[0].data === "external")
) {
return processed
}
let { content, offset } = processed
for (const attribute of attributes) {
if (attribute.value[0]?.type === "Text") {
const value = attribute.value[0]
if (value.data.startsWith("http")) continue
const symbol = `__IMAGE_${index}__`
const replacement = `{${symbol}}`
if (!imports[filename]) imports[filename] = {}
imports[filename][symbol] = value.data
dependencies.push(value.data)
content = content.substring(0, value.start + offset) + replacement + content.substring(value.end + offset)
offset += replacement.length - value.data.length
}
}
return { content, offset }
},
{ content, offset: 0 }
).content
return { code, dependencies }
},
script: ({ content, attributes, filename }) => {
if (!attributes.context) {
const localImports = Object.entries(imports[filename] || {})
if (localImports.length > 0) {
const dependencies = localImports.map(([symbol, path]) => path)
const code =
localImports.map(([symbol, path]) => `import ${symbol} from "${path}"`).join("\n") + "\n" + content
return { code, dependencies }
}
}
},
}
}
module.exports = imagePreprocess
I am interested in creating an image component for svelte but I am not sure how to approach it. We have build time approaches like:
- Svelte preprocessor like
svelte-image
- Vite / Rollup Plugin like
vite-imagetools
A know limitation (matyunya/svelte-image#31, JonasKruckenberg/imagetools#5) of these approaches is that the images cannot be dynamic i.e. inside variables, instead they have to be referenced as string literals for preprocessing to work.
I have a few questions for what would a runtime implementation would look like?
- Where would this code live? In an endpoint?
- Where should generated assets be stored?
- Are there any hooks in svelte kit that we can use to emit generated assets?
- Do we need additional hooks like NextJS
getStaticProps
/getServerSideProps
etc?
Some discussions on withastro/astro#492 that might be related.
I am interested in creating an image component for svelte but I am not sure how to approach it. We have build time approaches like:
- Svelte preprocessor like
svelte-image
- Vite / Rollup Plugin like
vite-imagetools
A know limitation (matyunya/svelte-image#31, JonasKruckenberg/imagetools#5) of these approaches is that the images cannot be dynamic i.e. inside variables, instead they have to be referenced as string literals for preprocessing to work.
I have a few questions for what would a runtime implementation would look like?
- Where would this code live? In an endpoint?
- Where should generated assets be stored?
- Are there any hooks in svelte kit that we can use to emit generated assets?
- Do we need additional hooks like NextJS
getStaticProps
/getServerSideProps
etc?Some discussions on snowpackjs/astro#492 that might be related.
hey, I played a bit with dynamic on demand optimization.
https://github.com/leimantas/svelte-image-caravaggio/blob/main/src/lib/components/Image.svelte
<Image/>
components are nice to add loading transitions which CSS can't yet do. But wrapping each image in such a stateful component harms loading performance. Perhaps such a task is better for a Svelte transition that hooks on image load
event (doesn't exist yet, just an idea):
<img transition:fade src="image.webp" alt="fade in when image loads">
Image processing on localhost becomes a huge build bottleneck when having to process multiple images each time per commit. It's not scalable. The job is better suited from a CMS or serverless function that provides an API similar to imagetools
, and most importantly caches the results:
<img src="image.jpg?w=300" srcset="image.jpg?w=300,image_2x.jpg?w=600 2x" alt="responsive widths from api">
On the other hand, network performance can be improved when one downloads all remotely sourced images and serves them from a single host, because we create fewer new connections. A config option downloadRemoteImages
in svelte.config.js
would suffice, or?
Another room for processing would be "width" and "height" parameters on images. Could also be an option on SvelteKit's config.
I've been working with SvelteKit and have tacked this issue with custom scripts wrapped around the framework. Would be nice to see this stuff get integrated. Perhaps I can upstream my changes if thats okay?
It would be nice for image processing to be available in SSG. I'm trying to switch from Gatsby to SvelteKit and the lack of a working <Image />
tag is the main thing that prevents me from doing so.
@paulkre I also think it would be great to see an image component similar to Gatsby! Here's an attempt at one which uses Vite Imagetools for processing. It's not perfect, and importing image data in pages is definitely verbose, but I think it emulates the Gatsby Image effect relatively well.
I've also put together a starter blog that uses this component and the metadata object in MDSvex to have optimised post thumbnails using the Image Component like you might find in a Gatsby blog.
Thank you everyone for the incredible work on Svelte & SvelteKit!
For anyone looking for a good library until this issue gets resolved: I just discovered https://github.com/xiphux/svimg which is very similar to https://github.com/matyunya/svelte-image but is maintained well and up-to-date :)
Anyone got a working configuration for svimg + Svelte Kit? I tried to set it up, but it doesn't seem to be doing much. It just creates empty folders for the processed image paths, but I see no errors, or really no other clues for what could be going wrong.
svimg does not currently support dynamic images, so maybe you are running into that issue...it seems like image processing is something that kit should probably try to handle, as none of the solutions so far seem to be able to handle dynamic images at all...that might be something that only kit can end up doing...but I'm not sure about that...starting down the path to try and figure that out...
I've looked at all the various image processing solutions and feel that https://github.com/JonasKruckenberg/vite-imagetools is probably the best and have been slowly working to make support for it a bit more official. You can dynamically choose from a list of images with it. But if you want something even more dynamic like handling an image pulled out of a database, that's going to be outside of what would make sense to build into SvelteKit.
Hi ben! XD
Caveat to everything I write...I am an amateur dev with little to no knowledge of build pipelines or how any of vite or kit actually work...no team either...just me...stumbling alone...in the dark :-)
So, I think that maybe this issue is almost 100% of my image use cases, which I am currently using vite-imagetools
for...
but it's...
not ideal...
Let's say I define images in my frontmatter for mdsvex files, like so:
---
title: Notes
subtitle: Every journey begins with a single sound.
portrait_image: /words/notes/portrait_notes.jpg
landscape_image: /words/notes/landscape_notes.jpg
alt: Making notes with a fountain pen.
---
<script context="module">
import NotesPortraitAVIF from '$images/words/notes.jpg?width=240;280;360;480;720;960;1920&format=avif&srcset'
import NotesLandscapeAVIF from '$images/words/notes.jpg?width=480;960;1280;1920;2560;3840&format=avif&srcset'
metadata.srcset_portrait_avif = NotesPortraitAVIF;
metadata.srcset_landscape_avif = NotesLandscapeAVIF;
import NotesPortraitJPG from '$images/words/notes.jpg?width=240;280;360;480;720;960;1920&format=jpg&srcset'
import NotesLandscapeJPG from '$images/words/notes.jpg?width=480;960;1280;1920;2560;3840&format=jpg&srcset'
metadata.srcset_portrait_jpg = NotesPortraitJPG;
metadata.srcset_landscape_jpg = NotesLandscapeJPG;
</script>
...I have to define all of the srcset
s for different image types and crops manually for EVERY mdsvex file, and add them to the metadata in a module script, as you can see from the above code.
...so this sort of works...although I cannot ACTUALLY use AVIF images at all, because netlify will not build the site, because AVIF images take too much processing power and time??? to make...I think...still hazy on that one, tbh...talking to the netlify folks about it more in the near future...
Then this kind of frontmatter would be used in the following way on a svelte page in an [#each}
block:
{#each sortedWords as {
slug,
title,
subtitle,
portrait_image,
landscape_image,
srcset_portrait_avif,
srcset_landscape_avif,
srcset_portrait_jpg,
srcset_landscape_jpg
} }
<Card
{slug}
{title}
{subtitle}
{portrait_image}
{landscape_image}
{srcset_portrait_avif}
{srcset_landscape_avif}
{srcset_portrait_jpg}
{srcset_landscape_jpg}
/>
{/each}
...where the Card
component uses the <picture>
element to use the correct srcset
, among other things.
So, as you can see, I currently have to manually add all this information to the frontmatter of every article, and I can't even use AVIF images at all because they simply stop the build entirely, it would seem.
So ideally I would want to be able to avoid all of this and only define the images in the frontmatter:
---
title: Notes
subtitle: Every journey begins with a single sound.
portrait_image: /words/notes/portrait_notes.jpg
landscape_image: /words/notes/landscape_notes.jpg
alt: Making notes with a fountain pen.
---
...then all this stuff:
'$images/words/notes.jpg?width=480;960;1280;1920;2560;3840&format=jpg&srcset'
for every possible image type and crop, etc...is defined in a config file for images or something...dunno...
Then transforming the images would happen locally and cached when dev
happens so no images would be transformed by the server. Image transforms should also not happen every time a dev
or build
is run, either, as that would take forever and be very wasteful, especially with AVIF image transforms. Only NEW images would be transformed, and ideally only once, or whenever a change is made...some type of diffing system maybe?
The reason I say kit should handle it because only kit and vite know about what would be passed into a prop in a component in an {#each}
block at dev
or build
time...I think...???
BUT...I have no idea how kit or vite work at all, so I have no idea if that is the best idea or even possible, etc...
So, this is what I would like to be able to do...dunno how to go about it or what the best approach might be...maybe kit should NOT do it, as you mentioned, but if so, do you have any thoughts as to how it might be done? I'm just starting to learn about node
and npm
scripts, so maybe that could be a possibility as well...
Anyhoo, I hope this makes sense, and maybe there is a way to solve these issues in some capacity XD
Hey @rchrdnsh, this can be made much nicer with a couple of options. First I suggest using the defaultDirectives
option so that you don't have to specify query parameters for the most part. Here's the vite.config.js
I use in my project:
import * as path from 'node:path';
import { sveltekit } from '@sveltejs/kit/vite';
import { imagetools } from 'vite-imagetools';
const fallback = {
'.heic': 'jpg',
'.heif': 'jpg',
'.avif': 'png',
'.jpeg': 'jpg',
'.jpg': 'jpg',
'.png': 'png',
'.tiff': 'jpg',
'.webp': 'png',
'.gif': 'gif'
};
/** @type {import('vite').UserConfig} */
const config = {
plugins: [
imagetools({
defaultDirectives: (url) => {
const ext = path.extname(url.pathname);
return new URLSearchParams(`format=avif;webp;${fallback[ext]}&as=picture`);
}
}),
sveltekit()
]
};
export default config;
I haven't tried putting images in frontmatter yet, but for my .svelte
files I'm using svelte-preprocess-import-assets
to automatically extract and import image URLs from code like <Image src="./_images/venostent.svg" alt="VenoStent" />
. Then imagetools will automatically create webp and avif versions of the file in this example. Here's an example of setting up the preprocessor:
import sveltePreprocess from 'svelte-preprocess';
import { vitePreprocess } from '@sveltejs/kit/vite';
import { importAssets } from 'svelte-preprocess-import-assets';
/** @type {import('@sveltejs/kit').Config} */
export default {
preprocess: [
importAssets({
sources: (defaultSources) => {
return [
...defaultSources,
{
tag: 'Image',
srcAttributes: ['src']
}
]
}
}),
vitePreprocess()
]
...
My Image
component roughly looks like:
<script>
/** @type {string | import('vite-imagetools').Picture} */
export let src;
/** @type {string} */
export let alt;
</script>
{#if typeof src === 'string'}
<img {src} {alt} {...$$restProps} />
{:else}
<picture>
{#each Object.entries(src.sources) as [format, images]}
<source srcset={images.map((i) => `${i.src} ${i.w}w`).join(', ')} type={'image/' + format} />
{/each}
<img src={src.img.src} width={src.img.w} height={src.img.h} {alt} {...$$restProps} />
</picture>
{/if}
I realize of course this is a fair bit of configuration required at the moment. We've just done new releases of both imagetools
and svelte-preprocess-import-assets
today to make this all work pretty well. The next step will be packaging it all up together to be easier to setup.
Thank you for the reply @benmccann XD
I must admit, however, that i do not really understand any of it ATM...but I will try!
Can't find anything about defaultDirectives
in the imagetools
docs, tho...
But thank you for helping and putting time and effort into this issue :-)
Can't find anything about defaultDirectives in the imagetools docs, tho...
See https://github.com/JonasKruckenberg/imagetools/blob/main/packages/vite/README.md#defaultdirectives
thank you!
trying this all out in a kit project and i'm getting the following error:
supportedExtensions is not defined
ReferenceError: supportedExtensions is not defined
at Object.defaultDirectives (file:///Users/rchrdnsh/Code/Svelte/RYKR-kit/vite.config.js.timestamp-1665513195017.mjs:21:11)
at Context.load (file:///Users/rchrdnsh/Code/Svelte/RYKR-kit/node_modules/vite-imagetools/dist/index.mjs:36:68)
at Object.load (file:///Users/rchrdnsh/Code/Svelte/RYKR-kit/node_modules/vite/dist/node/chunks/dep-6b3a5aff.js:41076:46)
at async loadAndTransform (file:///Users/rchrdnsh/Code/Svelte/RYKR-kit/node_modules/vite/dist/node/chunks/dep-6b3a5aff.js:37304:24)
...not sure what that is or where it should go or where it is coming from...I guess...
this is what my vite config looks like at the moment...I suppose I need to make a supportedExtensions
?
import { sveltekit } from '@sveltejs/kit/vite';
// import { defineConfig } from 'vite';
import { imagetools } from 'vite-imagetools';
import mkcert from 'vite-plugin-mkcert';
/** @type {import('vite').UserConfig} */
const config = {
// server: {
// hmr: false
// },
ssr: {
noExternal: [
'svelte-stripe-js',
'style-value-types',
'popmotion',
'framesync'
]
},
plugins: [
sveltekit(),
// imagetools(),
imagetools(
{
defaultDirectives: (url) => {
const extension = url.pathname.substring(url.pathname.lastIndexOf('.') + 1);
if (supportedExtensions.includes(extension)) {
return new URLSearchParams({
format: 'avif;webp;' + extension,
picture: true
});
}
return new URLSearchParams();
}
}
),
mkcert({hosts: ['localhost', 'app.local']})
]
};
export default config;
I am also interested in the putting images in frontmatter use case and am very pleased by the progress. Thank you Ben and others involved in moving this forward.
EDIT Re:
I realize of course this is a fair bit of configuration required at the moment. We've just done new releases of both
imagetools
andsvelte-preprocess-import-assets
today to make this all work pretty well. The next step will be packaging it all up together to be easier to setup.
β¦Is it worth making a demo of vite-imagetools all hooked up? Or will it get easier quickly enough that it's worth waiting instead of turning Ben's comment into a repo? π€π
oh geez! i must have missed the extensions, my bad @benmccann π¬
const supportedExtensions = ['png', 'jpg', 'jpeg'];
...seems to work now...or, at least...I'm apparently missing images, lol...gonna sort them all out now π
ok, was confused about the hidden messages...i get that you added it now...
ok, so, I'm trying to apply this technique to markdown frontmatter image and I am getting the following error, after it worked ok on the markdown inline images:
Cannot convert undefined or null to object
TypeError: Cannot convert undefined or null to object
at Function.entries (<anonymous>)
at eval (/src/library/images/Image.svelte:21:137)
at Object.$$render (/node_modules/svelte/internal/index.mjs:1771:22)
at eval (/src/routes/words/+page.svelte:48:92)
at Module.each (/node_modules/svelte/internal/index.mjs:1737:16)
at Object.default (/src/routes/words/+page.svelte:46:29)
at eval (/src/library/layout/Grid.svelte:19:74)
at Object.$$render (/node_modules/svelte/internal/index.mjs:1771:22)
at Object.default (/src/routes/words/+page.svelte:43:94)
at eval (/src/library/layout/Container.svelte:41:54)
...dunno what's going on, but maybe because the image file path has not been determined yet at the time the code runs its undefined in the Image
component?
Made an example repo with Imagetools all wired up, no frontmatter example yet, but it's a start and PRs + input very welcome. Uses a NASA pic, hooray public domain.
The vercel branch is the production branch on Vercel, it specifies the Vercel adapter and passes the edge: true
option to the adapter for Edge Functions. Rich covered this at Vite Conf.
Again, any advice or suggestions appreciated.
UPDATE 1, 20 Oct: Now available on Netlify as well, building off the netlify branch. Had to fight with sveltejs/adapter-netlify a bit, ended up downgrading 1.0.0-next.81
to 1.0.0-next.78
to get build and static file serving working in unison (should I file a bug?).
UPDATE 2, 21 Oct: Appears though iOS 16 adds support for AVIF, AVIF images do not currently render in iOS Safari if your phone is in Lockdown Mode. Lockdown Mode can be disabled on a per website basis.
@rdela Thank you for the example repo!
Any hope to see a solution to optimize images in markdown (body and frontmatter)? It's something that Gatsby is doing well and it's missing a bit in SvelteKitβ¦
Is there a way you could import a directory of static images and have svelte/vite batch process them? So rather than having to specific a specific file path in vite-imagetools, you could just specific a folder, or maybe a blob? And then vite-imagetools processes all of them, waits for all the promises to resolve, and returns an array of images?
@elibenton β I've had luck batch importing/glob'ing images with this component. Note that all targeted images live in $lib/assets/picture/*
.
Ideally I could find a way to dynamically import images, though I'm interested in trying Ben's approach above first. Until then I have this component.
<script context="module">
const pictures = import.meta.glob(
'/src/lib/assets/picture/*.{heic,heif,avif,jpg,jpeg,png,tiff,webp,gif}',
{
query: {
format: 'webp;avif;jpg',
width: '300;600;1200',
picture: '',
flatten: '',
background: '#ffffff'
},
import: 'default',
eager: true
}
);
</script>
<script>
/** REQUIRED */
/** @type {String} required */
export let src = undefined;
/** @type {String} required */
export let alt = undefined;
/** OPTIONAL */
/** @type {Boolean} */
export let draggable = false;
/** @type {('sync' | 'async' | 'auto')} */
export let decoding = 'async';
/** @type {('lazy' | 'eager' | 'auto')} */
export let loading = 'lazy';
/** @type {String} */
let classes = '';
export { classes as class };
/** @type {String|{{ fallback: { w: Number, h: Number, src: String }, sources?: Object }}} the selected image */
$: picture = pictures[src];
</script>
<picture>
{#each Object.entries(picture.sources) as [format, images]}
<source srcset={images.map((img) => `${img.src} ${img.w}w`).join(', ')} type="image/{format}" />
{/each}
<img
class={classes}
{decoding}
{loading}
{draggable}
src={picture.fallback.src}
{alt}
width={picture.fallback.w}
height={picture.fallback.h}
/>
</picture>
Hi all, if none of the available options are working for you, and you are looking to:
- batch generate modern responsive image sets (sizes, formats)
- use dynamic filenames
- with low quality image placeholders
- avoid
import.meta.glob
mess - keep images in
static/
instead ofsrc/lib/
- display in svelte/kit
- with cache-busting
- with optional support for image credit metadata
I have written a new tool: web-image-gen.
It does not use the vite pipeline, but plays along nicely with it.
@brev thank you for the ressource! It doesn't seems to parse and optimize markdown images, right?
It seems that this new package will solve all the image optimization issues in an easy and efficient way:
https://github.com/divriots/jampack
It seems that this new package will solve all the image optimization issues in an easy and efficient way: https://github.com/divriots/jampack
This doesn't work for SPAs though right? This requires that your site is static.
@benmccann Would you mind sharing more of your Image component file, more particularly how you're adding different widths?
Would you mind sharing more of your Image component file, more particularly how you're adding different widths?
@djmtype this PR may be of interest, planning to merge these changes from @SirNovi soon with a little documentation rdela/sveltekit-imagetools#1
(Thanks @benmccann and @eur2 for weighing in.)
Thanks @rdela. Does that mean they'll be a drop-in Image/Picture component akin to Astro Imagetools rather than appending options as a query within the script tags?
+1 would love to see this, mainly for automatic WebP. But image optimisations are always good to have and seems to be the biggest thing Page Speed Insights complains about.
For folks who want to use a CDN, there's @unpic/svelte
. It supports Cloudflare and Vercel, so possibly could be a helpful solution for folks using those adapters.
Great contributions above! But broken in the latest Vite Imagetools.
I fixed it up for current versions and added some improvements. Instructions for others:
Vite Imagetools Instructions
Tested with:
"@sveltejs/kit": "^1.5.0",
"vite": "^4.3.0",
"vite-imagetools": "~5.0.4",
- Install using:
npm i -D vite-imagetools
- Update
vite.config.js
to add:
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
+ import { imagetools } from 'vite-imagetools';
export default defineConfig({
plugins: [
+ imagetools({
+ defaultDirectives: new URLSearchParams({
+ format: 'avif;webp',
+ as: 'picture'
+ })
+ }),
sveltekit()
],
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
}
});
- Create this file at
src/lib/image.svelte
:
<script>
/**
* @typedef {Object} Picture
* @property {Object} sources - The object containing different sources of image data.
* @property {Object[]} sources.avif - The array of objects containing source and width for AVIF images.
* @property {string} sources.avif[].src - The source of the AVIF image.
* @property {number} sources.avif[].w - The width of the AVIF image.
* @property {Object[]} sources.webp - The array of objects containing source and width for WebP images.
* @property {string} sources.webp[].src - The source of the WebP image.
* @property {number} sources.webp[].w - The width of the WebP image.
* @property {Object} img - The object containing the default image source.
* @property {string} img.src - The default image source.
* @property {number} img.w - The width of the default image.
* @property {number} img.h - The height of the default image.
*/
/** REQUIRED */
/** @type {Picture} */
export let src;
export let alt = '';
/** OPTIONAL */
/** @type {Boolean} */
export let draggable = false;
/** @type {'async' | 'sync' | 'auto'} */
export let decoding = 'async';
/** @type {'lazy' | 'eager'} */
export let loading = 'lazy';
let classes = '';
export { classes as class };
/** @type {number} */
export let width;
</script>
<picture>
{#each Object.entries(src.sources) as [format, images]}
<source srcset={images.map((i) => `${i.src} ${i.w}w`).join(', ')} type={'image/' + format} />
{/each}
<img
src={src.img.src}
{alt}
class={classes}
{loading}
{decoding}
{draggable}
width={src.img.w}
height={src.img.h}
/>
</picture>
- Add an image at
lib/assets/images/example.jpg
- Use:
<script>
import Image from '$lib/image.svelte';
import example from '$lib/assets/images/example.jpg?w=400';
</script>
<pre>
{JSON.stringify(example, null, 2)}
</pre>
<Image src={example} alt="Ocean Unsplash" />
Known limitations
- Although it "works", without support for pixel density declaration, images look soft. This image component doesn't support pixel density declaration and should, but
vite-imagetools
?density=
property appears broken currently.
Trying to update my image component in the sveltekit config plugins section, like so:
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { imagetools } from 'vite-imagetools';
const supportedExtensions = ['webp', 'jpg', 'jpeg', 'png'];
export default defineConfig({
build: {
target: 'esnext'
},
plugins: [
imagetools({
defaultDirectives: (url) => {
const extension = url.pathname.substring(
url.pathname.lastIndexOf('.') + 1
);
if (supportedExtensions.includes(extension)) {
return new URLSearchParams({
format: `webp;jpg`,
w: `200;300;400;500;800;1000;2000`,
as: `picture`
});
}
return new URLSearchParams();
}
}),
sveltekit(),
]
});
...but I'm currently getting this error:
TypeError: Cannot read properties of undefined (reading 'src')
and here is my Image.svelte
component:
<script>
export let src;
export let alt;
</script>
{#if typeof src === 'string'}
<div class='svg-box'>
<img class='svg' src={src} alt={alt} width={`500`} height={`300`}/>
</div>
{:else if typeof src === 'object'}
<picture class:article={article === true}>
{#if src !== null || src !== undefined}
{#each Object.entries(src.sources) as [format, images]}
<source
srcset={images.map((image) => `${image.src} ${image.w}w`).join(', ')}
type={'image/' + format}
sizes="(orientation: portrait) 80vw, (orientation: landscape) 40vw"
/>
{/each}
<img
class={`${clazz || ''}`}
src={src.fallback.src}
alt={alt}
width={src.fallback.w}
height={src.fallback.h}
/>
{/if}
</picture>
{/if}
...the idea is if the image is an svg then the src
will be a string since svg is not in the supported file types in the config, so it renders the svg without the picture element...
This was all working just fine with the older attributes in vite-imagetools 4.0.19. Just upgraded to 5.0.X and it broke...
...not really understanding what else to change here to get it to work....
@rchrdnsh it's now img
rather than fallback
. If you use the types from vite-imagetools
that should help as well
I've kept my comment above up-to-date if you'd like an example: #241 (comment)
haha! works now, thank you @benmccann! XD
- Although it "works", without support for pixel density declaration, images look soft. This image component doesn't support pixel density declaration and should, but
vite-imagetools
?density=
property appears broken currently.
Thanks for testing it out and sharing the feedback. There was never such a directive in vite-imagetools
, but I agree there should be. I just added a new directive (with a different name) and will use it in #10788
@sveltejs/enhanced-img
is now available to help optimized local images contained within your project. Read more at https://kit.svelte.dev/docs/images
I'll leave this issue open for a little while longer as we consider support for image CDNs (#10323)
@benmccann @sveltejs/enhanced-img
seems to address @djmtypeβs question, any reason I shouldnβt update rdela/sveltekit-imagetools to use that? Or do you want to PR some changes? Is there a complete example anywhere other than the docs currently?
I think rdela/sveltekit-imagetools could probably be retired now and if there's any need for additional documentation we should try to update the official docs
Does anyone else think having a complete example in a repo anywhere is helpful?
UPDATE 2023-12-08: RETIRED
How might this work with divs using images via backgorund-image: url()
?
How might this work with divs using images via
backgorund-image: url()
?
Probably your best bet is to avoid using CSS background images if you can, and instead make a child element that is absolutely positioned and the same size as the parent, but have the "replaced content" (the actual "image" contained in the <img>
tag) be object-fit: cover
... There are a number of methods to get the child element to match the parent width/height when using position: absolute
, something like inset: 0; width: 100%; height: 100%
and CSS like that.
For a fixed background image covering the whole page I have this at the beginning of +layout.svelte
:
<script>
import Srcset from '$lib/components/image/Srcset.svelte';
</script>
<Srcset
alt=""
width="2400"
height="1500"
lazy={false}
quality={80}
draggable="false"
fetchpriority="high"
class="unselectable"
src="/images/background/vaneer.jpg"
sizes="max(100vw, calc(100vh/(1500/2400)))"
style="position: fixed; width: 100vw; height: 100vh; object-fit: cover; object-position: top center"
/>
<!--
Note: you may want to change sizes to something more like sizes="max(100vw, 100vh)" as most portrait
screens will have a DPR around 2.0 which means they will request this specified "size" times 2... and
100vh * 2 happens to be a pretty good middle ground for most mobile screens... so taking the aspect
ratio into consideration here is not so helpful as 1.0 DPR desktop devices are usually landscape... and
100vw would be a rather small image on mobile but filling 100% height would be too much when * 2...
Just want to illustrate the point that there is often more to the decision than just setting 100vw and you
will see that in Lighthouse scores saying potential savings of some KiB... For instance you may want to
take into consideration your website margins at certain breakpoints that reduce the image rendered size
to less than 100vw. [Perhaps calc(50vh/(1500/2400)) is even better; anticipating the doubling on mobile]
-->
Where Srcset
pumps out an <img>
tag that has srcset
entries at various widths pointing to a CDN.
However, this won't work if you need a repeating background image. The best you can do then is to use image-set. You will probably want to use preload in addition if you decide to use image-set
.
I suppose it depends on your design requirements, but I see a lot of nice results using just CSS gradients or even canvas animations. I think the general consensus is avoid CSS backgrounds if you can, as they currently aren't very flexible and when the CSS object model is being built is not an optimal time to begin downloading images.
I made a Image proxy that uses sharp. https://github.com/EmergencyBucket/svelte-sharp-image. It is pretty similar to NextJS's image tools. I would try to make a pr to add this to sveltekit but I'm not entirely sure how sveltekit works.