Skip to content
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

feat: add experimental @sveltejs/image package #9787

Closed
wants to merge 67 commits into from
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
9d83a60
wip
dummdidumm Apr 25, 2023
5716ed4
tweak API, add adapter config API, make kit aware of image plugin
dummdidumm Apr 26, 2023
fab9aec
add build time img optimization through ?generate, add deviceSizes/im…
dummdidumm Apr 26, 2023
8eac4b5
providerOptions, loading, priority
dummdidumm Apr 27, 2023
f29c9f6
more providers
dummdidumm Apr 27, 2023
7b2d891
fetchpriority
dummdidumm Apr 27, 2023
59a528d
remove any kit/image/adapter interop for now until we have a better p…
dummdidumm Apr 27, 2023
9971ab7
typings, loading fix
dummdidumm Apr 27, 2023
1a16981
readme, changeset
dummdidumm Apr 27, 2023
3af5be8
delete playground
dummdidumm Apr 27, 2023
400942d
use unreleased vite-imagetools and use @sveltejs/image on kit.svelte.dev
benmccann May 1, 2023
778fc7e
merge master
benmccann May 1, 2023
73cdf53
format
benmccann May 1, 2023
49c06cb
fix image height
benmccann May 1, 2023
cbc25b2
cleanup
benmccann May 1, 2023
c0a6eec
support picture tag
benmccann May 1, 2023
5a68b43
cleanup
benmccann May 1, 2023
2cdb3bf
simplify
benmccann May 1, 2023
7c7b2a6
remove comment. updated types cover it now
benmccann May 2, 2023
f3bb601
Update packages/image/src/Image.svelte
dummdidumm May 2, 2023
b5bbb13
Update packages/image/README.md
dummdidumm May 2, 2023
d0e30bd
Apply suggestions from code review
dummdidumm May 2, 2023
bfb7068
rename from vitePluginSvelteImage to images
benmccann May 2, 2023
d23bd24
merge master
benmccann May 2, 2023
4293d7c
update todo
benmccann May 2, 2023
0e07ee2
remove ambient.d.ts
benmccann May 2, 2023
81968ae
use vite-imagetools types
benmccann May 2, 2023
c8afcf8
vite-imagetools 5.0!
benmccann May 2, 2023
b2a288b
fix type
benmccann May 2, 2023
16783a3
fix type checking error
benmccann May 2, 2023
e1c89be
add optional dimensions to type
benmccann May 2, 2023
0b3105b
Make vite-imagetools an optional peer dependency
benmccann May 2, 2023
8040287
format
benmccann May 2, 2023
aef41b1
cleanup height styling
benmccann May 2, 2023
438c64d
oops. add back accidentally removed devDeps
benmccann May 2, 2023
a54237b
stupid typescript
benmccann May 2, 2023
4aa0891
tests? we don't need no stinkin tests!
benmccann May 2, 2023
fab06e5
update warning
benmccann May 2, 2023
463a9da
remove unused config functionality
benmccann May 2, 2023
f83793b
virtual module convention
benmccann May 2, 2023
c4a9fa8
remove dead import
benmccann May 2, 2023
c50c3cf
remove engines
benmccann May 2, 2023
9725200
update warnings
benmccann May 2, 2023
a13ff82
update test
benmccann May 2, 2023
c6f1695
fix code and improve error message displayed to client
benmccann May 3, 2023
8e02170
fix unit test
benmccann May 3, 2023
cd72904
Image preprocessor
benmccann May 3, 2023
3593bca
format
benmccann May 3, 2023
a1eb549
Apply suggestions from code review
dummdidumm May 3, 2023
f991577
jsdoc
dummdidumm May 3, 2023
12ed8a9
docs
dummdidumm May 3, 2023
ccd4e2b
speedup+guard by checking for @sveltejs/image string first
dummdidumm May 3, 2023
7412d29
formats option, custom directive to generate exactly what we want
dummdidumm May 3, 2023
3c4fa50
dev time warnings
dummdidumm May 3, 2023
848b450
kit changeset, readme/type tweaks, fix test
dummdidumm May 3, 2023
6e2168a
generate png as fallback, don't upscale
dummdidumm May 3, 2023
365252f
adjust config structure, add sizes to build config
dummdidumm May 3, 2023
959180e
virtual modifier
dummdidumm May 3, 2023
1bbe9bd
Update packages/image/README.md
dummdidumm May 3, 2023
77e7349
note existing image imports caveat
dummdidumm May 3, 2023
ea3cb41
fix fallback src computation
dummdidumm May 3, 2023
fe130a2
domain note
dummdidumm May 3, 2023
81fbbde
use upstream imagetools
benmccann May 3, 2023
afab1fb
Merge branch 'image-experiments' of github.com:sveltejs/kit into imag…
benmccann May 3, 2023
27a9e1e
remove unused vars
benmccann May 3, 2023
a2c8f4f
merge master
benmccann Sep 26, 2023
6bca660
cleanup
benmccann Sep 26, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/little-cameras-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/image': minor
---

feat: add experimental `@sveltejs/image` package
11 changes: 10 additions & 1 deletion packages/adapter-vercel/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { Adapter } from '@sveltejs/kit';
import './ambient.js';

export default function plugin(config?: Config): Adapter;
export default function plugin(
config?: Config & {
/**
* Enable or disable Vercel's image optimization
* https://vercel.com/docs/concepts/image-optimization
* @default false
*/
images?: boolean | { domains?: string[] };
}
): Adapter;

export interface ServerlessConfig {
/**
Expand Down
24 changes: 20 additions & 4 deletions packages/adapter-vercel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import esbuild from 'esbuild';
import { get_pathname } from './utils.js';

const VALID_RUNTIMES = ['edge', 'nodejs16.x', 'nodejs18.x'];

const DEFAULT_FUNCTION_NAME = 'fn';

const get_default_runtime = () => {
Expand Down Expand Up @@ -55,7 +54,7 @@ const plugin = function (defaults = {}) {
functions: `${dir}/functions`
};

const static_config = static_vercel_config(builder);
const static_config = await static_vercel_config(builder, defaults);

builder.log.minor('Generating serverless function...');

Expand Down Expand Up @@ -373,8 +372,11 @@ function write(file, data) {
}

// This function is duplicated in adapter-static
/** @param {import('@sveltejs/kit').Builder} builder */
function static_vercel_config(builder) {
/**
* @param {import('@sveltejs/kit').Builder} builder
* @param {Parameters<import('.').default>[0]} config
*/
async function static_vercel_config(builder, config) {
/** @type {any[]} */
const prerendered_redirects = [];

Expand Down Expand Up @@ -412,8 +414,22 @@ function static_vercel_config(builder) {
overrides[page.file] = { path: overrides_path };
}

/** @type {Record<string, any> | undefined} */
let images = undefined;
if (config.images) {
images = {
// TODO this is duplicated in @sveltejs/image -> figure out a good way to keep them in sync / make them configurable
sizes: [64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840],
domains: Array.isArray(config.images?.domains) ? config.images.domains : [],
// TODO should we expose the following and some other optional options through the adapter?
formats: ['image/avif', 'image/webp'],
minimumCacheTTL: 300
};
}
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved

return {
version: 3,
images,
routes: [
...prerendered_redirects,
{
Expand Down
1 change: 1 addition & 0 deletions packages/image/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @sveltejs/image
108 changes: 108 additions & 0 deletions packages/image/README.md
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.
Show resolved Hide 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.
Show resolved Hide resolved

## 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.
Show resolved Hide resolved
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.
Show resolved Hide resolved

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 })
Copy link
Member

Choose a reason for hiding this comment

The 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 @sveltejs/image installed?

Copy link
Member Author

Choose a reason for hiding this comment

The 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" />
Copy link
Member

@benmccann benmccann May 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cannot be cached. If you import it then Vite will compute a hash giving it a unique URL that you can set far forwards expires headers one. Importing also allows Vite to inline small images. I think we should still encourage images to be imported for dynamic CDNs.

You could set a project-level default for static vs dynamic and then opt-in/out with a query parameter that triggers the include/exclude of vite-imagetools

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

The 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 'cache-control': 'public, immutable, max-age=31536000'

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we talked this morning you said /path/to/your/image.jpg is something that would probably not exist locally. Do we need some documentation around how to deploy it? Or maybe some best practices like creating an /immutable path that can be cached and images outside of that would not be cached or something? Maybe with examples for the built-in providers?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I follow. What I meant is that if your src path starts with a / or with http(s):// it indicates that you're not importing some local image file on disk but instead want to reference something through the URL. What that means with regards to caching depends on where the image is stored. I wouldn't worry about that for now, we can document that later (since it's only a documentation thing, not an implementation issue).

Copy link
Member

@dominikg dominikg May 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using /favicon.ico resolves to /static/favicon.ico locally. so it could be confusing if some paths lead to local sources and others to a cdn url. Would an alias work? eg. $cdn/some-image.png where $cdn is a configured value from svelte/image/vite ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

problem solved

Suggested change
<Image src="/path/to/your/image.jpg" width={1200} height={1800} alt="An alt text" />
<Image src="https://example.com/path/to/your/image.jpg" width={1200} height={1800} alt="An alt text" />

```

`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.
Show resolved Hide resolved

### Static build time optimization:

```svelte
<script>
import Image from '@sveltejs/image';
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved
import YourImage from '/path/to/your/image.jpg?generate';
benmccann marked this conversation as resolved.
Show resolved Hide resolved
</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.
Show resolved Hide resolved

### Pros/Cons of the solutions
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved

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.
Show resolved Hide resolved

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.
Show resolved Hide resolved

You can mix and match both solutions in one project.

### `Image` component options
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved

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.
Show resolved Hide resolved
- `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
Copy link
Member

Choose a reason for hiding this comment

The 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>

Copy link
Member Author

@dummdidumm dummdidumm May 3, 2023

Choose a reason for hiding this comment

The 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..?
Other cons: If you refactor from one to the other it's more stuff to do for you with the getImage version

Copy link
Member

Choose a reason for hiding this comment

The 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 smoothload action, and I can't, and it just feels like this would be so much simpler than dealing with a component:

<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 <img> and <picture> based on factors that I don't fully understand, etc.

Maybe our mistake was trying to solve build time and runtime optimizations in the same thing?

Copy link
Member

Choose a reason for hiding this comment

The 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 <img> as part of the solution)

Copy link
Member

Choose a reason for hiding this comment

The 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 as part of the solution

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

Copy link
Member

Choose a reason for hiding this comment

The 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 src folder in the first place (except maybe for some very early prototyping that you later replace with real data, which isn't the same as 'outgrowing')

Choose a reason for hiding this comment

The 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.
benmccann marked this conversation as resolved.
Show resolved Hide resolved
84 changes: 84 additions & 0 deletions packages/image/package.json
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.
Show resolved Hide resolved
"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.
Show resolved Hide resolved
},
"devDependencies": {
"@playwright/test": "1.30.0",
"@types/node": "^16.18.6",
"@sveltejs/kit": "workspace:^",
"rollup": "^3.7.0",
benmccann marked this conversation as resolved.
Show resolved Hide resolved
"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"
}
}
Loading