sveltejs / kit

web development, streamlined
https://svelte.dev/docs/kit
MIT License
18.71k stars 1.94k forks source link

Image processing #241

Open Rich-Harris opened 3 years ago

Rich-Harris commented 3 years ago

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 https://github.com/sveltejs/kit/issues/241#issuecomment-1274046866 and further demonstrated in https://github.com/sveltejs/kit/issues/241#issuecomment-1286296389.

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 https://github.com/sveltejs/rfcs/pull/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 https://github.com/sveltejs/svelte/pull/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. https://github.com/sveltejs/kit/pull/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.

eur2 commented 1 year ago

It seems that this new package will solve all the image optimization issues in an easy and efficient way: https://github.com/divriots/jampack

jdgamble555 commented 1 year ago

Steve talks about some great options with the <picture> tag

https://youtu.be/-zzmfjIiC3M

J

davej commented 1 year ago

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.

djmtype commented 1 year ago

@benmccann Would you mind sharing more of your Image component file, more particularly how you're adding different widths?

rdela commented 1 year ago

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 https://github.com/rdela/sveltekit-imagetools/pull/1

(Thanks @benmccann and @eur2 for weighing in.)

djmtype commented 1 year ago

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?

TomSmedley commented 1 year ago

+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.

benmccann commented 1 year ago

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.

jasongitmail commented 1 year ago

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",
  1. Install using: npm i -D vite-imagetools
  2. 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}']
  }
});
  1. 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>
  1. Add an image at lib/assets/images/example.jpg
  2. 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

  1. 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.
rchrdnsh commented 1 year ago

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

benmccann commented 1 year ago

@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: https://github.com/sveltejs/kit/issues/241#issuecomment-1274046866

rchrdnsh commented 1 year ago

haha! works now, thank you @benmccann! XD

benmccann commented 1 year ago
  1. 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 https://github.com/sveltejs/kit/pull/10788

benmccann commented 12 months ago

@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 (https://github.com/sveltejs/kit/pull/10323)

rdela commented 11 months ago

@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?

benmccann commented 11 months ago

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

rdela commented 11 months ago

Does anyone else think having a complete example in a repo anywhere is helpful?

UPDATE 2023-12-08: RETIRED

braebo commented 9 months ago

How might this work with divs using images via backgorund-image: url()?

leoj3n commented 8 months ago

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.

mrxbox98 commented 3 months ago

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.