Skip to content

Commit

Permalink
docs: working with images (SWF-176)
Browse files Browse the repository at this point in the history
  • Loading branch information
mkucmus committed Feb 17, 2023
1 parent 6a4f1fe commit a15a308
Show file tree
Hide file tree
Showing 27 changed files with 622 additions and 108 deletions.
5 changes: 5 additions & 0 deletions .changeset/tender-queens-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"docs": patch
---

Working with images
5 changes: 5 additions & 0 deletions .changeset/thick-poets-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shopware-pwa/types": minor
---

Improved media types
5 changes: 5 additions & 0 deletions .changeset/wild-parents-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shopware-pwa/helpers-next": patch
---

Proper access for an URL of main product image
3 changes: 3 additions & 0 deletions apps/docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const sidebar = [
{ text: "Styling", link: "/framework/styling" },
{ text: "Context Composables", link: "/framework/context-composables" },
{ text: "Shared Composables", link: "/framework/shared-composables" },
{ text: "Images", link: "/framework/images" },
],
},
{
Expand All @@ -51,6 +52,7 @@ export const sidebar = [
{ text: "Testing", link: "/best-practices/testing" },
{ text: "Performance", link: "/best-practices/performance" },
{ text: "Deployment", link: "/best-practices/deployment" },
{ text: "Images", link: "/best-practices/images" },
],
},
{
Expand Down Expand Up @@ -80,6 +82,7 @@ export default defineConfigWithTheme<ThemeConfig>({
srcDir: "src",
// srcExclude: ["tutorial/**/description.md"], In case we need something to be excluded
scrollOffset: "header",
ignoreDeadLinks: true, // remove once MR #294 is merged
head: [
[
"link",
Expand Down
Binary file added apps/docs/src/.assets/edit-media-sizes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/docs/src/.assets/squoosh-app.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
142 changes: 142 additions & 0 deletions apps/docs/src/best-practices/images.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
head:
- - meta
- name: og:title
content: "Best practices: Images"
- - meta
- name: og:description
content: "Collection of good practices to manage images."
- - meta
- name: og:image
content: "https://frontends-og-image.vercel.app/Best%20practices:%20**Images** 🖼.png?fontSize=110px"
nav:
position: 30
---

# Images

Best practices for images.

## Optimization

Let's have a look on some good practices to help display images efficiently.

### Image format & Compression

Compression is the first step, relatively easy to achieve in order to reduce loading time by reducing file size, thus saving network traffic for images.

**WebP** is a new format for images, developed by Google, in order to add an alternative for _png_ images, but with lossy compression, enhanced by some new techniques allowing to compress with different level selectively within the same image. As it's become fully supported in all modern browsers - it can be recommended.

You can check how much you can save by using `webp` format instead of others raster-images formats. See how can it help you on [Thumbor](http://thumborize.globo.com/?url=https://frontends-demo.vercel.app).

:::info Test different formats
There are many image formats, which have different advantages, depending on images purposes. Probably you don't need `webp` files for vector images. Sometimes, when the high image quality is important, using lossy formats may not be a good idea. It always depends on the use case.

There are many tools to check different image formats, but the great one is [Squoosh](https://squoosh.app/) which allows you to experiment with images interactively:

![Squoosh screenshot](../.assets/squoosh-app.png)
:::

### Images hosting on CDN + Image processor

Using Content Delivery Network platforms (CDN) helps to reduce network distance, by serving resources from the closest server for an user.

Although it can be a standalone service, some platforms serves images with additional option of resizing on the fly, or being more general: processing the images, depending on provided query parameter, like `?width=400px`. Thanks to this, `<img>` element is more readable.

```html
<img
src="https://images.swfrontends.com/frontends-unsplash.png?width=400px"
srcset="
https://images.swfrontends.com/frontends-unsplash.png?width=400px 320w,
https://images.swfrontends.com/frontends-unsplash.png?width=800px 720w
"
>
```

Examples of open source image processors which can be used as a middleware to serve processed images:
* [thumbor](https://www.thumbor.org/)
* [lovell/sharp](https://github.com/lovell/sharp)
* [imgproxy/imgproxy](https://github.com/imgproxy/imgproxy)


## Responsive images

Utilize [srcset](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-srcset) attribute for `<img>` elements in order to load the image in size what is actually needed at the moment.
Decide what metric (pixel ratio - DPR or width) is more appropriate for your users when defining breakpoints.

Also, consider using `sizes` attribute which will indicate what image size is best to choose - if your images occupy less than 100% of viewport. The value can be defined in percentage of viewport width (`sizes="80vw"`) or fixed value (`sizes="600px"`) regardless the device size. Read more at [mdn web docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-sizes).

```html
<img
sizes="50vw"
srcset="frontends-header-xs.webp 600w, rontends-header-md.webp 1200w, rontends-header-xl.webp 2000w"
src="rontends-header-xs.webp"
alt="...">
<!-- src fallback is set to be mobile first -->
```

If you application serves many image formats and there is a significant part of users with older browsers, you can use [picture](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture) element.

In this example, browser will decide which image format is available to serve, otherwise the `<img>` will be picked as a fallback.

```html
<picture>
<source
type="image/avif"
srcset="
https://images.swfrontends.com/frontends-unsplash-320.avif 320w,
https://images.swfrontends.com/frontends-unsplash-720.avif 720w"
>
<source
type="image/webp"
srcset="
https://images.swfrontends.com/frontends-unsplash-320.webp 320w,
https://images.swfrontends.com/frontends-unsplash-720.webp 720w"
>
<img
src="https://images.swfrontends.com/frontends-unsplash.png"
alt="Logo Shopware Frontends"
>
</picture>
```

## Reduce Cumulative Layout Shift (CLS)

When Images occupy a big amount of space on web pages, they are a common cause of high [CLS](https://web.dev/cls/) scores.

* Always set `width` and `hight` attributes for your `<img>` elements, with values matching size of image source. So even if they are being loaded, the space of layout will be filled out.
* Define CSS style to override `<img>` attributes (there is a moment when image element is available in DOM, and CSS is not loaded yet):
```css
img {
max-width: 100%;
height: auto;
}
```
* Try to use low-quality placeholders (based on svg, for example) to avoid having empty blank spaces within the layout:
<div role="status" class="mt-4 max-w-sm p-4 animate-pulse md:p-6 ">
<div class="flex items-center justify-center h-32 mb-4 bg-gray-300 rounded dark:bg-gray-700">
<svg class="w-12 h-12 text-gray-200 dark:text-gray-600" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" viewBox="0 0 640 512"><path d="M480 80C480 35.82 515.8 0 560 0C604.2 0 640 35.82 640 80C640 124.2 604.2 160 560 160C515.8 160 480 124.2 480 80zM0 456.1C0 445.6 2.964 435.3 8.551 426.4L225.3 81.01C231.9 70.42 243.5 64 256 64C268.5 64 280.1 70.42 286.8 81.01L412.7 281.7L460.9 202.7C464.1 196.1 472.2 192 480 192C487.8 192 495 196.1 499.1 202.7L631.1 419.1C636.9 428.6 640 439.7 640 450.9C640 484.6 612.6 512 578.9 512H55.91C25.03 512 .0006 486.1 .0006 456.1L0 456.1z"/></svg>
</div>
</div>


## Speed up Largest Contentful Paint

"[LCP](https://web.dev/lcp/) element has an image on around three quarters of pages" says the result of [Web research](https://almanac.httparchive.org/en/2021/media#images). Moreover, on 70.6% mobile pages, LCP element has an image. On desktops, the rate is even bigger: 79.4%. So we can assume, that bad LCP scores are based on low image performance.

* Never use `loading="lazy"` on `<img>` elements if they are part of what an user see first on they viewport (consider editing the attributes for CMS elements in Shopware Experiences).
* Utilize `fetchpriority="high"` on `<img>` also tells the browser, that the asset (LCP resource is prioritized) is important and should be taken care of as fast as possible.


<PageRef page="../framework/images" title="Framework" sub="How to display images served by API" />



## Resources

Collection of useful blog posts and articles about performance related to images.

* https://web.dev/learn/images/
* https://austingil.com/better-html-images/
* https://www.smashingmagazine.com/2023/01/optimizing-image-element-lcp/
* https://web.dev/top-cwv-2023/
158 changes: 158 additions & 0 deletions apps/docs/src/framework/images.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
---
head:
- - meta
- name: og:title
content: "Working with Images"
- - meta
- name: og:description
content: "How to display images served by API"
- - meta
- name: og:image
content: "https://frontends-og-image.vercel.app/Working%20with%20**Images**.png?fontSize=110px"
nav:
position: 30
---

<script setup>
import StackBlitzLiveExample from "../components/StackBlitzLiveExample.vue";
</script>

# Working with Images

This section covers topics related to images, with a focus on what comes from API.

:::warning Not auto-loaded
Although images are not always contained in API responses, we try to keep the composables logic aware of that and ready to load if they are needed.

Which means if you need to work with images, ensure the requests contains additional [associations](https://shopware.stoplight.io/docs/store-api/cf710bf73d0cd-search-queries#associations).

Example of request's payload with media association included, to avoid an empty `media` object within the response:

```json
{
"associations": {
"media": {}
}
}
```
:::

## Structure of media objects

Media objects can be used in many places, such as:

* CMS objects (containing [CmsElementImage](https://github.com/shopware/frontends/blob/main/packages/composables/src/types/cmsElementTypes.ts#L71) element)
* Product (cover image, image gallery, attributes in type media, etc.)
* Category (main image, ...)
* ...

Regardless the outer container (see [ProductMedia](https://github.com/shopware/frontends/blob/main/packages/types/shopware-6-client/models/content/product/ProductMedia.d.ts#L8) as example) an image object can be wrapped with, the inner structure is reflected in type definition at [Media](https://github.com/shopware/frontends/blob/main/packages/types/shopware-6-client/models/content/media/Media.d.ts#L23)

Let's have a look what's inside:

```json
{
// irrelevant data omitted
...
"mimeType": "image/webp", // mime-type of media object, supported by the Shopware 6 platform
"fileExtension": "webp",
"fileSize": 492024,
"title": "Frontends Logo",
"metaData": {
"hash": "b795091b0a92b8a0605281f710dc1c28",
"type": 2,
"width": 3505, // original width
"height": 5258 // original height
},
"alt": "Shopware Frontends",
"url": "http://localhost/media/shopware-frontends-4P8HWu_NRp4-unsplash.jpg",
"fileName": "shopware-frontends-4P8HWu_NRp4-unsplash",
"thumbnails": [ // list of resized images for previously configured ranges
{
"width": 1920,
"height": 1920,
"url": "http://localhost/thumbnail/ainars-cekuls-4P8HWu_NRp4-unsplash_1920x1920.webp",
},
{
// omitted irrelevant data
"width": 800,
"height": 800,
"url": "http://localhost/thumbnail/ainars-cekuls-4P8HWu_NRp4-unsplash_800x800.webp",
"apiAlias": "media_thumbnail"
},
...
]
...
}
```

The media object, and its `thumbnails` list, contain all required information about the file to be used in the browser like URL and sizes.

## Thumbnails and resolutions

By default, every uploaded image is resized to the predefined width and height sizes (in pixels):
* 1920x1920
* 800x800
* 400x400

In order to change those sizes, or add another one (also the quality, or to keep aspect ratio), the values need to be adjusted in administration panel, for specific media folder.

![Edit media sizes](../.assets/edit-media-sizes.png)

:::warning Image processing
While a file is uploaded, it's been automatically resized for the current configuration in Administration > Media section. Thanks to this, the newly uploaded files will be available for all required dimensions. However keep in mind that if your settings have changes, the new dimensions won't be applied automatically for the old images.
:::

## Helpers

There are few functions that could be used to extract some crucial information about the media in short way. Browse [Helpers > Media](../packages/helpers/index.html#media) category to see them all.

Example how to work with Product's main image:

```ts
import type { Product } from "@shopware-pwa/types";
import { getMainImageUrl } from "@shopware-pwa/helpers-next";


const coverUrl = getMainImageUrl(product as Product);
// coverUrl is now an URL to the resource (or undefined)
```

## Responsive Images

Having additional information about resized images (see `thumbnails` array in `Media` object), we are able to use them to define [srcset](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-srcset) attribute for `<img>`.

```vue{8}
<script>
import type { Product, Media } from "@shopware-pwa/types";
const product: Product = {} // an object omitted
// get the cover media image (main image for a product)
const coverMedia = product.cover?.media as Media
// prepare `srcset` string for available thumbnails
// let the breakpoints be for every width range
const srcset = coverMedia?.thumbnails?.map((thumb) => `${thumb.url} ${thumb.width}w`).join(", ")
</script>
<template>
<img
:srcset="srcset"
:src="coverMedia?.url"
:alt="coverMedia?.alt"
:title="coverMedia?.title"
>
</template>
```

### Live example
Have a look on live example:
<StackBlitzLiveExample projectPath="shopware/frontends/tree/main/examples/responsive-images" openPath="/" />
<br/>

The example above shows how to use dimension sizes configured in admin panel as ranges for viewport. However it can be adjusted to your needs.

The `src` attribute points to the main image URL (not resized) as a fallback.

As long as `thumbnails` array is fulfilled, the same strategy can be applied when we work with every `media` object for each entity available in Shopware 6.


<PageRef page="../best-practices/images" title="Best Practices" sub="Best Practices to work with images" />
28 changes: 28 additions & 0 deletions examples/responsive-images/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
.DS_Store
dist
dist-ssr
coverage
*.local

/cypress/videos/
/cypress/screenshots/

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
Loading

0 comments on commit a15a308

Please sign in to comment.