-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat: add experimental @sveltejs/image
package
#9787
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9d83a60
5716ed4
fab9aec
8eac4b5
f29c9f6
7b2d891
59a528d
9971ab7
1a16981
3af5be8
400942d
778fc7e
73cdf53
49c06cb
cbc25b2
c0a6eec
5a68b43
2cdb3bf
7c7b2a6
f3bb601
b5bbb13
d0e30bd
bfb7068
d23bd24
4293d7c
0e07ee2
81968ae
c8afcf8
b2a288b
16783a3
e1c89be
0b3105b
8040287
aef41b1
438c64d
a54237b
4aa0891
fab06e5
463a9da
f83793b
c4a9fa8
c50c3cf
9725200
a13ff82
c6f1695
8e02170
cd72904
3593bca
a1eb549
f991577
12ed8a9
ccd4e2b
7412d29
3c4fa50
848b450
6e2168a
365252f
959180e
1bbe9bd
77e7349
ea3cb41
fe130a2
81fbbde
afab1fb
27a9e1e
a2c8f4f
6bca660
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@sveltejs/image': minor | ||
--- | ||
|
||
feat: add experimental `@sveltejs/image` package |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# @sveltejs/image |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,140 @@ | ||||||
# `@sveltejs/image` | ||||||
|
||||||
**WARNING**: This package is experimental. It uses pre-1.0 versioning and may introduce breaking changes with every minor version release. | ||||||
benmccann marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
This package aims to bring a plug and play image component to SvelteKit that is opinionated enough so you don't have to worry about the details, yet flexible enough for more advanced use cases or tweaks. It serves a smaller file format like `avif` or `webp` and uses the `srcset` and `sizes` attributes of the `img` tag to provide resized images suitable for various device sizes, which for example results in smaller images downloaded for mobile. | ||||||
|
||||||
## Setup | ||||||
|
||||||
Install: | ||||||
|
||||||
```bash | ||||||
npm install --save @sveltejs/image | ||||||
``` | ||||||
|
||||||
Adjust `vite.config.js`: | ||||||
|
||||||
```diff | ||||||
+import { images } from '@sveltejs/image/vite'; | ||||||
import { sveltekit } from '@sveltejs/kit/vite'; | ||||||
import { defineConfig } from 'vite'; | ||||||
|
||||||
export default defineConfig({ | ||||||
plugins: [ | ||||||
+ images({ | ||||||
runtime: { | ||||||
providers: { | ||||||
default: '@sveltejs/image/providers/<choose one>' | ||||||
} | ||||||
} | ||||||
}), | ||||||
sveltekit() | ||||||
] | ||||||
}); | ||||||
``` | ||||||
|
||||||
> `<choose one>` refers to choosing one of the ready-to-use providers. We plan to add more providers over time. You can create your own by creating a JavaScript file with a `export function getURL({ src, width, height }): string` function inside and then pointing to that file in the config. | ||||||
|
||||||
In case of Vercel, also adjust your `svelte.config.js`: | ||||||
|
||||||
```diff | ||||||
import adapter from '@sveltejs/adapter-vercel'; | ||||||
|
||||||
/** @type {import('@sveltejs/kit').Config} */ | ||||||
const config = { | ||||||
kit: { | ||||||
- adapter: adapter() | ||||||
+ adapter: adapter({ images: true }) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there a reason not to enable this by default? will anything bad happen if you do and don't have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honestly I don't know, but it feels wrong to automatically enable it. |
||||||
} | ||||||
}; | ||||||
|
||||||
export default config; | ||||||
``` | ||||||
|
||||||
## Usage | ||||||
|
||||||
### Dynamic runtime optimization using one of the providers, i.e. an image CDN: | ||||||
|
||||||
```svelte | ||||||
<script> | ||||||
import Image from '@sveltejs/image'; | ||||||
</script> | ||||||
|
||||||
<Image src="/path/to/your/image.jpg" width={1200} height={1800} alt="An alt text" /> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This cannot be cached. If you You could set a project-level default for static vs dynamic and then opt-in/out with a query parameter that triggers the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ".. that you can set far forwards expires headers one" - I don't understand what you mean by that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "one" was a typo and was supposed to be "on". What I mean is setting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When we talked this morning you said There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure I follow. What I meant is that if your There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. problem solved
Suggested change
|
||||||
``` | ||||||
|
||||||
`width` and `height` should be the intrinsic width/height of the referenced image - i.e. the dimensions of the image before styling. `alt` should describe the image. All are required. The `src` is transformed by calling `getURL` of the `default` provider provided in the `vite.config.js`. | ||||||
|
||||||
If you want to optimize an image from an external URL, add its domain to the `domains` config of the plugin. Note that you may also add a corresponding config to the image cdn (how exactly depends on the provider). | ||||||
|
||||||
### Static build time optimization: | ||||||
|
||||||
Static build time optimization uses `vite-imagetools`, which comes as an optional peer dependency, so you first need to install it: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we probably need some docs in this section about how installing import logo from '$lib/icons/logo.avif'; <img alt="logo" src={logo} /> ...and it broke. (Ideally it wouldn't break in the first place, i.e. you'd have to explicitly opt in to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can also make this configurable opt-in (or opt-out) by doing |
||||||
|
||||||
```bash | ||||||
npm install --save-dev vite-imagetools | ||||||
``` | ||||||
|
||||||
> If you have existing image imports like `import SomeImage from './some/image.jpg';` they will be treated differently now. If you want to get back the previous behavior of this import returning a URL string, add `?url` to the end of the import. | ||||||
|
||||||
Use in your `.svelte` components: | ||||||
|
||||||
```svelte | ||||||
<script> | ||||||
import { Image } from '@sveltejs/image'; | ||||||
</script> | ||||||
|
||||||
<Image src="./path/to/your/image.jpg" alt="An alt text" /> | ||||||
``` | ||||||
|
||||||
This optimizes the image at build time using `vite-imagetools`. `width` and `height` are optional as they can be inferred from the source image. | ||||||
|
||||||
Note that the `src` prop must start with a `.`. `@sveltejs/image` includes a preprocessor that will convert it to the following: | ||||||
|
||||||
```svelte | ||||||
<script> | ||||||
import { Image } from '@sveltejs/image'; | ||||||
import YourImage from './path/to/your/image.jpg'; | ||||||
</script> | ||||||
|
||||||
<Image src={YourImage} alt="An alt text" /> | ||||||
``` | ||||||
|
||||||
You can also write it yourself like this, if you want to. | ||||||
|
||||||
Note that the generated code is a `picture` tag (not an `img` tag as in the case of dynamic providers) wrapping one `source` tag per image type. | ||||||
|
||||||
### Choosing static vs dynamic | ||||||
|
||||||
Using the static provider generates the images at build time, so build time may take longer the more images you transform. | ||||||
|
||||||
Using an image CDN provides more flexibility with regards to sizes and you can pass image sources not known at build time, but it comes with potentially a bit of setup overhead (configuring the image CDN) and possibly usage cost. | ||||||
|
||||||
You can mix and match both solutions in one project. | ||||||
|
||||||
### `Image` component props | ||||||
|
||||||
There are a few things you can customize: | ||||||
|
||||||
- `priority`: set this to `true` for the most important/largest image on the page so it loads faster | ||||||
- `sizes`: If your image is less than full width on one or more screen sizes, add this info here. When using dynamic providers the widths can be adjusted accordingly to produce a more optimal `srcset`. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/sizes) for more info on the attribute | ||||||
- `style`: to style the image | ||||||
- `class`: to style the image. Be aware that you need to pass classes that are global (i.e. wrapped in `:global()` when coming from a `<style>` tag) | ||||||
Comment on lines
+121
to
+122
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure how I feel about this... I kinda feel like things like this would be better expressed using slots, so we can also use events, actions etc <Image {src} {width} {height} {alt} let:props>
<img class="banana" use:smoothload on:click={blah} {...props} />
</Image> There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something like <img {...getImage({ src, width, height })} .. /> feels better to me at first. But it probably does not work with the static import preprocessor then..? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah... this is a super shitty thing to say after so much work has gone into the component so far, but I'm using it and my main thought is why is this a component?. I want to use the <script lang="ts">
+ import { optimize } from '$lib/image';
import { smoothload } from '$lib/actions';
import type { Photo } from '$lib/types';
export let photo: Photo;
</script>
<div
class="block relative bg-slate-100 w-full rounded-md overflow-hidden shadow-xl"
style="aspect-ratio: {photo.width / photo.height}"
>
{#key photo}
<img
class="absolute left-0 top-0 w-full h-full"
- src={photo.url}
+ srcset={optimize(photo.url)}
alt={photo.description}
use:smoothload
/>
{/key}
</div> There's no magic, it's way easier to understand what's happening, it's not switching between Maybe our mistake was trying to solve build time and runtime optimizations in the same thing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (I don't think refactoring from one to the other is something we need to worry about — it's very unlikely that you'd need to do that, because they're really just different problems that happen to both involve an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think a common case is that users will start with the build-time solution and then outgrow it and move to a CDN. The build-time solution is much easier to setup - it doesn't require setting up a CDN account, etc. But as sites grow I imagine many people will end up migrating to a CDN There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Build time to me means things like icons; run time means things like product shots, which you'd never have in your There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For a user perspective (indie SaaS & small startups), I'd prefer to stick with build-time generated images as long as practical speed wise--which could be a while for an indie SaaS with 3-4 marketing pages and low-volume blog. Being build-time static means 1.) fewer knobs to get off the ground, and 2.) better guarantees that a DDoS will cost nothing on hosts that provide zero-cost static file serving, which wouldn't be the case with a misconfigured cache, etc. A couple reasons to stay static as long as practical for some devs. |
||||||
- `loading`: loading behavior of the image. Defaults to `lazy` which means it's loaded only when about to enter the viewport. Set to `eager` to load right away (the default when setting `priority`) | ||||||
- `provider`: you can pass more than the `default` provider in `vite.config.js`. If you then want to use a different provider than the `default` one, pass it here | ||||||
- `providerOptions`: provider-specific image CDN options. For example `quality` for Vercel | ||||||
|
||||||
## Best practices | ||||||
|
||||||
- Always provide a good `alt` text | ||||||
- Your original images should have a good quality/resolution. Images are easier to size down than up | ||||||
- Choose one image per page which is the most important/largest one and give it `priority` so it loads faster. This gives you better web vitals scores (largest contentful paint in particular) | ||||||
- Give the image a container or a styling so that it is constrained and does not jump around. `width` and `height` help the browser reserving space while the image is still loading | ||||||
|
||||||
## Roadmap | ||||||
benmccann marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
This is an experimental MVP for getting initial feedback on the implementation/usability of an image component usable with SvelteKit (can also be used with Vite only). Once the API is stable, we'll want to create a more seamless integration with SvelteKit, i.e. less setup required. | ||||||
|
||||||
## Acknowledgements | ||||||
|
||||||
We'd like to thank the authors of the Next/Nuxt/Astro/`unpic` image components for inspiring this work. We'd also like to thank the authors of `vite-imagetools` and `svelte-preprocess-import-assets` which are used in `@sveltejs/image`. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
{ | ||
"name": "@sveltejs/image", | ||
"version": "0.1.0", | ||
"description": "Image optimization for your Svelte apps", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/sveltejs/kit", | ||
"directory": "packages/image" | ||
}, | ||
"license": "MIT", | ||
"homepage": "https://kit.svelte.dev", | ||
"type": "module", | ||
"dependencies": { | ||
"esm-env": "^1.0.0", | ||
"svelte-preprocess-import-assets": "^1.0.0" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^16.18.6", | ||
"svelte": "^3.56.0", | ||
"typescript": "^4.9.4", | ||
"vite": "^4.3.0", | ||
"vite-imagetools": "^5.0.2" | ||
}, | ||
"peerDependencies": { | ||
"vite-imagetools": "^5.0.2" | ||
}, | ||
"peerDependenciesMeta": { | ||
"vite-imagetools": { | ||
"optional": true | ||
} | ||
}, | ||
"files": [ | ||
"src", | ||
"!src/**/*.spec.js", | ||
"types" | ||
], | ||
"scripts": { | ||
"lint": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore", | ||
"check": "tsc", | ||
"format": "prettier --write . --config ../../.prettierrc --ignore-path .gitignore" | ||
}, | ||
"exports": { | ||
"./package.json": "./package.json", | ||
".": { | ||
"types": "./types/index.d.ts", | ||
"import": "./src/index.js" | ||
}, | ||
"./vite": { | ||
"import": "./src/vite-plugin.js" | ||
}, | ||
"./providers/cloudflare": { | ||
"import": "./src/providers/cloudflare.js" | ||
}, | ||
"./providers/netlify": { | ||
"import": "./src/providers/netlify.js" | ||
}, | ||
"./providers/vercel": { | ||
"import": "./src/providers/vercel.js" | ||
}, | ||
"./providers/none": { | ||
"import": "./src/providers/none.js" | ||
} | ||
}, | ||
"types": "types/index.d.ts", | ||
"typesVersions": { | ||
"*": { | ||
"index": [ | ||
"types/index.d.ts" | ||
], | ||
"vite": [ | ||
"types/vite.d.ts" | ||
] | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.