Loaders are great for UX, it gives the user a feedback that some data crunching is going on, so they know that it's no point in trying to abuse the clicks, they just have to wait and that is just fine. BUT having to wait around 4s in order to see the product image is not just fine. I know that Magento brings a lot of value and base customizations out of the box and that it comes at a cost, but it's a bit to expeensive for me, so let's look at how we can decrease that price. First of all, we are gonna deal with perceived load time and not actual loading time, the latter is a job for performance optimization, async loading assets, minifying assets, server configuration, make use of a CDN and so on.

We can find the template that takes care of the gallery in vendor/magento/module-catalog/view/frontend/templates/product/view/gallery.phtml so before you make any modifications to it, go ahead and import it into your own theme app/design/frontend/<Vendor>/<Theme-Name>Magento_Catalog/templates/product/view/gallery.phtml. The default code in the template is:

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

// @codingStandardsIgnoreFile

/**
 * Product media data template
 *
 * @var $block \Magento\Catalog\Block\Product\View\Gallery
 */
?>
<div class="gallery-placeholder _block-content-loading" data-gallery-role="gallery-placeholder">
    <div data-role="loader" class="loading-mask">
        <div class="loader">
            <img src="<?= /* @escapeNotVerified */ $block->getViewFileUrl('images/loader-1.gif') ?>"
                 alt="<?= /* @escapeNotVerified */ __('Loading...') ?>">
        </div>
    </div>
</div>
<!--Fix for jumping content. Loader must be the same size as gallery.-->
<script>
    var config = {
            "width": <?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_medium', 'width') ?>,
            "thumbheight": <?php /* @escapeNotVerified */ echo $block->getImageAttribute('product_page_image_small', 'height')
                        ?: $block->getImageAttribute('product_page_image_small', 'width'); ?>,
            "navtype": "<?= /* @escapeNotVerified */ $block->getVar("gallery/navtype") ?>",
            "height": <?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_medium', 'height') ?>
        },
        thumbBarHeight = 0,
        loader = document.querySelectorAll('[data-gallery-role="gallery-placeholder"] [data-role="loader"]')[0];

    if (config.navtype === 'horizontal') {
        thumbBarHeight = config.thumbheight;
    }

    loader.style.paddingBottom = ( config.height / config.width * 100) + "%";
</script>
<script type="text/x-magento-init">
    {
        "[data-gallery-role=gallery-placeholder]": {
            "mage/gallery/gallery": {
                "mixins":["magnifier/magnify"],
                "magnifierOpts": <?= /* @escapeNotVerified */ $block->getMagnifier() ?>,
                "data": <?= /* @escapeNotVerified */ $block->getGalleryImagesJson() ?>,
                "options": {
                    "nav": "<?= /* @escapeNotVerified */ $block->getVar("gallery/nav") ?>",
                    <?php if (($block->getVar("gallery/loop"))): ?>
                        "loop": <?= /* @escapeNotVerified */ $block->getVar("gallery/loop") ?>,
                    <?php endif; ?>
                    <?php if (($block->getVar("gallery/keyboard"))): ?>
                        "keyboard": <?= /* @escapeNotVerified */ $block->getVar("gallery/keyboard") ?>,
                    <?php endif; ?>
                    <?php if (($block->getVar("gallery/arrows"))): ?>
                        "arrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/arrows") ?>,
                    <?php endif; ?>
                    <?php if (($block->getVar("gallery/allowfullscreen"))): ?>
                        "allowfullscreen": <?= /* @escapeNotVerified */ $block->getVar("gallery/allowfullscreen") ?>,
                    <?php endif; ?>
                    <?php if (($block->getVar("gallery/caption"))): ?>
                        "showCaption": <?= /* @escapeNotVerified */ $block->getVar("gallery/caption") ?>,
                    <?php endif; ?>
                    "width": "<?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_medium', 'width') ?>",
                    "thumbwidth": "<?= /* @escapeNotVerified */ $block->getImageAttribute('product_page_image_small', 'width') ?>",
                    <?php if ($block->getImageAttribute('product_page_image_small', 'height') || $block->getImageAttribute('product_page_image_small', 'width')): ?>
                        "thumbheight": <?php /* @escapeNotVerified */ echo $block->getImageAttribute('product_page_image_small', 'height')
                        ?: $block->getImageAttribute('product_page_image_small', 'width'); ?>,
                    <?php endif; ?>
                    <?php if ($block->getImageAttribute('product_page_image_medium', 'height') || $block->getImageAttribute('product_page_image_medium', 'width')): ?>
                        "height": <?php /* @escapeNotVerified */ echo $block->getImageAttribute('product_page_image_medium', 'height')
                        ?: $block->getImageAttribute('product_page_image_medium', 'width'); ?>,
                    <?php endif; ?>
                    <?php if ($block->getVar("gallery/transition/duration")): ?>
                        "transitionduration": <?= /* @escapeNotVerified */ $block->getVar("gallery/transition/duration") ?>,
                    <?php endif; ?>
                    "transition": "<?= /* @escapeNotVerified */ $block->getVar("gallery/transition/effect") ?>",
                    <?php if (($block->getVar("gallery/navarrows"))): ?>
                        "navarrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/navarrows") ?>,
                    <?php endif; ?>
                    "navtype": "<?= /* @escapeNotVerified */ $block->getVar("gallery/navtype") ?>",
                    "navdir": "<?= /* @escapeNotVerified */ $block->getVar("gallery/navdir") ?>"
                },
                "fullscreen": {
                    "nav": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/nav") ?>",
                    <?php if ($block->getVar("gallery/fullscreen/loop")): ?>
                        "loop": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/loop") ?>,
                    <?php endif; ?>
                    "navdir": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/navdir") ?>",
                    <?php if ($block->getVar("gallery/transition/navarrows")): ?>
                        "navarrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/navarrows") ?>,
                    <?php endif; ?>
                    "navtype": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/navtype") ?>",
                    <?php if ($block->getVar("gallery/fullscreen/arrows")): ?>
                        "arrows": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/arrows") ?>,
                    <?php endif; ?>
                    <?php if ($block->getVar("gallery/fullscreen/caption")): ?>
                        "showCaption": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/caption") ?>,
                    <?php endif; ?>
                    <?php if ($block->getVar("gallery/fullscreen/transition/duration")): ?>
                        "transitionduration": <?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/transition/duration") ?>,
                    <?php endif; ?>
                    "transition": "<?= /* @escapeNotVerified */ $block->getVar("gallery/fullscreen/transition/effect") ?>"
                },
                "breakpoints": <?= /* @escapeNotVerified */ $block->getBreakpoints() ?>
            }
        }
    }
</script>

The part that we are intersted in, is the loader one:

<div class="gallery-placeholder _block-content-loading" data-gallery-role="gallery-placeholder">
    <div data-role="loader" class="loading-mask">
        <div class="loader">
            <img src="<?= /* @escapeNotVerified */ $block->getViewFileUrl('images/loader-1.gif') ?>"
                 alt="<?= /* @escapeNotVerified */ __('Loading...') ?>">
        </div>
    </div>
</div>

Let's get rid of the loader GIF and replace it with products main image:

<div class="gallery-placeholder _block-content-loading" data-gallery-role="gallery-placeholder">
    <div data-role="loader" class="loading-mask">
        <div class="loader">
            <?php
                $json=$block->getGalleryImagesJson();
                $array = json_decode($json,true);
            ?>
            <img src="<?= $array[0]['full'] ?>" />
        </div>
    </div>
</div>

In the code above, we made use of the getGalleryImageJson block method, this provides a JSON object with all gallery images of the current product, after that we simply decode the JSON and print out the value of the first full image inside the src attribute. The result of this code will be that the user will see the product's main image instead of a loader until the gallery is being loaded, once the gallery is loaded, it will replace the main image.

The price we have to pay for this method is that we'll see a flicker once the swap happens.

Worth it? I think yes, but let us know what you think on Twitter