jenkins-bot has submitted this change and it was merged.

Change subject: Lazy load images (only JS, when configured)
......................................................................


Lazy load images (only JS, when configured)

* When configured with wgMFLazyLoadImages apply lazy loading
  * Both on client and server
* Images are rewritten to have a HTML only fallback and a placeholder for JS
enabled clients
* Images are loaded with JS when they are in viewport
  * On scroll or resize triggers the recalculation.
* Guard against width and height not existing in the original image markup
* Take into account disabled images to not do any of this
* Section collapsing now triggers a resize (it resizes the height of the page)
  to help trigger adjustments in content (like lazy loading)

Notes:
* setTimeout applied to 100 magically to always trigger CSS transition :(
* Only apply lazy loading to images after lead section in followup patch
  * https://phabricator.wikimedia.org/T124770#2011918
* Why this is happening in MobileFormatter instead of in parser hooks, and
  upstreaming plans: https://phabricator.wikimedia.org/T124770#2007602

Bug: T124770
Change-Id: I8b6e68a47f6ee081776bee28d824d51a183d7607
---
M extension.json
M includes/MobileFormatter.php
M includes/MobileFrontend.hooks.php
M resources/mobile.startup/Skin.js
M resources/mobile.toggle/toggle.js
A resources/skins.minerva.base.styles/images.less
6 files changed, 187 insertions(+), 6 deletions(-)

Approvals:
  Jdlrobson: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/extension.json b/extension.json
index df345f1..c45ea91 100644
--- a/extension.json
+++ b/extension.json
@@ -128,7 +128,8 @@
                                
"resources/skins.minerva.base.styles/pageactions.less",
                                
"resources/skins.minerva.base.styles/footer.less",
                                
"resources/skins.minerva.base.styles/common.less",
-                               "resources/skins.minerva.base.styles/icons.less"
+                               
"resources/skins.minerva.base.styles/icons.less",
+                               
"resources/skins.minerva.base.styles/images.less"
                        ]
                },
                "skins.minerva.content.styles": {
@@ -599,6 +600,7 @@
                                "resources/mobile.startup/panel.less"
                        ],
                        "scripts": [
+                               "resources/mobile.startup/util.js",
                                "resources/mobile.startup/Router.js",
                                "resources/mobile.startup/OverlayManager.js",
                                "resources/mobile.startup/PageGateway.js",
@@ -611,8 +613,7 @@
                                "resources/mobile.startup/Thumbnail.js",
                                "resources/mobile.startup/Page.js",
                                "resources/mobile.startup/Skin.js",
-                               "resources/mobile.startup/Schema.js",
-                               "resources/mobile.startup/util.js"
+                               "resources/mobile.startup/Schema.js"
                        ],
                        "position": "bottom"
                },
diff --git a/includes/MobileFormatter.php b/includes/MobileFormatter.php
index 3193aed..714f47f 100644
--- a/includes/MobileFormatter.php
+++ b/includes/MobileFormatter.php
@@ -118,11 +118,59 @@
 
                if ( $this->removeMedia ) {
                        $this->doRemoveImages();
+               } else {
+                       $mfLazyLoadImages = $ctx->getMFConfig()
+                               ->get( 'MFLazyLoadImages' );
+
+                       if (
+                                       $mfLazyLoadImages['base'] ||
+                                       ( $ctx->isBetaGroupMember() && 
$mfLazyLoadImages['beta'] )
+                       ) {
+                               $this->doRewriteImagesForLazyLoading();
+                       }
                }
+
                return parent::filterContent();
        }
 
        /**
+        * Enables images to be loaded asynchronously
+        */
+       private function doRewriteImagesForLazyLoading() {
+               $doc = $this->getDoc();
+
+               foreach ( $doc->getElementsByTagName( 'img' ) as $img ) {
+                       $parent = $img->parentNode;
+                       $width = $img->getAttribute( 'width' );
+                       $height = $img->getAttribute( 'height' );
+                       $dimensionsStyle = ( $width ? "width: {$width}px;" : '' 
) .
+                               ( $height ? "height: {$height}px;" : '' );
+
+                       // HTML only clients
+                       $noscript = $doc->createElement( 'noscript' );
+
+                       // Loading status of image placeholder
+                       $spinner = $doc->createElement( 'span' );
+                       $spinner->setAttribute( 'class', MobileUI::iconClass(
+                               'spinner', 'element', 'loading spinner'
+                       ) );
+
+                       // To be loaded image placeholder
+                       $imgPlaceholder = $doc->createElement( 'span' );
+                       $imgPlaceholder->setAttribute( 'class', 
'lazy-image-placeholder' );
+                       $imgPlaceholder->setAttribute( 'style', 
$dimensionsStyle );
+                       $imgPlaceholder->appendChild( $spinner );
+
+                       // Set the placeholder where the original image was
+                       $parent->replaceChild( $imgPlaceholder, $img );
+                       // Add the original image to the HTML only markup
+                       $noscript->appendChild( $img );
+                       // Insert the HTML only markup before the placeholder
+                       $parent->insertBefore( $noscript, $imgPlaceholder );
+               }
+       }
+
+       /**
         * Replaces images with [annotations from alt]
         */
        private function doRemoveImages() {
diff --git a/includes/MobileFrontend.hooks.php 
b/includes/MobileFrontend.hooks.php
index 2bf8a1a..a16f499 100644
--- a/includes/MobileFrontend.hooks.php
+++ b/includes/MobileFrontend.hooks.php
@@ -433,7 +433,8 @@
                        'wgMFEditorOptions' => $config->get( 'MFEditorOptions' 
),
                        'wgMFLicense' => MobileFrontendSkinHooks::getLicense( 
'editor' ),
                        'wgMFUploadLicenseLink' => $wgMFUploadLicense['link'],
-                       'wgMFSchemaEditSampleRate' => $config->get( 
'MFSchemaEditSampleRate' )
+                       'wgMFSchemaEditSampleRate' => $config->get( 
'MFSchemaEditSampleRate' ),
+                       'wgMFLazyLoadImages' => $config->get( 
'MFLazyLoadImages' ),
                );
 
                if ( $context->shouldDisplayMobileView() ) {
diff --git a/resources/mobile.startup/Skin.js b/resources/mobile.startup/Skin.js
index c2a06d1..5117638 100644
--- a/resources/mobile.startup/Skin.js
+++ b/resources/mobile.startup/Skin.js
@@ -1,7 +1,9 @@
 ( function ( M, $ ) {
 
        var browser = M.require( 'mobile.browser/browser' ),
-               View = M.require( 'mobile.view/View' );
+               View = M.require( 'mobile.view/View' ),
+               util = M.require( 'mobile.startup/util' ),
+               context = M.require( 'mobile.context/context' );
 
        /**
         * Representation of the current skin being rendered.
@@ -12,7 +14,9 @@
         * @uses Page
         */
        function Skin( options ) {
-               var self = this;
+               var self = this,
+                       isBeta = context.isBetaGroupMember(),
+                       wgMFLazyLoadImages = mw.config.get( 
'wgMFLazyLoadImages' );
 
                this.page = options.page;
                this.name = options.name;
@@ -41,6 +45,17 @@
                M.on( 'resize', $.proxy( this, 'emit', '_resize' ) );
                this.on( '_resize', loadWideScreenModules );
                this.emit( '_resize' );
+
+               if (
+                       !mw.config.get( 'wgImagesDisabled' ) && (
+                               wgMFLazyLoadImages.base ||
+                               ( isBeta && wgMFLazyLoadImages.beta )
+                       )
+               ) {
+                       $( function () {
+                               self.loadImages();
+                       } );
+               }
        }
 
        OO.mfExtend( Skin, View, {
@@ -157,6 +172,78 @@
                },
 
                /**
+                * Load images on demand
+                */
+               loadImages: function () {
+                       var self = this,
+                               $imageLinks = this.$( '#content' ).find( 
'.image' );
+
+                       /**
+                        * Load remaining images in viewport
+                        */
+                       function _loadImages() {
+
+                               $imageLinks = $imageLinks.filter( function ( 
index, link ) {
+                                       var $imageLink = $( link );
+
+                                       if (
+                                               util.isElementInViewport( 
$imageLink ) &&
+                                               $imageLink.find( '.spinner' 
).is( ':visible' )
+                                       ) {
+                                               self.loadImage( $imageLink );
+                                               return false;
+                                       }
+
+                                       return true;
+                               } );
+
+                               if ( !$imageLinks.length ) {
+                                       M.off( 'scroll', _loadImages );
+                                       M.off( 'resize', _loadImages );
+                                       M.off( 'section-toggled', _loadImages );
+                               }
+
+                       }
+
+                       M.on( 'scroll', _loadImages );
+                       M.on( 'resize', _loadImages );
+                       M.on( 'section-toggled', _loadImages );
+
+                       _loadImages();
+               },
+
+               /**
+                * Load an image on demand
+                * @param {jQuery.Object} $imageLink
+                */
+               loadImage: function ( $imageLink ) {
+                       var $noscript = $imageLink.find( 'noscript' ),
+                               $placeholder = $imageLink.find( 
'.lazy-image-placeholder' ),
+                               // Grab the image markup from the HTML only 
fallback
+                               // Image will start downloading
+                               $image = $( $.parseHTML( $noscript.text() ) ),
+                               $downloadingImage = $( '<img/>' );
+
+                       // When the image has loaded
+                       $downloadingImage.on( 'load', function () {
+                               // Swap the HTML inside the placeholder (to 
keep the layout and
+                               // dimensions the same and not trigger layouts
+                               $placeholder.empty().append( $image );
+                               // Set the loaded class after insertion of the 
HTML to trigger the
+                               // animations.
+                               setTimeout( function () {
+                                       $placeholder.addClass( 'loaded' );
+                               }, 100 );
+                       } );
+
+                       // Trigger image download after binding the load handler
+                       $downloadingImage.attr( {
+                               src: $image.attr( 'src' ),
+                               srcset: $image.attr( 'srcset' )
+                       } );
+               },
+
+               /**
                 * Returns the appropriate license message including links/name 
to
                 * terms of use (if any) and license page
                 */
diff --git a/resources/mobile.toggle/toggle.js 
b/resources/mobile.toggle/toggle.js
index 603c5f3..32aecf7 100644
--- a/resources/mobile.toggle/toggle.js
+++ b/resources/mobile.toggle/toggle.js
@@ -153,6 +153,8 @@
                                'aria-expanded': !wasExpanded
                        } );
 
+               M.emit( 'section-toggled', wasExpanded, sectionId );
+
                if ( !browser.isWideScreen() ) {
                        storeSectionToggleState( $heading, page );
                }
diff --git a/resources/skins.minerva.base.styles/images.less 
b/resources/skins.minerva.base.styles/images.less
new file mode 100644
index 0000000..8c4a0bd
--- /dev/null
+++ b/resources/skins.minerva.base.styles/images.less
@@ -0,0 +1,42 @@
+@import "minerva.variables.less";
+
+@transitionDuration: 0.3s;
+
+.lazy-image-placeholder {
+       display: inline-block;
+       // The image placeholder for shouldn't show for no-js devices
+       .client-nojs & {
+               display: none;
+       }
+
+       background-color: @colorGray14;
+       border: 1px solid @colorGray11;
+       border-radius: 2px;
+
+       // Hide anything around the dimensions of the placeholder (avoids 
spinner
+       // overflowing on smaller images
+       overflow: hidden;
+
+       // Center spinner vertically and horizontally
+       display: flex;
+       justify-content: center;
+       align-items: center;
+
+       // When inserted, don't show the image because we want to animate it
+       img {
+               opacity: 0;
+       }
+
+       // When the image has loaded transition background color and image 
opacity
+       // for a fade-in effect
+       &.loaded {
+               transition: background-color @transitionDuration ease-in;
+               background-color: transparent;
+               border: none;
+
+               img {
+                       opacity: 1;
+                       transition: opacity @transitionDuration ease-in;
+               }
+       }
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/268185
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I8b6e68a47f6ee081776bee28d824d51a183d7607
Gerrit-PatchSet: 19
Gerrit-Project: mediawiki/extensions/MobileFrontend
Gerrit-Branch: master
Gerrit-Owner: Jhernandez <[email protected]>
Gerrit-Reviewer: Bmansurov <[email protected]>
Gerrit-Reviewer: Jdlrobson <[email protected]>
Gerrit-Reviewer: Jhernandez <[email protected]>
Gerrit-Reviewer: Krinkle <[email protected]>
Gerrit-Reviewer: Phuedx <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to