withastro / roadmap

Ideas, suggestions, and formal RFC proposals for the Astro project.
311 stars 30 forks source link

A plan for a core image story #475

Closed matthewp closed 1 year ago

matthewp commented 1 year ago

Body

Summary

This proposal aims to outline a plan for a core story for images in Astro, doing so by:

Background & Motivation

src/assets folder

Using images in Astro is currently a bit confusing. Should my images go in public or src? How do I refer to them?

For this, we'd like to introduce a src/assets folder. This folder would be used for your source assets, as in, your not optimized, raw assets. Usage of this folder would be recommended, but not forced.

To make it easier to use this folder from anywhere in the project, an alias will be provided so it is possible to write ~/assets. Due to necessitating tsconfig.json changes, this will only affect new projects, and is completely optional.

Content Collection integration

It is fairly common for one of the property of a Markdown piece of content to need to be a reference to an asset (think, cover image for an article, picture for an author etc).

In tandem with the src/assets folder, we'd like to introduce a way for users to specify that a specific property needs to refer to a valid asset from the src/assets folder. This would allow us to provide validation of the asset, think cover needs to be a png of size 240x240, including validating that the file actually exists.

Facts

Image component

(see https://github.com/withastro/roadmap/discussions/447 for an earlier version of this proposal)

The current Image component can be confusing to use at time. Why do I need to set an aspectRatio? (what even is the aspect ratio of my image?) Why is my image getting cropped? What format should I use? What does quality means?

width, height and aspectRatio

We'd like to make it so less properties are needed in general. In most cases, the width and height would be automatically inferred from the source file and not passing them would just result in the same dimensions, but a smaller weight.

For cases where they're needed, we'd like to remove the aspectRatio property, instead preferring that users manually set both width and height. Combined with [[#Better behaviour for resizing images]], we believe that this will lead to a more consistant and easier to resonate about experience.

format

format would be set to webp by default, removing the need for it to be passed completely. We believe WebP to be a sensible default for most images, providing with a smaller file size while not sacrificing on compatibility and support for transparency.

quality

For quality, we'd like to make it easier for people to use without needing to know the in-and-out of the loader's algorithm for quality. We propose to fix this by introducing different presets that users can choose from, for example: quality: 'low' would automatically set an appropriate, approximative quality that is considered 'low' (specific implementation is left to the loaders to decide.)

Better behaviour for resizing images

Despite the tremendous power it offered, users were often confused by how @astrojs/image would crop their images to fit the desired aspect ratio. This was especially confusing for users coming from other popular frameworks that don't offer this feature.

As such, this feature would be removed of the base toolkit. Resizing would now only operate in ways that follow the original image aspect ratio (based on width). To control how an image is fitted inside its container, the object-fit and object-position CSS properties can be used.

For users used to other frameworks, this is a similar behaviour to the one offered by NextJS's next/image and Eleventy's eleventy-img.

Facts

Shape of ESM imports

Currently, importing images in Astro returns a simple string with the path of the image. The @astrojs/image integration enhance this by instead returning the following shape: {src: string, width: number, height: number, format: string}, allowing users to easily construct img tags with no CLS:

---
import image from "../my_image.png"
---

<img src={image.src} width={image.width} height={image.height} />

This shape should also give most of the information an user could need to build their own image integrations based on ESM imports.

Since this would be directly in core, there would be no more configuration changes needed to get the proper types and as such, editor integration should be painless (completions, type checking etc) and address user confusion around this.

In order to avoid any breakage, this shape would be under an experimental flag until the next major release of Astro. Similarly to content collections, we would automatically update your env.d.ts with the type changes needed for this.

Goals

Non-goals of this proposal

We realize that many of those features not being goals are considered to be downgrades compared to @astrojs/image. This is on purpose as we'd like to approach this project with a "small MVP, add features over time" mentality.

Creating a perfect Image component for core is a way more complex project than it might seems, the balance between "This has too much options, it's too confusing to use" and "This doesn't have enough options, it's unusable for anything even a little bit complex" is incredibly hard to hit.

We believe that, much like we did with other features in Astro, by building a good core and providing ways build around it, we'll ultimately achieve the best result possible.

Example

All the examples below show the result path in the built website

Basic image from src/assets

Source

---
import { Image } from "astro:image";
import myImage from "~/assets/my_image.png"; // Image is 1600x900

// Relative paths are also supported:
// import myImage from "../../assets/my_image.png"
---

<Image src={myImage} alt="Image showing Elsa from the movie Frozen, she has blonde hair and is wearing a long blue dress" />

Result

<img src="/_astro/my_image.hash.webp" width="1600" height="900" decoding="async" loading="lazy" alt="Image showing Elsa from the movie Frozen, she has blonde hair and is wearing a long blue dress" />

Resized image from src/assets

Source

---
import { Image } from "astro:image";
import myImage from "~/assets/my_image.png"; // Image is 1600x900
---

<Image src={myImage} width={800} alt="..." />

Result

<!-- Result image is a webp of 800x450 -->
<img src="..." width="800" height="450" decoding="async" loading="lazy" alt="..." />

Source

---
import { Image } from "astro:image";
import myImage from "~/assets/my_image.png"; // Image is 1600x900
---

<Image src={myImage} width={800} height={900} alt="..." />

Result

<!-- Result image is a webp of 800x450 -->
<img src="..." width="800" height="900" decoding="async" loading="lazy" alt="..." />

Basic remote image

Source

---
import { Image } from "astro:image";
// Image is 1920x1080
---

<Image src="https://example.com/image.png" alt="..." />
<!-- ERROR! `width` and `height` are required -->

<Image src="https://example.com/image.png" width={1280} alt="..." />
<!-- ERROR! `height` is required -->

<Image src="https://example.com/image.png" width={1280} height={720} alt="..." />

Result (3rd example)

<!-- Remote images are not optimized or resized.
<img src="https://example.com/image.png" decoding="async" loading="lazy" width="1280" height="720" alt="...">

Markdown

Source

Source image is a .png of 640x960

---
cover: ~/assets/cover.png 
---

Result

Result is a .webp of 640x960

{
    cover: {
        src: "/_astro/cover.hash.webp",
        width: 640,
        height: 960
    }
}

Source

My super image of 640x480:
![...](~/assets/my_image.png)

OR

![...](./my_image.png)

OR

![...](../../assets/my_image.png)

Result

<img src="/_astro/my_image.hash.webp" width="640" height="480" loading="lazy" decoding="async" alt="...">
etmartinkazoo commented 1 year ago

This is great, thank you for all of your work. In my mind nailing the image story is critical as it is one of the most difficult parts of building for the web that is so often overlooked.

In the meantime, I hope that issue 6010 can get resolved. I was really looking forward to using a combination of SSG and SSR post 2.0 on a few sites with prerender but cannot until this issue is resolved. I'd love to offer a hand to help.

leomp12 commented 1 year ago

Are there any downsides to adding dimensions to the output image filename? /_astro/my_image.640_960.hash.webp" ?

Transforming images on SSR is always bad, just adding sharp to function dependencies increases cold start noticeably 😬 I'm considering compiling a static version first especially for compiling the images, I would use a wrapper with some adaptations for <Image> and <Picture>, move the compiled images and then build and deploy with SSR.

I can read the dimensions picture by picture, but it would be much simpler if it was already in the filename, and I think this could be useful in other ways as well. Wdyt?


Update

My bad, it's necessary just a bash line to list images with respective sizes...

And yes, at least for Firebase Functions prevent publishing @astrojs/image has a really considerable great impact. If someone is interested I'm running astro build twice on production build, first one with static output (reduced paths with getStaticPaths) to transform the images, second one with Node adapter. Copy the compiled images to dist/server/ and create a "manifest" file, on second build I'm not using @astrojs/image source, replacing it with Vite alias to a custom Picture.runtime.astro.

Princesseuh commented 1 year ago

This proposal is now in Stage 3! https://github.com/withastro/roadmap/pull/500