-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
feat: add dynamic image optimization #10323
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 2 commits
a4d5eca
3bf5d7f
7e258fe
7855995
8c6eeef
1a9a930
a22daf0
a6fe421
987eef7
c761719
30d1fd9
e2a3ae6
d5b3f18
09e3ad2
5f4ae65
92851ce
1ad9e80
940cfaa
aab14c0
7cad350
655953a
906a194
0252485
4695a6e
3c2063e
eec30ae
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,4 @@ | ||
| /** | ||
| * https://vercel.com/docs/concepts/image-optimization | ||
| */ | ||
| export default function loader(src: string, width: number, options?: { quality?: number }): string; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| // https://vercel.com/docs/concepts/image-optimization | ||
|
|
||
| /** | ||
| * @param {string} src | ||
| * @param {number} width | ||
| * @param {{ quality?: number }} [options] | ||
| */ | ||
| export default function loader(src, width, options) { | ||
|
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. it'd probably make sense to either generate the types with |
||
| const url = new URL(src, 'http://n'); // If the base is a relative URL, we need to add a dummy host to the URL | ||
| if (url.pathname === '/_vercel/image') { | ||
| set_param(url, 'w', width); | ||
| set_param(url, 'q', options?.quality ?? 75, false); | ||
|
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 is rather surprising to me. I'd expect if the |
||
| } else { | ||
| url.pathname = `/_vercel/image`; | ||
|
benmccann marked this conversation as resolved.
Outdated
|
||
| set_param(url, 'url', src); | ||
| set_param(url, 'w', width); | ||
| set_param(url, 'q', options?.quality ?? 75); | ||
| } | ||
| return src === url.href ? url.href : relative_url(url); | ||
| } | ||
|
|
||
| /** | ||
| * @param {URL} url | ||
| */ | ||
| function relative_url(url) { | ||
| const { pathname, search } = url; | ||
| return `${pathname}${search}`; | ||
| } | ||
| /** | ||
| * @param {URL} url | ||
| * @param {string} param | ||
| * @param {any} value | ||
| * @param {boolean} [override] | ||
| */ | ||
| function set_param(url, param, value, override = true) { | ||
| if (value === undefined) { | ||
| return; | ||
| } | ||
|
|
||
| if (value === null) { | ||
| if (override || url.searchParams.has(param)) { | ||
| url.searchParams.delete(param); | ||
| } | ||
| } else { | ||
| if (override || !url.searchParams.has(param)) { | ||
| url.searchParams.set(param, value); | ||
| } | ||
| } | ||
| } | ||
| 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. This is enabled by default if you have | ||
| * defined the Vercel loader in `kit.images.loader`, else disabled by default. | ||
|
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. It'd be nice to show how to do this. E.g. do you set |
||
| * https://vercel.com/docs/concepts/image-optimization | ||
| */ | ||
| images?: boolean; | ||
| } | ||
| ): Adapter; | ||
|
|
||
| export interface ServerlessConfig { | ||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -430,6 +430,31 @@ export interface KitConfig { | |||||
| */ | ||||||
| errorTemplate?: string; | ||||||
| }; | ||||||
| /** | ||||||
| * Image optimization configuration | ||||||
| */ | ||||||
| images?: { | ||||||
| /** | ||||||
| * Path to a a file that contains a loader that will be used to generate the an image URL out of the given source and width. | ||||||
|
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.
Suggested change
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. Perhaps we should say "module" rather than "file"? It could be good to mention that it would often be a module from an npm package being used here |
||||||
| * It optionally also takes third parameter for options. | ||||||
| * | ||||||
| * ```js | ||||||
| * export default function loader(src, width, opts) { | ||||||
|
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. can we move |
||||||
| * return `https://example.com/${src}?w=${width}&q=${opts.quality || 75}`; | ||||||
| * } | ||||||
| * ``` | ||||||
| */ | ||||||
| loader?: string; | ||||||
| /** | ||||||
| * Which srcset sizes to generate | ||||||
|
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 this description correct? I thought it was the screen sizes. I know you had a
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. You have to tell Vercel (and some other image optimization providers, too) which image widths / sizes / call-it-whatever-you-want are allowed to be generated. 3840 may sound like a big number, but it really isn't when you think about a 4k resolution laptop screen.
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. The main problem with this config and the domain config is that image optimization providers may have specific settings / requirements. The question now is - how do you make it so that you don't have to duplicate the config? Either you put a common denominator inside the Kit config, and adapters can read from that config, or you provide that config through the adapters, but then we need a way to get the config from the adapter into Kit.
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'd not have any common config. E.g. the
I don't think it's bad if you end up having the same config in different implementations. And in fact it might be helpful because different implementations might have config with the same name that behaves slightly differently |
||||||
| * @default [640, 828, 1200, 1920, 3840] | ||||||
| */ | ||||||
| sizes?: number[]; | ||||||
| /** | ||||||
| * Which external domains to trust when optimizing images | ||||||
|
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. what's the usecase for this? it sort of seems like an oddly-specific feature. I imagine the user could create their own wrapper around
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. It's needed if you want to optimize other images than ones that reside on your domain - else Vercel will not optimize them: https://vercel.com/docs/build-output-api/v3/configuration#images . AFAIK other provides have some variant of this, too, so I added it the general config.
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. Ah, makes sense. But could we move these options to the Vercel loader since they're somewhat Vercel-specific? It's a bit confusing to me that some options are here and some are in the adapter, so you need to check two places to see what the available options are. Maybe we should just put them all in the adapter? |
||||||
| */ | ||||||
| domains?: string[]; | ||||||
| }; | ||||||
| /** | ||||||
| * Inline CSS inside a `<style>` block at the head of the HTML. This option is a number that specifies the maximum length of a CSS file to be inlined. All CSS files needed for the page and smaller than this value are merged and inlined in a `<style>` block. | ||||||
| * | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { DEV } from 'esm-env'; | ||
| import { sizes, loader, domains } from '__sveltekit/images'; | ||
|
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'd prefer not to tie it to SvelteKit, so I think it'd be nicer to create a singleton instance rather than using a virtual module.
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. What do you mean by that? How else would you get the config into the app?
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 the app needs it. If you move |
||
|
|
||
| /** | ||
| * @param {string} src | ||
| * @param {any} [options] | ||
| * @returns {{ src: string, srcset?: string }} | ||
| */ | ||
| export function getImage(src, options) { | ||
|
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 feel like we might want It's much nicer to write this: Than this: I do like the explicitness of |
||
| if (DEV) { | ||
| if (!matches_domain(src)) { | ||
| console.warn( | ||
| `$app/images: Image src '${src}' does not match any of the allowed domains and will therefore not be optimized.` | ||
| ); | ||
| } | ||
| return { srcset: src, src }; | ||
| } | ||
|
|
||
| if (!matches_domain(src)) { | ||
| return { src }; | ||
| } | ||
|
|
||
| const srcset = sizes | ||
|
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 really understand how this is supposed to be used. If the Also, I think Vercel is the only CDN that supports
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 went back and looked at your old PR as saw that the I'm not sure if you intended to do that here as well and just missed copying it over or if you meant to do it differently here? I've added support for
|
||
| .map((size) => { | ||
| const url = loader(src, size, options); | ||
| const w = size + 'w'; | ||
| return `${url} ${w}`; | ||
| }) | ||
| .join(', '); | ||
| const _src = loader(src, sizes[sizes.length - 1], options); | ||
|
|
||
| // Order of attributes is important here as they are set in this order | ||
| // and having src before srcset would result in a flicker | ||
|
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. Better safe than sorry, but I haven't noticed any flicker when |
||
| return { srcset, src: _src }; | ||
|
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'd expect |
||
| } | ||
|
|
||
| /** | ||
| * @param {string} src | ||
| */ | ||
| function matches_domain(src) { | ||
|
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 feel like this is mostly bloating the runtime as most CDNs don't need this |
||
| const url = new URL(src, 'http://n'); // if src is protocol relative, use dummy domain | ||
| if (url.href === src) { | ||
| return domains.some((domain) => url.hostname === domain); | ||
| } else { | ||
| return true; // relative urls are always ok | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.