For coders TYPO3 Tech Corner

Integrate Webp, Responsive Images and Lazy Loading into TYPO3

Integrate Webp, Responsive Images and Lazy Loading into TYPO3

With the following snippet, you can easily optimize images that are rendered via CType textmedia, textpic and image:

  • WEBP: Deliver images both in a classic format (e.g. PNG for older browsers) and in the new WEBP format with mostly better compression and therefore smaller. Note: This is only possible from TYPO3 10 without changes.
  • Responsive Images: Images should be loaded in the size that is suitable for the mobile device. So I don't need to deliver pictures with a width of 2000px to smartphones.
  • Lazy Loading: Assets should only be loaded when the visitor scrolls to the picture. This means that less data has to be transferred when the page is initially loaded.

1. Simple example

If you are already using "fluid_styled_content", you can simply overwrite the partial "Media/Rendering/Image.html" in your site package:

<html xmlns:f="https://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true"> <picture> <source srcset="{f:uri.image(image:file, maxWidth: 2561, fileExtension: 'webp')}" media="(min-width: 1601px)" type="image/webp"> <source srcset="{f:uri.image(image:file, maxWidth: 2560, fileExtension: 'jpg')}" media="(min-width: 1601px)" type="image/jpeg"> <source srcset="{f:uri.image(image:file, maxWidth: 1601, fileExtension: 'webp')}" media="(min-width: 1201px)" type="image/webp"> <source srcset="{f:uri.image(image:file, maxWidth: 1600, fileExtension: 'jpg')}" media="(min-width: 1201px)" type="image/jpeg"> <source srcset="{f:uri.image(image:file, maxWidth: 1201, fileExtension: 'webp')}" media="(min-width: 769px)" type="image/webp"> <source srcset="{f:uri.image(image:file, maxWidth: 1200, fileExtension: 'jpg')}" media="(min-width: 769px)" type="image/jpeg"> <source srcset="{f:uri.image(image:file, maxWidth: 769, fileExtension: 'webp')}" media="(min-width: 481px)" type="image/webp"> <source srcset="{f:uri.image(image:file, maxWidth: 768, fileExtension: 'jpg')}" media="(min-width: 481px)" type="image/jpeg"> <source srcset="{f:uri.image(image:file, maxWidth: 481, fileExtension: 'webp')}" type="image/webp"> <source srcset="{f:uri.image(image:file, maxWidth: 480, fileExtension: 'jpg')}" type="image/jpeg"> <img class="image-embed-item" title="{file.title}" alt="{file.description -> f:format.nl2br()}" src="{f:uri.image(image:file, maxWidth: 1600)}" width="{dimensions.width}" height="{dimensions.height}" loading="lazy" /> </picture> </html>

The browser renders line by line and adopts the first setting that applies to it (e.g. Safari on desktop image as JPG with 2560px maximum width, old IE uses the classic img-tag, modern browsers then webp depending on the available screen width).

Don't forget to allow webp as a general image format in your LocalConfiguration file:

$GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] = 'gif,jpg,jpeg,tif,tiff,bmp,pcx,tga,png,pdf,ai,svg,webp';

2. Advanced example with more logic

That alone helps you, but you can also take it to the extreme. Assuming that images that are displayed next to a text in a column are usually much less wide than an image above or below a text, you can also help with a DataProcessor.

<?php declare(strict_types=1); namespace In2code\In2template\DataProcessing; use TYPO3\CMS\Extbase\Object\Exception; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; use TYPO3\CMS\Frontend\DataProcessing\GalleryProcessor as GalleryProcessorCore; /** * Class GalleryProcessor * extends the original GalleryProcessor with additional information of the maximum image width * for a better responsive image rendering in Image partial. */ class GalleryProcessor extends GalleryProcessorCore { /** * @var int[] */ protected $maxWidths = [ 'full' => 1600, // maximum image width if image is over or under a text 'beside' => 800 // maximum image width if image is placed beside a text ]; /** * Max width for fullwidthgallery. Related to the number of images that are included in backend, the maximum width * is calculated. For whatever reason if you only add 3 images, they are very small, but if you add 4 images (2x2) a * single image can be very large, etc... * * @var array */ protected $maxWidthsFullWidthGallery = [ 1 => 450, // number of images => max width 2 => 450, 3 => 450, 4 => 1400, // Change from 1 to 2 rows 5 => 450, 6 => 900, 7 => 450, 8 => 650, 9 => 450, 10 => 520, 11 => 450, 12 => 450, 13 => 450, // Change from 2 to 3 rows 14 => 450 ]; /** * @param ContentObjectRenderer $cObj The data of the content element or page * @param array $contentObjectConfiguration The configuration of Content Object * @param array $processorConfiguration The configuration of this processor * @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View) * @return array the processed data as key/value store * @throws Exception */ public function process( ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData ) { $processedData = parent::process($cObj, $contentObjectConfiguration, $processorConfiguration, $processedData); $processedData = $this->extendDimensions($processedData); return $processedData; } /** * @param array $processedData * @return array */ protected function extendDimensions(array $processedData): array { foreach ((array)$processedData['gallery']['rows'] as $rowKey => $row) { foreach ((array)$row['columns'] as $columnKey => $column) { if (!empty($column['dimensions'])) { if ((int)$processedData['data']['layout'] !== 30) { $maxWidth = $this->calculateMaxWidthForTextMedia($processedData, $row); } else { $maxWidth = $this->calculateMaxWidthForFullWidthGallery($processedData['gallery']['rows']); } $processedData['gallery']['rows'][$rowKey]['columns'][$columnKey]['dimensions']['maxWidth'] = $maxWidth; } } } return $processedData; } /** * @param array $processedData * @param array $row * @return int */ protected function calculateMaxWidthForTextMedia(array $processedData, array $row): int { $maxWidth = $this->maxWidths['full']; if ($processedData['data']['imageorient'] !== 0 && $processedData['data']['imageorient'] !== 8) { $maxWidth = $this->maxWidths['beside']; } if (count($row) > 0) { $maxWidth = (int)ceil($maxWidth / count($row)); } return $maxWidth; } /** * @param array $rows * @return int */ protected function calculateMaxWidthForFullWidthGallery(array $rows): int { $numberOfImages = count($rows); $maxWidth = $this->maxWidthsFullWidthGallery[0]; if (array_key_exists($numberOfImages, $this->maxWidthsFullWidthGallery)) { $maxWidth = $this->maxWidthsFullWidthGallery[$numberOfImages]; } return $maxWidth; } }

You can then respond to the new information in the partial with if-conditions:

<html xmlns:f="https://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true"> <picture> <f:comment> {dimensions.maxWidth} is calculated with a DataProcessor and should show the maximum possible image width. This depends if an image is shown above a text or beside a text (half as width) and in addition how many image columns are available </f:comment> <f:if condition="{dimensions.maxWidth} > 1601"> <source srcset="{f:uri.image(image:file, maxWidth: 2561, fileExtension: 'webp')}" media="(min-width: 1601px)" type="image/webp"> <source srcset="{f:uri.image(image:file, maxWidth: 2560, fileExtension: 'jpg')}" media="(min-width: 1601px)" type="image/jpeg"> </f:if> <f:if condition="{dimensions.maxWidth} > 1201"> <source srcset="{f:uri.image(image:file, maxWidth: 1601, fileExtension: 'webp')}" media="(min-width: 1201px)" type="image/webp"> <source srcset="{f:uri.image(image:file, maxWidth: 1600, fileExtension: 'jpg')}" media="(min-width: 1201px)" type="image/jpeg"> </f:if> <f:if condition="{dimensions.maxWidth} > 769"> <source srcset="{f:uri.image(image:file, maxWidth: 1201, fileExtension: 'webp')}" media="(min-width: 769px)" type="image/webp"> <source srcset="{f:uri.image(image:file, maxWidth: 1200, fileExtension: 'jpg')}" media="(min-width: 769px)" type="image/jpeg"> </f:if> <f:if condition="{dimensions.maxWidth} > 481"> <source srcset="{f:uri.image(image:file, maxWidth: 769, fileExtension: 'webp')}" media="(min-width: 481px)" type="image/webp"> <source srcset="{f:uri.image(image:file, maxWidth: 768, fileExtension: 'jpg')}" media="(min-width: 481px)" type="image/jpeg"> </f:if> <source srcset="{f:uri.image(image:file, maxWidth: 481, fileExtension: 'webp')}" type="image/webp"> <source srcset="{f:uri.image(image:file, maxWidth: 480, fileExtension: 'jpg')}" type="image/jpeg"> <img class="image-embed-item" title="{file.title}" alt="{file.description -> f:format.nl2br()}" src="{f:uri.image(image:file, maxWidth: 1600)}" width="{dimensions.width}" height="{dimensions.height}" loading="{settings.media.lazyLoading}" /> </picture> </html>

The processor can then be integrated via TypoScript:

tt_content.textmedia.dataProcessing { // Overwrite original GalleryProcessor 20 = In2code\In2template\DataProcessing\GalleryProcessor }

Back

"Code faster, look at the time" - does this sound familiar to you?

How about time and respect for code quality? Working in a team? Automated tests?

Join us