-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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 10 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,108 @@ | ||||||
| # `@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.
|
||||||
|
|
||||||
| This package aims to bring a plug&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 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. | ||||||
|
dummdidumm marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| ## Setup | ||||||
|
|
||||||
| Install: | ||||||
|
|
||||||
| ```bash | ||||||
| npm install --save @sveltejs/image | ||||||
| ``` | ||||||
|
|
||||||
| Adjust `vite.config.js`: | ||||||
|
|
||||||
| ```diff | ||||||
| +import { vitePluginSvelteImage } from '@sveltejs/image/vite'; | ||||||
| import { sveltekit } from '@sveltejs/kit/vite'; | ||||||
| import { defineConfig } from 'vite'; | ||||||
|
|
||||||
| export default defineConfig({ | ||||||
| plugins: [ | ||||||
| + vitePluginSvelteImage({ providers: { default: '@sveltejs/image/providers/<choose one>' } }), | ||||||
|
benmccann marked this conversation as resolved.
Outdated
|
||||||
| sveltekit() | ||||||
| ] | ||||||
| }); | ||||||
| ``` | ||||||
|
|
||||||
| > `<choose one>` refers to chosing one of the ready-to-use providers. We plan to add more providers over time. You can create your own by creating a JavaScript with a `export function getURL({ src, width, height }): string` function inside. | ||||||
|
dummdidumm marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| In case of Vercel, adjust `svelte.config.js`: | ||||||
|
|
||||||
| ```diff | ||||||
| import adapter from '@sveltejs/adapter-vercel'; | ||||||
|
|
||||||
| /** @type {import('@sveltejs/kit').Config} */ | ||||||
| const config = { | ||||||
| kit: { | ||||||
| - adapter: adapter() | ||||||
| + adapter: adapter({ images: true }) | ||||||
|
Member
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
Member
Author
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 | ||||||
|
|
||||||
| ### When 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" /> | ||||||
|
Member
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
Member
Author
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
Member
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
Member
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
Member
Author
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
Contributor
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
Member
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 natural width/height of the referenced image. `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`. | ||||||
|
dummdidumm marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| ### Static build time optimization: | ||||||
|
|
||||||
| ```svelte | ||||||
| <script> | ||||||
| import Image from '@sveltejs/image'; | ||||||
|
dummdidumm marked this conversation as resolved.
Outdated
|
||||||
| import YourImage from '/path/to/your/image.jpg?generate'; | ||||||
|
benmccann marked this conversation as resolved.
Outdated
|
||||||
| </script> | ||||||
|
|
||||||
| <Image src={YourImage} alt="An alt text" /> | ||||||
| ``` | ||||||
|
|
||||||
| This optimizes the image at build time using `vite-imagetools`. `width` and `height` are optional as they can be infered from the source image. | ||||||
|
dummdidumm marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| ### Pros/Cons of the solutions | ||||||
|
dummdidumm marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| Using the static provider generates the images at build time, so that build time may take longer the more images you transform. | ||||||
|
dummdidumm marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| 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 setup overhead (configuring the image CDN) and possibly usage cost. | ||||||
|
dummdidumm marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| You can mix and match both solutions in one project. | ||||||
|
|
||||||
| ### `Image` component options | ||||||
|
dummdidumm marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| There are a few things you can customize: | ||||||
|
|
||||||
| - `priority`: give this to the most important/largest image on the page so it loads faster | ||||||
|
dummdidumm marked this conversation as resolved.
Outdated
|
||||||
| - `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
Member
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>
Member
Author
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..?
Member
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?
Member
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
Member
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
Member
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.
|
||||||
|
|
||||||
| 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. | ||||||
|
benmccann marked this conversation as resolved.
Outdated
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| { | ||
| "name": "@sveltejs/image", | ||
| "version": "0.1.0", | ||
| "description": "Image optimization for your Svelte apps", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/sveltejs/image", | ||
|
dummdidumm marked this conversation as resolved.
Outdated
|
||
| "directory": "packages/image" | ||
| }, | ||
| "license": "MIT", | ||
| "homepage": "https://kit.svelte.dev", | ||
| "type": "module", | ||
| "dependencies": { | ||
| "esm-env": "^1.0.0", | ||
| "vite-imagetools": "^4.0.19" | ||
|
benmccann marked this conversation as resolved.
Outdated
|
||
| }, | ||
| "devDependencies": { | ||
| "@playwright/test": "1.30.0", | ||
| "@types/node": "^16.18.6", | ||
| "@sveltejs/kit": "workspace:^", | ||
| "rollup": "^3.7.0", | ||
|
benmccann marked this conversation as resolved.
Outdated
|
||
| "svelte": "^3.56.0", | ||
| "svelte-preprocess": "^5.0.0", | ||
| "typescript": "^4.9.4", | ||
| "uvu": "^0.5.6", | ||
| "vite": "^4.3.0" | ||
| }, | ||
| "peerDependencies": { | ||
| "svelte": "^3.54.0", | ||
| "vite": "^4.0.0" | ||
| }, | ||
| "files": [ | ||
| "src", | ||
| "!src/**/*.spec.js", | ||
| "types" | ||
| ], | ||
| "scripts": { | ||
| "lint": "prettier --check . --config ../../.prettierrc --ignore-path .gitignore", | ||
| "check": "tsc", | ||
| "check:all": "tsc && pnpm -r --filter=\"./**\" check", | ||
| "format": "prettier --write . --config ../../.prettierrc --ignore-path .gitignore", | ||
| "test": "pnpm test:unit && pnpm test:integration", | ||
| "test:integration": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test", | ||
| "test:cross-platform:dev": "pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:cross-platform:dev", | ||
| "test:cross-platform:build": "pnpm test:unit && pnpm -r --workspace-concurrency 1 --filter=\"./test/**\" test:cross-platform:build", | ||
| "test:unit": "uvu src \"(spec\\.js|test[\\\\/]index\\.js)\"" | ||
| }, | ||
| "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" | ||
| ] | ||
| } | ||
| }, | ||
| "engines": { | ||
| "node": "^16.14 || >=18" | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.