Compare commits

..

1 Commits

Author SHA1 Message Date
lebaudantoine 06c4e277db ⚡️(frontend) serve illustrations as WebP with a PNG fallback
The three intro slider illustrations were the heaviest part of the
homepage. On a cold load of the production homepage (cache disabled),
the browser transferred 2.67 MB in total, of which 2.00 MB was these
three PNG files: 75% of the page weight, and about 3.5 times all the
JavaScript combined (579 kB).

HTTP compression cannot help here, PNG being already compressed. The
files are heavy because of the grain texture applied on the
illustrations, which a lossless format has to store pixel by pixel.

Encoded as WebP at quality 90, which preserves the grain, the three
slides go from 2.00 MB to 182 kB (-91%). The homepage cold load drops
from 2.67 MB to about 0.86 MB (-68%), and the illustrations now
account for about 21% of the transfer instead of 75%.

The fourth illustration, used on the recording screens, gets the same
treatment (721 kB to 84 kB).

We checked in PostHog that most of our users are on browser versions
supporting WebP. For the remaining ones, a new Picture primitive
renders a <picture> element with the WebP file as a source and the
original PNG as the <img> fallback, so nobody loses the illustrations.

Add webp to the nginx static assets location, otherwise the new files
would be served without the 30 days cache headers.
2026-11-14 14:10:51 +01:00
17 changed files with 55 additions and 23 deletions
+1
View File
@@ -29,6 +29,7 @@ and this project adheres to
- 🍱(frontend) add Portuguese glyphs to Marianne subsetted fonts
- 🚸(frontend) bring new speakers to the top of the carousel
- 🚸(frontend) bring raised hands into view in the carousel
- ⚡️(frontend) serve illustrations as WebP with a PNG fallback
### Fixed
-2
View File
@@ -61,8 +61,6 @@ La Suite Meet is fully self-hostable and released under the MIT License, ensurin
We’re continuously adding new features to enhance your experience, with the latest updates coming soon!
⭐ **Find La Suite Meet useful?** Click the Star button at the top right of this page. It helps other people and public organisations discover the project.
### 🚀 Major roll out to all French public servants
On the 29th of January 2026, Prime Minister Sébastien Lecornu, announced the full deployment of Visio—the French government’s dedicated Meet platform—to all public servants. ([Source in English](https://www.nytimes.com/2026/01/29/world/europe/france-zoom-alternative-visio.html))
+1 -1
View File
@@ -33,7 +33,7 @@ server {
}
# Serve static files with caching
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|webp|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
+1 -1
View File
@@ -72,7 +72,7 @@ server {
}
# Serve static files with caching
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|webp|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
+1 -1
View File
@@ -11,7 +11,7 @@ server {
}
# Serve static files with caching
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|webp|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

@@ -1,6 +1,6 @@
import { styled } from '@/styled-system/jsx'
import { css } from '@/styled-system/css'
import { Button } from '@/primitives'
import { Button, Picture } from '@/primitives'
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -30,12 +30,10 @@ const Body = styled('p', {
},
})
const Image = styled('img', {
base: {
maxHeight: '362px',
height: '100%',
width: 'fit-content',
},
const image = css({
maxHeight: '362px',
height: '100%',
width: 'fit-content',
})
const Dot = styled('div', {
@@ -165,7 +163,6 @@ const carouselNavButton = css({
},
})
// todo - optimize how images are imported
const SLIDES: Slide[] = [
{
key: 'slide1',
@@ -251,7 +248,12 @@ export const IntroSlider = () => {
visible={index === slideIndex}
key={index}
>
<Image src={slide.src} alt="" role="presentation" />
<Picture
src={slide.src}
alt=""
role="presentation"
className={image}
/>
<TextAnimation visible={index == slideIndex}>
<Heading>{t(`${slide.key}.title`)}</Heading>
<Body>{t(`${slide.key}.body`)}</Body>
@@ -1,4 +1,4 @@
import { A, Div, H, Text } from '@/primitives'
import { A, Div, H, Picture, Text } from '@/primitives'
import { css } from '@/styled-system/css'
import { useTranslation } from 'react-i18next'
import { LoginPrompt } from './LoginPrompt'
@@ -45,7 +45,7 @@ export const NoAccessView = ({
flexDirection="column"
alignItems="center"
>
<img
<Picture
src={imagePath}
alt=""
className={css({
@@ -1,4 +1,4 @@
import { Button, Div, Text } from '@/primitives'
import { Button, Div, Picture, Text } from '@/primitives'
import { css } from '@/styled-system/css'
import { useRoomId } from '@/features/rooms/livekit/hooks/useRoomId'
@@ -169,7 +169,7 @@ export const ScreenRecordingSidePanel = () => {
flexDirection="column"
alignItems="center"
>
<img
<Picture
src="/assets/intro-slider/4.png"
alt=""
className={css({
@@ -1,4 +1,4 @@
import { A, Button, Div, Text } from '@/primitives'
import { A, Button, Div, Picture, Text } from '@/primitives'
import { css } from '@/styled-system/css'
import { useRoomId } from '@/features/rooms/livekit/hooks/useRoomId'
@@ -183,7 +183,7 @@ export const TranscriptSidePanel = () => {
flexDirection="column"
alignItems="center"
>
<img
<Picture
src="/assets/intro-slider/3.png"
alt=""
className={css({
@@ -9,7 +9,7 @@ import { mediaUrl } from '@/api/mediaUrl'
import { useUser } from '@/features/auth/api/useUser'
import { UserAware } from '@/features/auth/components/UserAware'
import { Screen } from '@/layout/Screen'
import { H, LinkButton, Text } from '@/primitives'
import { H, LinkButton, Picture, Text } from '@/primitives'
import { formatDate } from '@/utils/formatDate'
import { ErrorScreen } from '@/components/ErrorScreen'
import { LoadingScreen } from '@/components/LoadingScreen'
@@ -118,7 +118,7 @@ const RecordingDownload = () => {
<Screen layout="centered" footer={false}>
<Center>
<VStack>
<img
<Picture
src="/assets/intro-slider/4.png"
alt={''}
className={css({
+30
View File
@@ -0,0 +1,30 @@
import { type ComponentPropsWithoutRef } from 'react'
import { css } from '@/styled-system/css'
const WEBP_CONVERTIBLE_EXTENSION = /\.(png|jpe?g)$/i
export type PictureProps = Omit<ComponentPropsWithoutRef<'img'>, 'src'> & {
src: string
}
/**
* Serves the WebP version of an image, and falls back on the original file
* for browsers that do not support WebP.
*
* The `<picture>` wrapper uses `display: contents`, so the `<img>` is laid out
* exactly as if it was rendered alone: style it through the usual props.
*/
export const Picture = ({ src, alt, ...props }: PictureProps) => {
const hasWebpVersion = WEBP_CONVERTIBLE_EXTENSION.test(src)
return (
<picture className={css({ display: 'contents' })}>
{hasWebpVersion && (
<source
srcSet={src.replace(WEBP_CONVERTIBLE_EXTENSION, '.webp')}
type="image/webp"
/>
)}
<img src={src} alt={alt} {...props} />
</picture>
)
}
+1
View File
@@ -23,6 +23,7 @@ export { Link } from './Link'
export { Menu } from './Menu'
export { MenuList } from './MenuList'
export { P } from './P'
export { Picture } from './Picture'
export { Popover } from './Popover'
export { ScreenReaderAnnouncer } from './ScreenReaderAnnouncer'
export { Text } from './Text'
@@ -80,7 +80,7 @@ data:
}
# Serve static files with caching
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|webp|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}