49

I am trying to optimize the size of my site when it is being outputted to the client. I am down to 1.9MB and 29KB when caching. The issue is that the first load contains an image which is very unoptimized for mobile devices; it has a 1080p resolution.

So I am looking for a method that allows me to first load a low-res version (min.bg.jpg) and once the site has loaded, use a high-res version - or even one with a resolution close to the device being used (NNNxNNN.bg.jpg or just bg.jpg).

The background is set using CSS just like everyone would expect. Its applied to the body and the entire statement looks like this:

body {
    background: url("/cdn/theme/images/bg.jpg");
    color: white;
    height: 100%;
    width: 100%;
    background-repeat: no-repeat;
    background-position: 50% 50%;
    background-attachment: fixed;
}

Now, I want to change that to use min.bg.jpg instead for the first load, and then something like this:

jQuery(function(){
    jQuery("body").[...]
});

Which way do I go on asynchronously downloading the new background, and then inserting it as the new CSS background image?

To show some differences, here is an example of the main and mini version I am using for testing:

Ingwie@Ingwies-Macbook-Pro.local ~/Work/BIRD3/cdn/theme/images $ file *.jpg
bg.jpg:     JPEG image data, EXIF standard
min.bg.jpg: JPEG image data, JFIF standard 1.01
Ingwie@Ingwies-Macbook-Pro.local ~/Work/BIRD3/cdn/theme/images $ du -h *.jpg
1,0M    bg.jpg
620K    min.bg.jpg
Ingwie Phoenix
  • 2,703
  • 2
  • 24
  • 33

7 Answers7

47

A bit late, but you can use this extremely simple solution: You can put the two images in the css background:

  background-image: url("high-res.jpg"),url("low-res.jpg");

The browser will display the low-res image fist, then display the high-res over the low-res when it has been loaded.

user7956100
  • 471
  • 1
  • 4
  • 3
  • 10
    This works really well for me. Remember that other `background-*` css tags must also be duplicated, e.g., `background-repeat: no-repeat, no-repeat;` see the [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Backgrounds_and_Borders/Using_multiple_backgrounds) for more info. – Quinn Comendant Jun 01 '18 at 04:34
  • 1
    Awesome! Note that urls referenced only in `background-image` will load after `src` refs, even if said `src`s belong to elements later in the document. One way to force the low-res background-image to load earlier in the queue is to also make it the `src` of a hidden `` tag at the beginning of the document to quick off the request. – zhark Jun 05 '19 at 03:48
  • 3
    That's a nice solution, but is it specified anywhere? I can't find it on the [csswg specification site](https://drafts.csswg.org/css-backgrounds-3/#background-image) for example, nor on the [Mozilla](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Backgrounds_and_Borders/Using_multiple_backgrounds) site. It's a bad idea to rely on "features" that aren't specified because they may be changed from under you or stop working in other contexts without warning (if they even really work to begin with). – Dennis Hackethal May 08 '21 at 00:59
34

Let's try a basic one :

<img border="0" 
     style="background:url(https://i.stack.imgur.com/zWfJ5.jpg) no-repeat; 
            width:1920px;
            height:1200px"
     src="https://i.stack.imgur.com/XOYra.jpg" width="1920" height="1200" />

zWfJ5.jpg is the low-resolution version, and XOYra.jpg is the high-resolution version.

If there is a way to arrange the loading so the background-image displays first, this could be the simplest i can think of.

where low resolution 44k: low resolution 44k:

and high resolution is 1.16M high resolution is 1.16M

result :

jsFiddled here ( this needs a bigger image for loading comparison. )

Nathan Wailes
  • 9,872
  • 7
  • 57
  • 95
Milche Patern
  • 19,632
  • 6
  • 35
  • 52
  • 2
    CSS `background:url()` is resolved _after_ `img[src=]`? :o that is seriously powerful. – Ingwie Phoenix Jan 06 '16 at 00:54
  • Or before, but i can see what you mean here. Though for other readers, you may want to elaborate a little bit here. :) – Ingwie Phoenix Jan 06 '16 at 00:55
  • For me on Chrome 71 (Ubuntu), the CSS background image is not always resolved before the `img src` attribute, sometimes waiting to load the entire higher resolution image before starting to retrieve the lower resolution image. I've written an answer that avoids this problem. – FThompson Jan 10 '19 at 06:39
22

Here's the method I use...

CSS:

#div_whatever {
   position: whatever;
   background-repeat: no-repeat;
   background-position: whatever whatever; 
   background-image: url(dir/image.jpg);
   /* image.jpg is a low-resolution at 30% quality. */
}

#img_highQuality {
    display: none;
}

HTML:

<img id="img_highQuality" src="dir/image.png">
<!-- img.png is a full-resolution image. -->

<div id="div_whatever"></div>

JQUERY:

$("#img_highQuality").off().on("load", function() {
    $("#div_whatever").css({
        "background-image" : "url(dir/image.png)"
    });
});
// Side note: I usually define CSS arrays because
// I inevitably want to go back and add another 
// property at some point.

What happens:

  1. A low-res version of the background quickly loads.
  2. Meanwhile, the higher resolution version is loading as a hidden image.
  3. When the high-res image is loaded, jQuery swaps the div's low-res image with the high-res version.

PURE JS VERSION

This example would be efficient for changing one to many elements.

CSS:

.hidden {
   display: none;
}

#div_whatever {
   position: whatever;
   background-repeat: no-repeat;
   background-position: whatever whatever; 
   background-image: url(dir/image.jpg);
   /* image.jpg is a low-resolution at 30% quality. */
}

HTML:

<div id="div_whatever"></div>
<img id="img_whatever" class="hidden" src="dir/image.png" onload="upgradeImage(this);">

JAVASCRIPT:

function upgradeImage(object) {
    var id = object.id;
    var target = "div_" + id.substring(4);

    document.getElementById(target).style.backgroundImage = "url(" + object.src + ")";
}

UPDATE / ENHANCEMENT (1/31/2017)

This enhancement is inspired by gdbj's excellent point that my solution results in the image path being specified in three locations. Although I didn't use gdbj's addClass() technique, the following jQuery code is modified to extract the image path (rather than it being hardwired into the jQuery code). More importantly, this version allows for multiple low-res to high-res image substitutions.

CSS

.img_highres {
  display: none;
}

#div_whatever1 {
  width: 100px;
  height: 100px;
  background-repeat: no-repeat;
  background-position: center center;
  background-image: url(PATH_TO_LOW_RES_PHOTO_1);
}

#div_whatever2 {
  width: 200px;
  height: 200px;
  background-repeat: no-repeat;
  background-position: center center;
  background-image: url(PATH_TO_LOW_RES_PHOTO_2);
}

HTML

<div id="div_whatever1"></div>
<img id="img_whatever1" class="img_highres" src="PATH_TO_HIGH_RES_PHOTO_1">

<div id="div_whatever2"></div>
<img id="img_whatever2" class="img_highres" src="PATH_TO_HIGH_RES_PHOTO_2">

JQUERY

  $(function() {
      $(".img_highres").off().on("load", function() {
         var id = $(this).attr("id");
         var highres = $(this).attr("src").toString();
         var target = "#div_" + id.substring(4);
         $(target).css("background-image", "url(" + highres + ")");
      });
   });

What's happens:

  1. Low res images are loaded for each of the divs based on their CSS background-image settings. (Note that the CSS also sets the div to the intended dimensions.)
  2. Meanwhile, the higher resolution photos are being loaded as hidden images (all sharing a class name of img_highres).
  3. A jQuery function is triggered each time an img_highres photo completes loading.
  4. The jQuery function reads the image src path, and changes the background image of the corresponding div. In the example above, the naming convention is "div_[name]" for the visible divs and "img_[same name]" for the high res images loaded in the background.
Community
  • 1
  • 1
Alan M.
  • 1,309
  • 2
  • 19
  • 29
  • Oh! that's cool! Do you know what the equivalent to the `off` method, and the event for `load` is in vanilla JS? My site's JS is small so I don't use jQuery. – Ingwie Phoenix Jul 21 '15 at 12:40
  • 1
    @Ingwie: I updated my answer to show a pure JS/CSS version without jQuery. – Alan M. Jul 21 '15 at 19:02
  • 1
    Nice answer, but why not keep all your CSS in the same place? Better to define a css entry `#div_whatever.highres` and put your image url there, and then simply add the highres class via jquery for maintainability, and allows you to use breakpoints! – gdbj Jan 29 '17 at 18:50
  • @gdbj: Thank you for your comment. You're right; it would be great to have both images defined in CSS, but unless I'm mistaken (which is entirely possible), I don't believe jQuery's load event fires on background images, so (assuming that's correct) the only way to detect whether the high res image is loaded is to define it in the HTML or with JavaScript. – Alan M. Jan 30 '17 at 00:10
  • 1
    oh, you misunderstand me. I agree re: having the image path in HTML and set it to display none. However, you are writing css in your jquery file, effectively having image urls in THREE places, instead of just two. I did a slight edit of your code above, and just used `$("#div_whatever").addClass('highres');` to achieve the same result. A minor point, to be sure. – gdbj Jan 31 '17 at 02:11
  • @gdbj: I think you're making an excellent point. I posted an addendum to my answer. While it doesn't use addClass(), it no longer hard-wires the image path into the jQuery code. While at it, I also added the ability to accommodate multiple low-res/high-res image sets. – Alan M. Jan 31 '17 at 21:48
  • Confused. You added a comment to Milche's solution below saying that it was a better implementation than this solution, yet you posted an update as recently as 2017 to this solution. Does this mean that Milche's solution did not behave as expected? – Mike Purcell Jul 04 '17 at 18:21
  • @Mike (part 1 of 2): I still like Milche's solution, but there's no harm having an alternative. More importantly, gdbj made a valid "best-practice" point, and I wanted my example to reflect it. Since there have been many times, on Stack Exchange, that I have used answers (and questions, for that matter) to solve an issue unrelated to the original question or intent, and/or have picked up useful pointers and techniques that I wasn't anticipating, I wanted to make sure my answer was as helpful as possible for situations both within and outside of the original scope of the question. – Alan M. Jul 04 '17 at 22:15
  • @Mike (part 2 of 2): My answer for this situation would come in handy for someone who might want to execute other programmatic actions after an image is loaded — e.g,. if you were also waiting to display some additional controls. If that's the case, you'd be writing Javascript for that anyhow, and might want to have the entire sequence contained as shown in my examples instead of having a portion of the structure in the img. – Alan M. Jul 04 '17 at 22:19
  • This answer is good and works well, but you could avoid creating the extra `img` element for the high res image by instead using the [`Image`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/Image) class which creates one under the hood entirely in JavaScript. See my answer for explanation. – FThompson Jan 10 '19 at 06:43
2

I would normally optimise the image using Grunt or an online tool such as Tiny PNG to reduce the file size.

Then you could choose to defer the loading of the images, I found the following article helpful when it came to deferring images - https://www.feedthebot.com/pagespeed/defer-images.html

The article discusses using a base64 image for the initial loading and then deferring the loading of the high-quality image. The image mark up mentioned in the article is as follows...

<img src="data:image/png;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=" data-src="your-image-here">

The JavaScript mentioned in the article is as follows...

window.addEventListener("load", () => {
  const images = document.querySelectorAll("img");
  for (let img of images)
    if (img.hasAttribute("data-src"))
      img.src = imgDefer[i].getAttribute("data-src");
});

I hope this helps.

undefined
  • 1,019
  • 12
  • 24
steffcarrington
  • 404
  • 3
  • 13
1

On Ubuntu / Chrome 71, Milche's answer does not work consistently for me and the higher resolution image (via img src) often loads and resolves before the lower resolution image (via css background) even begins downloading.

My solution is to start with the lower res image as the src and use the Image class to create an unattached <img> instance with the high res image. Once it loads, then update the existing <img> source with the high res image source.

HTML:

<img id='my-image' src='low-res.png' alt='Title' width='1920px' height='1200px'>

JavaScript:

window.addEventListener('load', function() {
    loadHighResImage(document.getElementById('my-image'), 'high-res.png')
})

function loadHighResImage(elem, highResUrl) {
    let image = new Image()
    image.addEventListener('load', () => elem.src = highResUrl)
    image.src = highResUrl
}

Fiddle: https://jsfiddle.net/25aqmd67/

This approach works for lower res images that are simply scaled down as well.

FThompson
  • 28,352
  • 13
  • 60
  • 93
  • Why does this one not have the race condition? – Dennis Hackethal May 08 '21 at 00:08
  • @weltschmerz The `Image` object loads the high res image in the background. After the high res image loads (the `load` event), we set the `img` source to the now-loaded high res image. Both images are loaded asynchronously, and the final state is that the `img` source is the high res image. It's possible that the high res image loads before the low res image (due to the async requests), but the low res image won't replace the high res image if that occurs. – FThompson May 09 '21 at 15:33
0

All answers above mostly work with a little adjustment, but here is the way I think short and simple to kick off.

Note:

  • Uncomment the code load the high-resolution image for usage, a sleep function is just for simulating a slow network.
  • Actually, this method does not load 2 resources (low and high) simultaneous, but it's acceptable because low resource won't take much time to load.
  • Just copy whole code and run for a quick check.

<!DOCTYPE html>
<html>
  <head>
    <script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>

    <style type="text/css">
      
    </style>
  </head>

  <body>
    <!-- Load low res image first -->
    <img style="width: 400px; height: auto;" alt="" src="https://s3-ap-southeast-1.amazonaws.com/wheredat/banner-low-quality/banner_20180725_123048.jpg" onload="upgrade(this)">
  </body>
  
  <script type="text/javascript">

 function upgrade(image){

                // After load low res image, remove onload listener.

  // Remove onload listener.
  $(image).prop("onload", null);

                // Load high resolution image.
                // $(image).attr('src', 'https://s3-ap-southeast-1.amazonaws.com/wheredat/banner/banner_20180725_123048.jpeg');

  // Simulate slow network, after 1.5s, the high res image loads.
  sleep(1500).then(() => {
      // Do something after the sleep!

   // Load a high resolution image.
   $(image).attr('src', 'https://s3-ap-southeast-1.amazonaws.com/wheredat/banner/banner_20180725_123048.jpeg');
  }); 
 }

 // Sleep time expects milliseconds
 function sleep (time) {
   return new Promise((resolve) => setTimeout(resolve, time));
 }

  </script>
  
</html>
Nguyen Tan Dat
  • 3,780
  • 1
  • 23
  • 24
0

After days of working on it, I finally found this great way and it works with css background-image

html

<div class="background"></div>

css

.div {
   background-image: url(/imgs/low-res.jpg);
   transition: background-image 0.3s ease-in-out;
}

JS

// This code sets the background-image style of elements with the background class to the value of their data-src attribute using fetch and requestAnimationFrame


// Add an event listener to the window object that runs a function when the page is fully loaded

window.addEventListener("load", () => {

// Select all the elements that have the background class and store them in a variable called backgrounds
const backgrounds = document.querySelectorAll(".background");

// Loop through each element
// Check if the element has an attribute called data-src
for (let bg of backgrounds)
if (bg.hasAttribute("data-src")) {

// Get the value of the data-src attribute and store it in a variable called url
let url = bg.getAttribute("data-src");

// Use the fetch API to get the image data from the url
fetch(url)

.then((response) => response.blob()) // Convert the response to a blob object
.then((blob) => {

// Create a new URL object from the blob object
let objectURL = URL.createObjectURL(blob);

// Use the requestAnimationFrame method to synchronize the style change with the browser's repaint cycle
requestAnimationFrame(() => {

// Set the background-image style of the element to the object URL
bg.style.backgroundImage = "url(" + objectURL + ")";

   });
  });
 }
});
GawiSh
  • 31
  • 8