From c429e875079648b0823d12c5beb839036bf3ffb4 Mon Sep 17 00:00:00 2001 From: Daniil Gaponov Date: Sun, 6 Sep 2026 16:32:58 +0300 Subject: [PATCH] perf(ui-samples): serve mobile sample cards through next/image The mobile UI-samples cards rendered a plain pointing straight at the imported asset, bypassing the image optimizer that the rest of the page already uses. The sources are up to 2212px wide while the cards display at roughly viewport width, so the originals were served in full. Pass the imported StaticImageData to next/image instead of just its `.src`, so the images are resized per device and converted to WebP/AVIF. Measured on the homepage at a 390px viewport, production build, fresh profile, all seven cards scrolled into view: card images 1234 kB -> 305 kB (-929 kB, -75%) hotel-booking 329 -> 72 listing 126 -> 28 dashboard 189 -> 32 osn 126 -> 40 mail 183 -> 52 kubernetes 120 -> 34 task-tracker 163 -> 46 The cards also had no alt attribute at all. They now use the sample's localised title ("Dashboard", "Booking page", ...), which the block already had available. The SCSS needs `width: 100%; height: auto` because next/image emits intrinsic width/height attributes; without it the card laid out at the source image's natural width of 2216px instead of the container's 508px. That was caught in a browser and fixed, not shipped. Desktop is unaffected: the block is display:none above the sm breakpoint and issues zero card requests there, before and after. Build, lint and typecheck pass. E2E not run - Playwright browsers are not installed locally; these are visual changes, so the snapshot suite is worth running before merge. Co-Authored-By: Claude Opus 5 (1M context) --- .../UISamplesMobile/UISamplesMobile.scss | 4 ++++ .../UISamplesMobile/UISamplesMobile.tsx | 15 ++++++++++----- src/blocks/UISamples/samples.ts | 14 +++++++------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/blocks/UISamples/components/UISamplesMobile/UISamplesMobile.scss b/src/blocks/UISamples/components/UISamplesMobile/UISamplesMobile.scss index 61d92d6d0eb2..e7a5b565692f 100644 --- a/src/blocks/UISamples/components/UISamplesMobile/UISamplesMobile.scss +++ b/src/blocks/UISamples/components/UISamplesMobile/UISamplesMobile.scss @@ -14,5 +14,9 @@ $block: '.#{variables.$ns}ui-samples-mobile'; &__card { pointer-events: none; + // next/image emits intrinsic width/height attributes, so without this the card lays + // out at the source image's natural width (2216px) instead of the container width. + width: 100%; + height: auto; } } diff --git a/src/blocks/UISamples/components/UISamplesMobile/UISamplesMobile.tsx b/src/blocks/UISamples/components/UISamplesMobile/UISamplesMobile.tsx index 4a5d0e8d7b26..8918321fc98b 100644 --- a/src/blocks/UISamples/components/UISamplesMobile/UISamplesMobile.tsx +++ b/src/blocks/UISamples/components/UISamplesMobile/UISamplesMobile.tsx @@ -1,3 +1,4 @@ +import Image, {type StaticImageData} from 'next/image'; import React from 'react'; import {block} from 'src/utils'; @@ -8,11 +9,15 @@ import './UISamplesMobile.scss'; const b = block('ui-samples-mobile'); type UISampleCardProps = { - imageSrc: string; + image: StaticImageData; + alt: string; }; -const UISampleCard: React.FC = ({imageSrc}) => { - return ; +const UISampleCard: React.FC = ({image, alt}) => { + // next/image rather than a plain : the source files are up to 2212px wide while the + // cards render at roughly viewport width, so the originals wasted around 1 MB on mobile. + // This resizes per device and serves WebP/AVIF. + return {alt}; }; export const UISamplesMobile: React.FC = () => { @@ -20,8 +25,8 @@ export const UISamplesMobile: React.FC = () => { return (
- {samples.map(({type, imagePreviewSrc}) => ( - + {samples.map(({type, imagePreview, title}) => ( + ))}
); diff --git a/src/blocks/UISamples/samples.ts b/src/blocks/UISamples/samples.ts index 9f8c8cf8e7ca..104111b70fd3 100644 --- a/src/blocks/UISamples/samples.ts +++ b/src/blocks/UISamples/samples.ts @@ -35,7 +35,7 @@ export const useSampleComponents = () => { () => [ { type: SampleComponent.Dashboard, - imagePreviewSrc: dashboardImage.src, + imagePreview: dashboardImage, Component: LazyDashboardPreview2, title: t('ui_samples_dashboard_tab'), breadCrumbsItems: ['Dashboard'], @@ -43,42 +43,42 @@ export const useSampleComponents = () => { }, { type: SampleComponent.HotelBooking, - imagePreviewSrc: hotelBookingImage.src, + imagePreview: hotelBookingImage, Component: LazyApartmentCardPreview, title: t('ui_samples_apartment_tab'), blank: true, }, { type: SampleComponent.Listing, - imagePreviewSrc: listingImage.src, + imagePreview: listingImage, Component: LazyTablePreview, title: t('ui_samples_table_tab'), breadCrumbsItems: ['Table'], }, { type: SampleComponent.TaskTracker, - imagePreviewSrc: taskTrackerImage.src, + imagePreview: taskTrackerImage, Component: LazyTasksPreview, title: t('ui_samples_task_tracker_tab'), blank: true, }, { type: SampleComponent.Kubernetes, - imagePreviewSrc: kubernetesImage.src, + imagePreview: kubernetesImage, Component: LazyKubernetesPreview, title: t('ui_samples_kubernetes_tab'), blank: true, }, { type: SampleComponent.Osn, - imagePreviewSrc: osnImage.src, + imagePreview: osnImage, Component: LazyOsnPreview, title: t('ui_samples_osn_tab'), blank: true, }, { type: SampleComponent.Mail, - imagePreviewSrc: mailImage.src, + imagePreview: mailImage, Component: LazyMailPreview, title: t('ui_samples_mail_tab'), blank: true,