From f8eb47e974adbd5c1cf5ebf1d5ba9fe85a483ed8 Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Fri, 18 Nov 2022 11:31:12 +0000
Subject: [PATCH] Issue #3192234 by heddn, pivica, Qusai Taha, yogeshmpawar,
 bingolitte, Phil Wolstenholme, smustgrave, Berdir, devkinetic, maximpodorov,
 ranjith_kumar_k_u, nikitagupta, ankithashetty, vsujeetkumar, pooja saraah,
 timohuisman, glynster, Luke.Leber, Ahmad Abbad, flyke, chetanbharambe,
 Graber, zcht, sasanikolic, John Pitcairn, catch, Rar9, Anybody,
 Ambient.Impact, alexpott, manarak, Fabianx, longwave, benmorss, Martijn de
 Wit, Wim Leers, larowlan: Apply width and height attributes to allow
 responsive image tag use loading="lazy"

---
 .../config/schema/responsive_image.schema.yml |   7 +
 .../responsive_image/responsive_image.module  |  51 +++-
 .../responsive_image.post_update.php          |  14 +-
 .../ResponsiveImageFormatter.php              |  38 ++-
 .../src/ResponsiveImageConfigUpdater.php      |  34 +++
 .../src/ResponsiveImageStyleForm.php          |   2 +-
 .../responsive_image-loading-attribute.php    |  70 ++++++
 .../responsive_image_test_module.schema.yml   |   7 +
 .../ResponsiveImageFieldDisplayTest.php       |  66 +++++-
 .../ResponsiveImageLazyLoadUpdateTest.php     |  62 +++++
 .../ResponsiveImageFieldUiTest.php            |   2 +-
 core/modules/views/src/ViewsConfigUpdater.php |  63 +++++
 .../views.view.test_responsive_images.yml     | 224 ++++++++++++++++++
 .../src/Kernel/ViewsConfigUpdaterTest.php     |  69 +++++-
 core/modules/views/views.module               |  11 +
 core/modules/views/views.post_update.php      |  11 +
 ...ntity_view_display.media.image.default.yml |   2 +
 ...iew_display.media.image.responsive_3x2.yml |   2 +
 ...entity_view_display.media.image.square.yml |   2 +
 19 files changed, 715 insertions(+), 22 deletions(-)
 create mode 100644 core/modules/responsive_image/tests/fixtures/update/responsive_image-loading-attribute.php
 create mode 100644 core/modules/responsive_image/tests/src/Functional/ResponsiveImageLazyLoadUpdateTest.php
 create mode 100644 core/modules/views/tests/fixtures/update/views.view.test_responsive_images.yml

diff --git a/core/modules/responsive_image/config/schema/responsive_image.schema.yml b/core/modules/responsive_image/config/schema/responsive_image.schema.yml
index f05a8e2608e7..8fae6175938b 100644
--- a/core/modules/responsive_image/config/schema/responsive_image.schema.yml
+++ b/core/modules/responsive_image/config/schema/responsive_image.schema.yml
@@ -67,3 +67,10 @@ field.formatter.settings.responsive_image:
     image_link:
       type: string
       label: 'Link image to'
+    image_loading:
+      type: mapping
+      label: 'Image loading settings'
+      mapping:
+        attribute:
+          type: string
+          label: 'Loading attribute'
diff --git a/core/modules/responsive_image/responsive_image.module b/core/modules/responsive_image/responsive_image.module
index eab182dc6219..8d48bedf0e3e 100644
--- a/core/modules/responsive_image/responsive_image.module
+++ b/core/modules/responsive_image/responsive_image.module
@@ -5,12 +5,14 @@
  * Responsive image display formatter for image fields.
  */
 
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
 use Drupal\Core\Url;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Logger\RfcLogLevel;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\image\Entity\ImageStyle;
 use Drupal\responsive_image\Entity\ResponsiveImageStyle;
+use Drupal\responsive_image\ResponsiveImageConfigUpdater;
 use Drupal\responsive_image\ResponsiveImageStyleInterface;
 use Drupal\breakpoint\BreakpointInterface;
 
@@ -31,11 +33,11 @@ function responsive_image_help($route_name, RouteMatchInterface $route_match) {
       $output .= '<dt>' . t('Fallback image style') . '</dt>';
       $output .= '<dd>' . t('The fallback image style is typically the smallest size image you expect to appear in this space. The fallback image should only appear on a site if an error occurs.') . '</dd>';
       $output .= '<dt>' . t('Breakpoint groups: viewport sizing vs art direction') . '</dt>';
-      $output .= '<dd>' . t('The breakpoint group typically only needs a single breakpoint with an empty media query in order to do <em>viewport sizing.</em> Multiple breakpoints are used for changing the crop or aspect ratio of images at different viewport sizes, which is often referred to as <em>art direction.</em> Once you select a breakpoint group, you can choose which breakpoints to use for the responsive image style. By default, the option <em>do not use this breakpoint</em> is selected for each breakpoint. See the <a href=":breakpoint_help">help page of the Breakpoint module</a> for more information.', [':breakpoint_help' => Url::fromRoute('help.page', ['name' => 'breakpoint'])->toString()]) . '</dd>';
+      $output .= '<dd>' . t('The breakpoint group typically only needs a single breakpoint with an empty media query in order to do <em>viewport sizing.</em> Multiple breakpoints are used for changing the crop or aspect ratio of images at different viewport sizes, which is often referred to as <em>art direction.</em> A new breakpoint group should be created for each aspect ratio to avoid content shift. Once you select a breakpoint group, you can choose which breakpoints to use for the responsive image style. By default, the option <em>do not use this breakpoint</em> is selected for each breakpoint. See the <a href=":breakpoint_help">help page of the Breakpoint module</a> for more information.', [':breakpoint_help' => Url::fromRoute('help.page', ['name' => 'breakpoint'])->toString()]) . '</dd>';
       $output .= '<dt>' . t('Breakpoint settings: sizes vs image styles') . '</dt>';
-      $output .= '<dd>' . t('While you have the option to provide only one image style per breakpoint, the sizes option allows you to provide more options to browsers as to which image file it can display, even when using multiple breakpoints for art direction. Breakpoints are defined in the configuration files of the theme.') . '</dd>';
+      $output .= '<dd>' . t('While you have the option to provide only one image style per breakpoint, the sizes attribute allows you to provide more options to browsers as to which image file it can display. If using sizes field and art direction, all selected image styles should use the same aspect ratio to avoid content shifting. Breakpoints are defined in the configuration files of the theme.') . '</dd>';
       $output .= '<dt>' . t('Sizes field') . '</dt>';
-      $output .= '<dd>' . t('Once the sizes option is selected, you can let the browser know the size of this image in relation to the site layout, using the <em>Sizes</em> field. For a hero image that always fills the entire screen, you could simply enter 100vw, which means 100% of the viewport width. For an image that fills 90% of the screen for small viewports, but only fills 40% of the screen when the viewport is larger than 40em (typically 640px), you could enter "(min-width: 40em) 40vw, 90vw" in the Sizes field. The last item in the comma-separated list is the smallest viewport size: other items in the comma-separated list should have a media condition paired with an image width. <em>Media conditions</em> are similar to a media query, often a min-width paired with a viewport width using em or px units: e.g. (min-width: 640px) or (min-width: 40em). This is paired with the <em>image width</em> at that viewport size using px, em or vw units. The vw unit is viewport width and is used instead of a percentage because the percentage always refers to the width of the entire viewport.') . '</dd>';
+      $output .= '<dd>' . t('The sizes attribute paired with the srcset attribute provides information on how much space these images take up within the viewport at different browser breakpoints, but the aspect ratios should remain the same across those breakpoints. Once the sizes option is selected, you can let the browser know the size of this image in relation to the site layout, using the <em>Sizes</em> field. For a hero image that always fills the entire screen, you could simply enter 100vw, which means 100% of the viewport width. For an image that fills 90% of the screen for small viewports, but only fills 40% of the screen when the viewport is larger than 40em (typically 640px), you could enter "(min-width: 40em) 40vw, 90vw" in the Sizes field. The last item in the comma-separated list is the smallest viewport size: other items in the comma-separated list should have a media condition paired with an image width. <em>Media conditions</em> are similar to a media query, often a min-width paired with a viewport width using em or px units: e.g. (min-width: 640px) or (min-width: 40em). This is paired with the <em>image width</em> at that viewport size using px, em or vw units. The vw unit is viewport width and is used instead of a percentage because the percentage always refers to the width of the entire viewport.') . '</dd>';
       $output .= '<dt>' . t('Image styles for sizes') . '</dt>';
       $output .= '<dd>' . t('Below the Sizes field you can choose multiple image styles so the browser can choose the best image file size to fill the space defined in the Sizes field. Typically you will want to use image styles that resize your image to have options that range from the smallest px width possible for the space the image will appear in to the largest px width possible, with a variety of widths in between. You may want to provide image styles with widths that are 1.5x to 2x the space available in the layout to account for high resolution screens. Image styles can be defined on the <a href=":image_styles">Image styles page</a> that is provided by the <a href=":image_help">Image module</a>.', [':image_styles' => Url::fromRoute('entity.image_style.collection')->toString(), ':image_help' => Url::fromRoute('help.page', ['name' => 'image'])->toString()]) . '</dd>';
       $output .= '</dl></dd>';
@@ -186,7 +188,20 @@ function template_preprocess_responsive_image(&$variables) {
     $variables['img_element'] = [
       '#theme' => 'image',
       '#uri' => _responsive_image_image_style_url($responsive_image_style->getFallbackImageStyle(), $variables['uri']),
+      '#attributes' => [],
     ];
+
+    // We don't set dimensions for fallback image if rendered in picture tag.
+    // In Firefox, it results in sizing the entire picture element to the size
+    // of the fallback image, instead of the size on the source element.
+    $dimensions = responsive_image_get_image_dimensions($responsive_image_style->getFallbackImageStyle(), [
+      'width' => $variables['width'],
+      'height' => $variables['height'],
+    ],
+      $variables['uri']
+    );
+    $variables['img_element']['#width'] = $dimensions['width'];
+    $variables['img_element']['#height'] = $dimensions['height'];
   }
   else {
     $variables['output_image_tag'] = FALSE;
@@ -195,6 +210,7 @@ function template_preprocess_responsive_image(&$variables) {
     $variables['img_element'] = [
       '#theme' => 'image',
       '#uri' => _responsive_image_image_style_url($responsive_image_style->getFallbackImageStyle(), $variables['uri']),
+      '#attributes' => [],
     ];
   }
 
@@ -362,7 +378,9 @@ function _responsive_image_build_source_attributes(array $variables, BreakpointI
   $sizes = [];
   $srcset = [];
   $derivative_mime_types = [];
-  foreach ($multipliers as $multiplier => $image_style_mapping) {
+  // Traverse the multipliers in reverse so the largest image is processed last.
+  // The last image's dimensions are used for img.srcset height and width.
+  foreach (array_reverse($multipliers) as $multiplier => $image_style_mapping) {
     switch ($image_style_mapping['image_mapping_type']) {
       // Create a <source> tag with the 'sizes' attribute.
       case 'sizes':
@@ -400,6 +418,7 @@ function _responsive_image_build_source_attributes(array $variables, BreakpointI
         // be used. We multiply it by 100 so multipliers with up to two decimals
         // can be used.
         $srcset[intval(mb_substr($multiplier, 0, -1) * 100)] = _responsive_image_image_style_url($image_style_mapping['image_mapping'], $variables['uri']) . ' ' . $multiplier;
+        $dimensions = responsive_image_get_image_dimensions($image_style_mapping['image_mapping'], ['width' => $width, 'height' => $height], $variables['uri']);
         break;
     }
   }
@@ -418,6 +437,21 @@ function _responsive_image_build_source_attributes(array $variables, BreakpointI
   if (!empty($sizes)) {
     $source_attributes->setAttribute('sizes', implode(',', array_unique($sizes)));
   }
+  // The images used in a particular srcset attribute should all have the same
+  // aspect ratio. The sizes attribute paired with the srcset attribute provides
+  // information on how much space these images take up within the viewport at
+  // different breakpoints, but the aspect ratios should remain the same across
+  // those breakpoints. Multiple source elements can be used for art direction,
+  // where aspect ratios should change at particular breakpoints. Each source
+  // element can still have srcset and sizes attributes to handle variations for
+  // that particular aspect ratio. Because the same aspect ratio is assumed for
+  // all images in a srcset, dimensions are always added to the source
+  // attribute. Within srcset, images are sorted from largest to smallest in
+  // terms of the real dimension of the image.
+  if (!empty($dimensions['width']) && !empty($dimensions['height'])) {
+    $source_attributes->setAttribute('width', $dimensions['width']);
+    $source_attributes->setAttribute('height', $dimensions['height']);
+  }
   return $source_attributes;
 }
 
@@ -508,3 +542,12 @@ function responsive_image_library_info_alter(array &$libraries, $module) {
     $libraries['drupal.ajax']['dependencies'][] = 'responsive_image/ajax';
   }
 }
+
+/**
+ * Implements hook_ENTITY_TYPE_presave() for entity_view_display.
+ */
+function responsive_image_entity_view_display_presave(EntityViewDisplayInterface $view_display): void {
+  /** @var \Drupal\responsive_image\ResponsiveImageConfigUpdater $config_updater */
+  $config_updater = \Drupal::classResolver(ResponsiveImageConfigUpdater::class);
+  $config_updater->processResponsiveImageField($view_display);
+}
diff --git a/core/modules/responsive_image/responsive_image.post_update.php b/core/modules/responsive_image/responsive_image.post_update.php
index 0c790e51e978..c71ae03b5699 100644
--- a/core/modules/responsive_image/responsive_image.post_update.php
+++ b/core/modules/responsive_image/responsive_image.post_update.php
@@ -6,6 +6,7 @@
  */
 
 use Drupal\Core\Config\Entity\ConfigEntityUpdater;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
 use Drupal\responsive_image\ResponsiveImageConfigUpdater;
 use Drupal\responsive_image\ResponsiveImageStyleInterface;
 
@@ -22,10 +23,21 @@ function responsive_image_removed_post_updates() {
  * Re-order mappings by breakpoint ID and descending numeric multiplier order.
  */
 function responsive_image_post_update_order_multiplier_numerically(array &$sandbox = NULL): void {
+  /** @var \Drupal\responsive_image\ResponsiveImageConfigUpdater $responsive_image_config_updater */
   $responsive_image_config_updater = \Drupal::classResolver(ResponsiveImageConfigUpdater::class);
-  assert($responsive_image_config_updater instanceof ResponsiveImageConfigUpdater);
   $responsive_image_config_updater->setDeprecationsEnabled(FALSE);
   \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'responsive_image_style', function (ResponsiveImageStyleInterface $responsive_image_style) use ($responsive_image_config_updater): bool {
     return $responsive_image_config_updater->orderMultipliersNumerically($responsive_image_style);
   });
 }
+
+/**
+ * Add the image loading settings to responsive image field formatter instances.
+ */
+function responsive_image_post_update_image_loading_attribute(array &$sandbox = NULL): void {
+  $responsive_image_config_updater = \Drupal::classResolver(ResponsiveImageConfigUpdater::class);
+  $responsive_image_config_updater->setDeprecationsEnabled(FALSE);
+  \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'entity_view_display', function (EntityViewDisplayInterface $view_display) use ($responsive_image_config_updater): bool {
+    return $responsive_image_config_updater->processResponsiveImageField($view_display);
+  });
+}
diff --git a/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php b/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php
index 4c625da4fcc4..5aeb7984ba00 100644
--- a/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php
+++ b/core/modules/responsive_image/src/Plugin/Field/FieldFormatter/ResponsiveImageFormatter.php
@@ -115,6 +115,9 @@ public static function defaultSettings() {
     return [
       'responsive_image_style' => '',
       'image_link' => '',
+      'image_loading' => [
+        'attribute' => 'lazy',
+      ],
     ] + parent::defaultSettings();
   }
 
@@ -122,6 +125,8 @@ public static function defaultSettings() {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state) {
+    $elements = parent::settingsForm($form, $form_state);
+
     $responsive_image_options = [];
     $responsive_image_styles = $this->responsiveImageStyleStorage->loadMultiple();
     uasort($responsive_image_styles, '\Drupal\responsive_image\Entity\ResponsiveImageStyle::sort');
@@ -145,6 +150,29 @@ public function settingsForm(array $form, FormStateInterface $form_state) {
       ],
     ];
 
+    $image_loading = $this->getSetting('image_loading');
+    $elements['image_loading'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Image loading'),
+      '#weight' => 10,
+      '#description' => $this->t('Lazy render images with native image loading attribute (<em>loading="lazy"</em>). This improves performance by allowing browsers to lazily load images. See <a href="@url">Lazy loading</a>.', [
+        '@url' => 'https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading#images_and_iframes',
+      ]),
+    ];
+    $loading_attribute_options = [
+      'lazy' => $this->t('Lazy'),
+      'eager' => $this->t('Eager'),
+    ];
+    $elements['image_loading']['attribute'] = [
+      '#title' => $this->t('Lazy loading attribute'),
+      '#type' => 'select',
+      '#default_value' => $image_loading['attribute'],
+      '#options' => $loading_attribute_options,
+      '#description' => $this->t('Select the lazy loading attribute for images. <a href=":link">Learn more.</a>', [
+        ':link' => 'https://html.spec.whatwg.org/multipage/urls-and-fetching.html#lazy-loading-attributes',
+      ]),
+    ];
+
     $link_types = [
       'content' => $this->t('Content'),
       'file' => $this->t('File'),
@@ -183,7 +211,12 @@ public function settingsSummary() {
       $summary[] = $this->t('Select a responsive image style.');
     }
 
-    return $summary;
+    $image_loading = $this->getSetting('image_loading');
+    $summary[] = $this->t('Loading attribute: @attribute', [
+      '@attribute' => $image_loading['attribute'],
+    ]);
+
+    return array_merge($summary, parent::settingsSummary());
   }
 
   /**
@@ -236,6 +269,9 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
       $item_attributes = $item->_attributes;
       unset($item->_attributes);
 
+      $image_loading_settings = $this->getSetting('image_loading');
+      $item_attributes['loading'] = $image_loading_settings['attribute'];
+
       $elements[$delta] = [
         '#theme' => 'responsive_image_formatter',
         '#item' => $item,
diff --git a/core/modules/responsive_image/src/ResponsiveImageConfigUpdater.php b/core/modules/responsive_image/src/ResponsiveImageConfigUpdater.php
index e1d8ff06d14b..0e1f09dee93c 100644
--- a/core/modules/responsive_image/src/ResponsiveImageConfigUpdater.php
+++ b/core/modules/responsive_image/src/ResponsiveImageConfigUpdater.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\responsive_image;
 
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+
 /**
  * Provides a BC layer for modules providing old configurations.
  *
@@ -68,4 +70,36 @@ public function orderMultipliersNumerically(ResponsiveImageStyleInterface $respo
     return $changed;
   }
 
+  /**
+   * Processes responsive image type fields.
+   *
+   * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $view_display
+   *   The view display.
+   *
+   * @return bool
+   *   Whether the display was updated.
+   */
+  public function processResponsiveImageField(EntityViewDisplayInterface $view_display): bool {
+    $changed = FALSE;
+
+    foreach ($view_display->getComponents() as $field => $component) {
+      if (isset($component['type'])
+        && $component['type'] === 'responsive_image'
+        && !array_key_exists('image_loading', $component['settings'])
+      ) {
+        $component['settings']['image_loading']['attribute'] = 'eager';
+        $view_display->setComponent($field, $component);
+        $changed = TRUE;
+      }
+    }
+
+    $deprecations_triggered = &$this->triggeredDeprecations['3192234'][$view_display->id()];
+    if ($this->deprecationsEnabled && $changed && !$deprecations_triggered) {
+      $deprecations_triggered = TRUE;
+      @trigger_error(sprintf('The responsive image loading attribute update for "%s" is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Configuration should be updated to accommodate the changes described at https://www.drupal.org/node/3279032.', $view_display->id()), E_USER_DEPRECATED);
+    }
+
+    return $changed;
+  }
+
 }
diff --git a/core/modules/responsive_image/src/ResponsiveImageStyleForm.php b/core/modules/responsive_image/src/ResponsiveImageStyleForm.php
index 7bceff174ec4..27e9b9e2709d 100644
--- a/core/modules/responsive_image/src/ResponsiveImageStyleForm.php
+++ b/core/modules/responsive_image/src/ResponsiveImageStyleForm.php
@@ -199,7 +199,7 @@ public function form(array $form, FormStateInterface $form_state) {
       '#default_value' => $responsive_image_style->getFallbackImageStyle(),
       '#options' => $image_styles,
       '#required' => TRUE,
-      '#description' => $this->t('Select the smallest image style you expect to appear in this space. The fallback image style should only appear on the site if an error occurs.'),
+      '#description' => $this->t('Select the image style you wish to use as the style when a browser does not support responsive images.'),
     ];
 
     $form['#tree'] = TRUE;
diff --git a/core/modules/responsive_image/tests/fixtures/update/responsive_image-loading-attribute.php b/core/modules/responsive_image/tests/fixtures/update/responsive_image-loading-attribute.php
new file mode 100644
index 000000000000..213d10816a0c
--- /dev/null
+++ b/core/modules/responsive_image/tests/fixtures/update/responsive_image-loading-attribute.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * @file
+ * Test lazy load update by modifying an image field form display.
+ */
+
+use Drupal\Core\Database\Database;
+
+$connection = Database::getConnection();
+
+// Add a responsive image style.
+$styles = [];
+$styles['langcode'] = 'en';
+$styles['status'] = TRUE;
+$styles['dependencies']['config'][] = 'image.style.large';
+$styles['dependencies']['config'][] = 'image.style.medium';
+$styles['dependencies']['config'][] = 'image.style.thumbnail';
+$styles['id'] = 'responsive_image_style';
+$styles['uuid'] = '46225242-eb4c-4b10-9a8c-966130b18630';
+$styles['label'] = 'Responsive Image Style';
+$styles['breakpoint_group'] = 'responsive_image';
+$styles['fallback_image_style'] = 'medium';
+$styles['image_style_mappings'] = [
+  [
+    'image_mapping_type' => 'sizes',
+    'image_mapping' => [
+      'sizes' => '100vw',
+      'sizes_image_styles' => [
+        'large',
+        'medium',
+        'thumbnail',
+      ],
+    ],
+    'breakpoint_id' => 'responsive_image.viewport_sizing',
+    'multiplier' => '1x',
+  ],
+];
+
+$connection->insert('config')
+  ->fields([
+    'collection',
+    'name',
+    'data',
+  ])
+  ->values([
+    'collection' => '',
+    'name' => 'responsive_image.styles.responsive_image_style',
+    'data' => serialize($styles),
+  ])
+  ->execute();
+
+// Update article view display to use responsive_image.
+$article_form_display = $connection->select('config')
+  ->fields('config', ['data'])
+  ->condition('collection', '')
+  ->condition('name', 'core.entity_view_display.node.article.default')
+  ->execute()
+  ->fetchField();
+$article_form_display = unserialize($article_form_display);
+$article_form_display['content']['field_image']['type'] = 'responsive_image';
+$article_form_display['content']['field_image']['settings'] = [
+  'responsive_image_style' => 'responsive_image_style',
+  'image_link' => '',
+];
+$connection->update('config')
+  ->fields(['data' => serialize($article_form_display)])
+  ->condition('collection', '')
+  ->condition('name', 'core.entity_view_display.node.article.default')
+  ->execute();
diff --git a/core/modules/responsive_image/tests/modules/responsive_image_test_module/config/schema/responsive_image_test_module.schema.yml b/core/modules/responsive_image/tests/modules/responsive_image_test_module/config/schema/responsive_image_test_module.schema.yml
index 62080127352d..549a1bb928e6 100644
--- a/core/modules/responsive_image/tests/modules/responsive_image_test_module/config/schema/responsive_image_test_module.schema.yml
+++ b/core/modules/responsive_image/tests/modules/responsive_image_test_module/config/schema/responsive_image_test_module.schema.yml
@@ -9,3 +9,10 @@ field.formatter.settings.responsive_image_test:
     image_link:
       type: string
       label: 'Link image to'
+    image_loading:
+      type: mapping
+      label: 'Image loading settings'
+      mapping:
+        attribute:
+          type: string
+          label: 'Loading attribute'
diff --git a/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php b/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php
index 8abca68b22ad..e66978284a0e 100644
--- a/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php
+++ b/core/modules/responsive_image/tests/src/Functional/ResponsiveImageFieldDisplayTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\responsive_image\Functional;
 
 use Drupal\image\Entity\ImageStyle;
+use Drupal\image\ImageStyleInterface;
 use Drupal\node\Entity\Node;
 use Drupal\file\Entity\File;
 use Drupal\responsive_image\Plugin\Field\FieldFormatter\ResponsiveImageFormatter;
@@ -317,6 +318,11 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles =
       $medium_style = ImageStyle::load('medium');
       $this->assertSession()->responseContains($this->fileUrlGenerator->transformRelative($medium_style->buildUrl($image_uri)) . ' 220w, ' . $this->fileUrlGenerator->transformRelative($large_style->buildUrl($image_uri)) . ' 360w');
       $this->assertSession()->responseContains('media="(min-width: 851px)"');
+      // Assert the output of the 'width' attribute.
+      $this->assertSession()->responseContains('width="360"');
+      // Assert the output of the 'height' attribute.
+      $this->assertSession()->responseContains('height="240"');
+      $this->assertSession()->responseContains('loading="lazy"');
     }
     $this->assertSession()->responseContains('/styles/large/');
     $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:responsive_image.styles.style_one');
@@ -333,6 +339,7 @@ protected function doTestResponsiveImageFieldFormatters($scheme, $empty_styles =
       '#theme' => 'image',
       '#alt' => $alt,
       '#uri' => $this->fileUrlGenerator->transformRelative($large_style->buildUrl($image->getSource())),
+      '#attributes' => ['loading' => 'lazy'],
     ];
     // The image.html.twig template has a newline after the <img> tag but
     // responsive-image.html.twig doesn't have one after the fallback image, so
@@ -416,18 +423,44 @@ public function testResponsiveImageFieldFormattersEmptyMediaQuery() {
   }
 
   /**
-   * Tests responsive image formatter on node display with one source.
+   * Tests responsive image formatter on node display with one and two sources.
    */
-  public function testResponsiveImageFieldFormattersOneSource() {
+  public function testResponsiveImageFieldFormattersMultipleSources() {
+    // Setup known image style sizes so the test can assert on known sizes.
+    $large_style = ImageStyle::load('large');
+    assert($large_style instanceof ImageStyleInterface);
+    $large_style->addImageEffect([
+      'id' => 'image_resize',
+      'data' => [
+        'width' => '480',
+        'height' => '480',
+      ],
+    ]);
+    $large_style->save();
+    $medium_style = ImageStyle::load('medium');
+    assert($medium_style instanceof ImageStyleInterface);
+    $medium_style->addImageEffect([
+      'id' => 'image_resize',
+      'data' => [
+        'width' => '220',
+        'height' => '220',
+      ],
+    ]);
+    $medium_style->save();
+
     $this->responsiveImgStyle
       // Test the output of an empty media query.
       ->addImageStyleMapping('responsive_image_test_module.empty', '1x', [
         'image_mapping_type' => 'image_style',
-        'image_mapping' => 'medium',
+        'image_mapping' => $medium_style->id(),
+      ])
+      ->addImageStyleMapping('responsive_image_test_module.empty', '1.5x', [
+        'image_mapping_type' => 'image_style',
+        'image_mapping' => $large_style->id(),
       ])
       ->addImageStyleMapping('responsive_image_test_module.empty', '2x', [
         'image_mapping_type' => 'image_style',
-        'image_mapping' => 'large',
+        'image_mapping' => $large_style->id(),
       ])
       ->save();
     $node_storage = $this->container->get('entity_type.manager')->getStorage('node');
@@ -444,6 +477,10 @@ public function testResponsiveImageFieldFormattersOneSource() {
       'settings' => [
         'image_link' => '',
         'responsive_image_style' => 'style_one',
+        'image_loading' => [
+          // Test the image loading default option can be overridden.
+          'attribute' => 'eager',
+        ],
       ],
     ];
     $display = \Drupal::service('entity_display.repository')
@@ -454,12 +491,25 @@ public function testResponsiveImageFieldFormattersOneSource() {
     // View the node.
     $this->drupalGet('node/' . $nid);
 
-    // Assert the media attribute is present if it has a value.
-    $large_style = ImageStyle::load('large');
-    $medium_style = ImageStyle::load('medium');
+    // Assert the img tag has medium and large images and fallback dimensions
+    // from the large image style are used.
     $node = $node_storage->load($nid);
     $image_uri = File::load($node->{$field_name}->target_id)->getFileUri();
-    $this->assertSession()->responseContains('<img srcset="' . $this->fileUrlGenerator->transformRelative($medium_style->buildUrl($image_uri)) . ' 1x, ' . $this->fileUrlGenerator->transformRelative($large_style->buildUrl($image_uri)) . ' 2x"');
+    $medium_transform_url = $this->fileUrlGenerator->transformRelative($medium_style->buildUrl($image_uri));
+    $large_transform_url = $this->fileUrlGenerator->transformRelative($large_style->buildUrl($image_uri));
+    $this->assertSession()->responseMatches('/<img loading="eager" srcset="' . \preg_quote($medium_transform_url, '/') . ' 1x, ' . \preg_quote($large_transform_url, '/') . ' 1.5x, ' . \preg_quote($large_transform_url, '/') . ' 2x" width="220" height="220" src="' . \preg_quote($large_transform_url, '/') . '" alt="\w+" \/>/');
+
+    $this->responsiveImgStyle
+      // Test the output of an empty media query.
+      ->addImageStyleMapping('responsive_image_test_module.wide', '1x', [
+        'image_mapping_type' => 'image_style',
+        'image_mapping' => $large_style->id(),
+      ])
+      ->save();
+
+    // Assert the picture tag has source tags that include dimensions.
+    $this->drupalGet('node/' . $nid);
+    $this->assertSession()->responseMatches('/<picture>\s+<source srcset="' . \preg_quote($large_transform_url, '/') . ' 1x" media="\(min-width: 851px\)" type="image\/png" width="480" height="480"\/>\s+<source srcset="' . \preg_quote($medium_transform_url, '/') . ' 1x, ' . \preg_quote($large_transform_url, '/') . ' 1.5x, ' . \preg_quote($large_transform_url, '/') . ' 2x" type="image\/png" width="220" height="220"\/>\s+<img loading="eager" src="' . \preg_quote($large_transform_url, '/') . '" alt="\w+" \/>\s+<\/picture>/');
   }
 
   /**
diff --git a/core/modules/responsive_image/tests/src/Functional/ResponsiveImageLazyLoadUpdateTest.php b/core/modules/responsive_image/tests/src/Functional/ResponsiveImageLazyLoadUpdateTest.php
new file mode 100644
index 000000000000..cbf90e75779b
--- /dev/null
+++ b/core/modules/responsive_image/tests/src/Functional/ResponsiveImageLazyLoadUpdateTest.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Drupal\Tests\responsive_image\Functional;
+
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Drupal\FunctionalTests\Update\UpdatePathTestBase;
+
+/**
+ * Tests lazy-load upgrade path.
+ *
+ * @coversDefaultClass \Drupal\responsive_image\ResponsiveImageConfigUpdater
+ *
+ * @group responsive_image
+ * @group legacy
+ */
+class ResponsiveImageLazyLoadUpdateTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles(): void {
+    $this->databaseDumpFiles = [
+      __DIR__ . '/../../../../system/tests/fixtures/update/drupal-9.4.0.filled.standard.php.gz',
+      __DIR__ . '/../../fixtures/update/responsive_image.php',
+      __DIR__ . '/../../fixtures/update/responsive_image-loading-attribute.php',
+    ];
+  }
+
+  /**
+   * Test new lazy-load setting upgrade path.
+   *
+   * @see responsive_image_post_update_image_loading_attribute
+   */
+  public function testUpdate(): void {
+    $data = EntityViewDisplay::load('node.article.default')->toArray();
+    $this->assertArrayNotHasKey('image_loading', $data['content']['field_image']['settings']);
+
+    $this->runUpdates();
+
+    $data = EntityViewDisplay::load('node.article.default')->toArray();
+    $this->assertArrayHasKey('image_loading', $data['content']['field_image']['settings']);
+    $this->assertEquals('eager', $data['content']['field_image']['settings']['image_loading']['attribute']);
+  }
+
+  /**
+   * Test responsive_image_entity_view_display_presave invokes deprecations.
+   *
+   * @covers ::processResponsiveImageField
+   */
+  public function testEntitySave(): void {
+    $this->expectDeprecation('The responsive image loading attribute update for "node.article.default" is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Configuration should be updated to accommodate the changes described at https://www.drupal.org/node/3279032.');
+    $view_display = EntityViewDisplay::load('node.article.default');
+    $this->assertArrayNotHasKey('image_loading', $view_display->toArray()['content']['field_image']['settings']);
+
+    $view_display->save();
+
+    $view_display = EntityViewDisplay::load('node.article.default');
+    $this->assertArrayHasKey('image_loading', $view_display->toArray()['content']['field_image']['settings']);
+    $this->assertEquals('eager', $view_display->toArray()['content']['field_image']['settings']['image_loading']['attribute']);
+  }
+
+}
diff --git a/core/modules/responsive_image/tests/src/FunctionalJavascript/ResponsiveImageFieldUiTest.php b/core/modules/responsive_image/tests/src/FunctionalJavascript/ResponsiveImageFieldUiTest.php
index bc6bef3baaf5..5a2d5cf72396 100644
--- a/core/modules/responsive_image/tests/src/FunctionalJavascript/ResponsiveImageFieldUiTest.php
+++ b/core/modules/responsive_image/tests/src/FunctionalJavascript/ResponsiveImageFieldUiTest.php
@@ -102,7 +102,7 @@ public function testResponsiveImageFormatterUi() {
     $field_image_type->setValue('responsive_image');
 
     $summary_text = $assert_session->waitForElement('xpath', $this->cssSelectToXpath('#field-image .ajax-new-content .field-plugin-summary'));
-    $this->assertEquals('Select a responsive image style.', $summary_text->getText());
+    $this->assertEquals('Select a responsive image style. Loading attribute: lazy', $summary_text->getText());
 
     $page->pressButton('Save');
     $assert_session->responseContains("Select a responsive image style.");
diff --git a/core/modules/views/src/ViewsConfigUpdater.php b/core/modules/views/src/ViewsConfigUpdater.php
index c0f5b4c55ae5..11b33752280e 100644
--- a/core/modules/views/src/ViewsConfigUpdater.php
+++ b/core/modules/views/src/ViewsConfigUpdater.php
@@ -110,6 +110,69 @@ public function setDeprecationsEnabled($enabled) {
     $this->deprecationsEnabled = $enabled;
   }
 
+  /**
+   * Performs all required updates.
+   *
+   * @param \Drupal\views\ViewEntityInterface $view
+   *   The View to update.
+   *
+   * @return bool
+   *   Whether the view was updated.
+   */
+  public function updateAll(ViewEntityInterface $view) {
+    return $this->processDisplayHandlers($view, FALSE, function (&$handler, $handler_type, $key, $display_id) use ($view) {
+      $changed = FALSE;
+      if ($this->processResponsiveImageLazyLoadFieldHandler($handler, $handler_type, $view)) {
+        $changed = TRUE;
+      }
+      return $changed;
+    });
+  }
+
+  /**
+   * Add lazy load options to all responsive_image type field configurations.
+   *
+   * @param \Drupal\views\ViewEntityInterface $view
+   *   The View to update.
+   *
+   * @return bool
+   *   Whether the view was updated.
+   */
+  public function needsResponsiveImageLazyLoadFieldUpdate(ViewEntityInterface $view): bool {
+    return $this->processDisplayHandlers($view, TRUE, function (&$handler, $handler_type) use ($view) {
+      return $this->processResponsiveImageLazyLoadFieldHandler($handler, $handler_type, $view);
+    });
+  }
+
+  /**
+   * Processes responsive_image type fields.
+   *
+   * @param array $handler
+   *   A display handler.
+   * @param string $handler_type
+   *   The handler type.
+   * @param \Drupal\views\ViewEntityInterface $view
+   *   The View being updated.
+   *
+   * @return bool
+   *   Whether the handler was updated.
+   */
+  protected function processResponsiveImageLazyLoadFieldHandler(array &$handler, string $handler_type, ViewEntityInterface $view): bool {
+    $changed = FALSE;
+
+    // Add any missing settings for lazy loading.
+    if (($handler_type === 'field')
+      && isset($handler['plugin_id'], $handler['type'])
+      && $handler['plugin_id'] === 'field'
+      && $handler['type'] === 'responsive_image'
+      && !isset($handler['settings']['image_loading'])) {
+      $handler['settings']['image_loading'] = ['attribute' => 'eager'];
+      $changed = TRUE;
+    }
+
+    return $changed;
+  }
+
   /**
    * Processes all display handlers.
    *
diff --git a/core/modules/views/tests/fixtures/update/views.view.test_responsive_images.yml b/core/modules/views/tests/fixtures/update/views.view.test_responsive_images.yml
new file mode 100644
index 000000000000..6537c27184af
--- /dev/null
+++ b/core/modules/views/tests/fixtures/update/views.view.test_responsive_images.yml
@@ -0,0 +1,224 @@
+uuid: 6a7eb126-7ba9-493f-a209-e3aa0672b8f5
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.storage.entity_test.bar
+    - responsive_image.styles.responsive_image_style_id
+  module:
+    - entity_test
+    - responsive_image
+id: test_responsive_images
+label: 'Responsive Images'
+module: views
+description: ''
+tag: ''
+base_table: entity_test
+base_field: id
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Default
+    position: 0
+    display_options:
+      access:
+        type: none
+        options: {  }
+      cache:
+        type: tag
+        options: {  }
+      query:
+        type: views_query
+        options:
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_comment: ''
+          query_tags: {  }
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Apply
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      pager:
+        type: mini
+        options:
+          items_per_page: 10
+          offset: 0
+          id: 0
+          total_pages: null
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+          tags:
+            previous: ‹‹
+            next: ››
+      style:
+        type: default
+        options:
+          grouping: {  }
+          row_class: ''
+          default_row_class: true
+          uses_fields: false
+      row:
+        type: fields
+        options:
+          inline: {  }
+          separator: ''
+          hide_empty: false
+          default_field_elements: true
+      fields:
+        name:
+          table: entity_test
+          field: name
+          id: name
+          entity_type: null
+          entity_field: name
+          plugin_id: field
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          settings: {  }
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+        bar:
+          id: bar
+          table: entity_test__bar
+          field: bar
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: target_id
+          type: responsive_image
+          settings:
+            responsive_image_style: responsive_image_style_id
+            image_link: ''
+          group_column: ''
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          plugin_id: field
+      filters: {  }
+      sorts: {  }
+      header: {  }
+      footer: {  }
+      empty: {  }
+      relationships: {  }
+      arguments: {  }
+      display_extenders: {  }
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - entity_test_view_grants
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url.query_args
diff --git a/core/modules/views/tests/src/Kernel/ViewsConfigUpdaterTest.php b/core/modules/views/tests/src/Kernel/ViewsConfigUpdaterTest.php
index 6b6ecf9aedf1..ac67671eb623 100644
--- a/core/modules/views/tests/src/Kernel/ViewsConfigUpdaterTest.php
+++ b/core/modules/views/tests/src/Kernel/ViewsConfigUpdaterTest.php
@@ -3,6 +3,11 @@
 namespace Drupal\Tests\views\Kernel;
 
 use Drupal\Core\Config\FileStorage;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\responsive_image\Entity\ResponsiveImageStyle;
+use Drupal\Tests\responsive_image\Functional\ViewsIntegrationTest;
+use Drupal\views\ViewsConfigUpdater;
 
 /**
  * @coversDefaultClass \Drupal\views\ViewsConfigUpdater
@@ -13,13 +18,65 @@
 class ViewsConfigUpdaterTest extends ViewsKernelTestBase {
 
   /**
-   * Dummy test to keep this test file with the loadTestView method.
-   *
-   * @see https://www.drupal.org/project/drupal/issues/3261245
-   * @todo Remove the dummyTest function when this class contains a real test.
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'views_config_entity_test',
+    'entity_test',
+    'breakpoint',
+    'field',
+    'file',
+    'image',
+    'responsive_image',
+    'responsive_image_test_module',
+  ];
+
+  /**
+   * @covers ::needsResponsiveImageLazyLoadFieldUpdate
    */
-  public function testPass() {
-    $this->assertTrue(TRUE);
+  public function testNeedsResponsiveImageLazyLoadFieldUpdate(): void {
+    $config_updater = $this->container
+      ->get('class_resolver')
+      ->getInstanceFromDefinition(ViewsConfigUpdater::class);
+    assert($config_updater instanceof ViewsConfigUpdater);
+
+    FieldStorageConfig::create([
+      'field_name' => 'user_picture',
+      'entity_type' => 'user',
+      'type' => 'image',
+    ])->save();
+    FieldConfig::create([
+      'entity_type' => 'user',
+      'field_name' => 'user_picture',
+      'file_directory' => 'pictures/[date:custom:Y]-[date:custom:m]',
+      'bundle' => 'user',
+    ])->save();
+
+    // Create a responsive image style.
+    ResponsiveImageStyle::create([
+      'id' => ViewsIntegrationTest::RESPONSIVE_IMAGE_STYLE_ID,
+      'label' => 'Foo',
+      'breakpoint_group' => 'responsive_image_test_module',
+    ]);
+    // Create an image field to be used with a responsive image formatter.
+    FieldStorageConfig::create([
+      'type' => 'image',
+      'entity_type' => 'entity_test',
+      'field_name' => 'bar',
+    ])->save();
+    FieldConfig::create([
+      'entity_type' => 'entity_test',
+      'bundle' => 'entity_test',
+      'field_name' => 'bar',
+    ])->save();
+
+    $test_view = $this->loadTestView('views.view.test_responsive_images');
+    $needs_update = $config_updater->needsResponsiveImageLazyLoadFieldUpdate($test_view);
+    $test_view->save();
+    $this->assertTrue($needs_update);
+
+    $default_display = $test_view->getDisplay('default');
+    self::assertEquals('eager', $default_display['display_options']['fields']['bar']['settings']['image_loading']['attribute']);
   }
 
   /**
diff --git a/core/modules/views/views.module b/core/modules/views/views.module
index e66c677e356c..d331407e15e9 100644
--- a/core/modules/views/views.module
+++ b/core/modules/views/views.module
@@ -12,9 +12,11 @@
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Url;
 use Drupal\views\Plugin\Derivative\ViewsLocalTask;
+use Drupal\views\ViewEntityInterface;
 use Drupal\views\ViewExecutable;
 use Drupal\views\Entity\View;
 use Drupal\views\Views;
+use Drupal\views\ViewsConfigUpdater;
 
 /**
  * Implements hook_help().
@@ -815,3 +817,12 @@ function views_view_delete(EntityInterface $entity) {
     }
   }
 }
+
+/**
+ * Implements hook_ENTITY_TYPE_presave().
+ */
+function views_view_presave(ViewEntityInterface $view) {
+  /** @var \Drupal\views\ViewsConfigUpdater $config_updater */
+  $config_updater = \Drupal::classResolver(ViewsConfigUpdater::class);
+  $config_updater->updateAll($view);
+}
diff --git a/core/modules/views/views.post_update.php b/core/modules/views/views.post_update.php
index ddb0a807263d..50cf71df1fa8 100644
--- a/core/modules/views/views.post_update.php
+++ b/core/modules/views/views.post_update.php
@@ -52,3 +52,14 @@ function views_post_update_oembed_eager_load(?array &$sandbox = NULL): void {
     return $view_config_updater->needsOembedEagerLoadFieldUpdate($view);
   });
 }
+
+/**
+ * Add lazy load options to all responsive image type field configurations.
+ */
+function views_post_update_responsive_image_lazy_load(?array &$sandbox = NULL): void {
+  /** @var \Drupal\views\ViewsConfigUpdater $view_config_updater */
+  $view_config_updater = \Drupal::classResolver(ViewsConfigUpdater::class);
+  \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function (ViewEntityInterface $view) use ($view_config_updater): bool {
+    return $view_config_updater->needsResponsiveImageLazyLoadFieldUpdate($view);
+  });
+}
diff --git a/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.default.yml b/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.default.yml
index 16a9d5a4c461..1496bfa2c8f9 100644
--- a/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.default.yml
+++ b/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.default.yml
@@ -18,6 +18,8 @@ content:
     settings:
       responsive_image_style: 3_2_image
       image_link: ''
+      image_loading:
+        attribute: lazy
     third_party_settings: {  }
     weight: 1
     region: content
diff --git a/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.responsive_3x2.yml b/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.responsive_3x2.yml
index 6a55f5fd2ffc..90138d2bc572 100644
--- a/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.responsive_3x2.yml
+++ b/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.responsive_3x2.yml
@@ -24,6 +24,8 @@ content:
     settings:
       responsive_image_style: 3_2_image
       image_link: ''
+      image_loading:
+        attribute: lazy
     third_party_settings: {  }
     weight: 1
     region: content
diff --git a/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.square.yml b/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.square.yml
index 0b01d6c50b28..a25cc1bb7bc7 100644
--- a/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.square.yml
+++ b/core/profiles/demo_umami/config/install/core.entity_view_display.media.image.square.yml
@@ -24,6 +24,8 @@ content:
     settings:
       responsive_image_style: square
       image_link: ''
+      image_loading:
+        attribute: lazy
     third_party_settings: {  }
     weight: 1
     region: content
-- 
GitLab