×

Status message

Demo Page: demos/parallax-images potentially unstable

Demo: Parallax Images

In this demonstration/test I will be applying a parallax illusion programically via jQuery & Javascript using the image below:

The concept is simple: multiply the difference between the image's size, and it's container's by the amount it has passed by on the screen in order to provide a 'parallax' type effect.

This demo runs best in a browser such as Chrome with smooth scrolling enabled chrome://flags/#smooth-scrolling

First Run

My first attempt at the effect implemented the use of a single element, to retain ease of replication, and using the background to both display and position the image.

HTML:

<div class="parallax" data-parallax="150%"></div>

CSS:

.parallax {
  background: url(<?php print $test_img_src ?>) no-repeat top left;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  background-attachment: scroll;
  background-size: auto;
  border: 1px solid;
  border-color: rgba(0, 0, 0, 0.25) rgba(127, 127, 127, 0.1) rgba(255, 255, 255, 0.15);
  min-height: 50vh;
  max-height: 924px;
  border-radius: 10px;
}

jQuery / Javascript:

function scroll(){
  var scrollTop = jQuery(document).scrollTop(), windowHeight = jQuery(window).height();

  jQuery('.parallax').each(function() {
    var imgTopDist = jQuery(this).offset().top, wrapHeight = jQuery(this).height();
    var offset = Math.min(Math.max(((imgTopDist - scrollTop) + wrapHeight), 0), windowHeight + wrapHeight) / (windowHeight + wrapHeight);
    var img = jQuery(this),
      style = img.currentStyle || window.getComputedStyle(this, false),
      bgUrl = style.backgroundImage.slice(4, -1).replace(/"/g, "");
    var bg = new Image();
    
    bg.src = bgUrl;
    var bgWidth = bg.width, bgHeight = bg.height;
    var parallaxPerc = jQuery(this).attr("data-parallax");
    
    if(parallaxPerc) {
      oldBgHeight = bgHeight;
      bgHeight = wrapHeight*(1+parseFloat(parseInt(parallaxPerc) / 100));
      jQuery(this).css({ "background-size": bgWidth*(bgHeight / oldBgHeight)+'px ' + bgHeight+'px' });
    }
    
    var coords = '50% ' + (-1 * Math.abs(bgHeight-wrapHeight)*offset ) + 'px';
    jQuery(this).css({ "background-position": coords });
  });
}
jQuery(document).scroll(scroll);

Once the first rendition of the test was successful, there were many issues that became quite clear.

Background Image Repainting Issues

In my attempt to make things simple, by designing the effect around one element, and using background images, I created a repainting nightmare. For every '.parallax' image on the page, an entire repaint was needed for every background position change, regardless if the position was actually different than before, whether it was on the screen or not. This cost big time in rendering performance and caused 'choppy' scroll performance issues, especially on mobile devices.

Costly Recalculations

In the jQuery / Javascript that was used a lot of the computations could have been stored in arrays. Given, this first test was only to get the effect working in the first place, but it was a mess optimizations-wise. Upon each scroll event it recalculated and updated each element regardless if it was visible on-screen, a huge no-no. So I decided to clean it up a bit and test it again.

Second Run: Better Programming Logic

Aiming to alleviate some of the unnecessary programming overhead, I decided to tackle some obvious logic flaws:

  • Prevent updates to off-screen parallax elements.
  • Do not update background size if same as before.

Bam! 16.3% faster frame times! As recorded below, with the optimized programming:

First Run

~37.75ms

(33.3-42.2ms) (~26.5fps)
Frame Render Delays
1.17-2.36ms Parallax Calculations
9.84-15.12ms Rasterized Re-Paints
74.39-114.29ms CPU time
430.23-451.32ms Rasterizer Image Decoding
Second Run

~31.65ms

(23.3-40.0ms) (~31.5fps)
Frame Render Delays
0.82-1.97ms Parallax Calculations
9.21-14.26ms Rasterized Re-Paints
31.66-54.24ms CPU time
430.23-451.32ms Rasterizer Image Decoding

Optimized jQuery / Javascript:

function scroll(){
  var scrollTop = jQuery(document).scrollTop(), windowHeight = jQuery(window).height();
  
  jQuery('.parallax').each(function() {
    var imgTopDist = jQuery(this).offset().top, wrapHeight = jQuery(this).height(), scrollRange = windowHeight+wrapHeight;
    var imgRange = Math.min(Math.max(((imgTopDist - scrollTop) + wrapHeight), 0), scrollRange);
    
    if(imgRange >= 0 && imgRange <= scrollRange) {
      var offset = imgRange / scrollRange;
      
      var style = jQuery(this).currentStyle || window.getComputedStyle(this, false);
      var bg = new Image();
        bg.src = style.backgroundImage.slice(4, -1).replace(/"/g, "");
      var bgWidth = bg.width, bgHeight = bg.height;
      
      var parallaxPerc = jQuery(this).attr("data-parallax");
      if(parallaxPerc) {
        var newHeight = wrapHeight*(1+parseFloat(parseInt(parallaxPerc) / 100));
        if(bgHeight != newHeight) {
          var oldBgHeight = bgHeight;
          bgHeight = newHeight;
          jQuery(this).css({ "background-size": bgWidth*(bgHeight / oldBgHeight)+'px ' + bgHeight+'px' });
        }
      }
      
      jQuery(this).css({ "background-position": '50% ' + (-1 * Math.abs(bgHeight-wrapHeight)*offset ) + 'px' });
    }
  });
}
jQuery(document).scroll(scroll);

Sadly, this improvement to the code didn't help much with frame rates being as slow as 26, 7, and even 5 frames per second. There were several reasons for this issue. The image was being displayed as a background, and as such required huge repaints upon every update. How could this be fixed? A number of ways: apply a translate3d hack to the '.parallax' element in order to force all repaints of them to the faster GPU thread, use a full-sized 'img' element inside of a wrapper, use a canvas element. Since I do not currently like canvas elements, I will not be attempting to use them.

translate3d Hack

Secondly, I tried using the translate3d hack in order to force all repaints of them to the faster GPU thread, as mentioned above, by adding transform: translate3d(0px,0px,0px); to my '.parallax' CSS class. The following shows my results:

Second Run

~31.65ms

(23.3-40.0ms) (~31.5fps)
Frame Render Delays
0.82-1.97ms Parallax Calculations
9.21-14.26ms Rasterized Re-Paints
31.66-54.24ms CPU time
430.23-451.32ms Rasterizer Image Decoding
Third Run

~16.7ms

(14.2-43.1ms) (~59.8fps)
Frame Render Delays
0.88-1.52ms Parallax Calculations
3.34-7.81ms Rasterized Re-Paints
29.55-45.91ms CPU time
430.23-451.32ms Rasterizer Image Decoding

Wow! Compared to the first run at ~37.75ms, the parallax effect is now 126% (2.26x) faster, with frame rates as fast as 59-61fps! You would think this would be the end of my journey, but no. There were issues pertaining to Rasterizer image decodings taking 430.23-451.32ms long, delaying the page scrolls by roughly half a second before each new parallax element came into view for the first time, as well as the occational drop to a 33.3-43.1ms. Time for a revamp!

Using the Wrapped <img> Approach!

Many changes had been made in preporation for the fourth run:

  • The HTML structure was changed from a single 'div' element to a 'div' wrapped image in order to avoid repaints.
  • The CSS was completly cleaned and rebuilt with mobile-rendering friendly attributes in mind (using 'transform3d()' instead of 'top').
  • The jQuery / Javascript was also completly optimized in a push to minimize computations in every circumstance.
  • The demonstration image was reduced in size 4.11MB to 1.03MB (2137 × 1425 pixels)!

HTML:

<div class="parallax"><img src="../my-parallax-img.png"></div>

CSS:

.parallax {
  border: 1px solid;
  border-color: rgba(0, 0, 0, 0.25) rgba(127, 127, 127, 0.1) rgba(255, 255, 255, 0.15);
  border-radius: 10px;
  height: 50vh; /* any fixed size works */
  overflow: hidden;
  position: relative;
  z-index: 1; /* forces wrapper border-radius to hide absolute img child */
}
.parallax img {
  position: absolute;
  left: 50%;
  top: 0px;
  min-width: initial;
  min-height: initial;
  max-width: initial;
  max-height: initial;
  width: auto;
  height: auto; /* overwritten on init */
  transform: translate3d(-50%, 0px, 0); /* overwritten on scroll, hardware accelerated */
}

Smarter jQuery / Javascript:

function calcParallax(recalc) {
  window.requestAnimationFrame(function() {
    var scrollTop = jQuery(document).scrollTop(), windowHeight = jQuery(window).height();
    
    jQuery('.parallax').each(function() {
      var wrapTopDist = jQuery(this).offset().top, wrapHeight = jQuery(this).height(), wrapRange = windowHeight+wrapHeight;
      var wrapPos = Math.max((wrapTopDist-scrollTop)+wrapHeight, 0);
      
      if(recalc || (wrapPos <= wrapRange && wrapPos >= 0)) {
        var wrap = jQuery(this), img = wrap.find('img'), amt = Math.min(wrapPos, wrapRange) / wrapRange;
        
        var imgHeight = img.height();
        if(recalc) {
          var newHeight = Math.round(wrapHeight*(1+(wrap.data("scale") ? parseFloat(wrap.data("scale")) : 1)));
          if(imgHeight != newHeight) {
            imgHeight = newHeight;
            img.css('height', imgHeight+'px');
          }
        }
        
        img.css('transform', 'translate3d(-50%, ' + -(Math.abs(imgHeight-wrapHeight)*amt).toFixed(Math.max(window.devicePixelRatio-1, 0))+'px, 0)');
      }
    });
  });
}

function checks() {
  calcParallax(true);
}
function scrollchecks() {
  calcParallax(false);
}

The results were almost as expected, with some pleasant suprises! First of all, moving from a background-image oriented design to a wrapper based one removed alot of re-paint overhead as the browser was no longer needing to repaint an entire image on each scroll frame. Unexpectedly, image decode times for the rasterizer disapeared entirely! Meaning that roughly half a second's worth of lag was no longer a threat!

Smarter Programming

The best part

Third Run

~16.7ms

(14.2-43.1ms) (~59.8fps)
Frame Render Delays
0.88-1.52ms Parallax Calculations
3.34-7.81ms Rasterized Re-Paints
29.55-45.91ms CPU time
430.23-451.32ms Rasterizer Image Decoding
Fourth Run

~16.7ms

(10.6-23.1ms) (~59.8fps)
Frame Render Delays
0.76-0.92ms Parallax Calculations
0.5-5.35ms Rasterized Re-Paints
11.28-18.5ms CPU time
0ms Rasterizer Image Decoding

Overview

First Run

~37.75ms

(33.3-42.2ms) (~26.5fps)
Frame Render Delays
1.17-2.36ms Parallax Calculations
9.84-15.12ms Rasterized Re-Paints
74.39-114.29ms CPU time
430.23-451.32ms Rasterizer Image Decoding
Second Run

~31.65ms

(23.3-40.0ms) (~31.5fps)
Frame Render Delays
0.82-1.97ms Parallax Calculations
9.21-14.26ms Rasterized Re-Paints
31.66-54.24ms CPU time
430.23-451.32ms Rasterizer Image Decoding
Third Run

~16.7ms

(14.2-43.1ms) (~59.8fps)
Frame Render Delays
0.88-1.52ms Parallax Calculations
3.34-7.81ms Rasterized Re-Paints
29.55-45.91ms CPU time
430.23-451.32ms Rasterizer Image Decoding
Fourth Run

~16.7ms

(10.6-23.1ms) (~59.8fps)
Frame Render Delays
0.76-0.92ms Parallax Calculations
0.5-5.35ms Rasterized Re-Paints
11.28-18.5ms CPU time
0ms Rasterizer Image Decoding

Test Region:

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse ut sem lacus. Praesent porta mauris vel dignissim molestie. Nullam ac dignissim est. In augue nibh, feugiat eget placerat nec, semper et eros. Sed ut nisi eros. Nulla ac ex dui. Nam dignissim porttitor orci, id eleifend odio posuere nec. Duis ac hendrerit sem, non dapibus neque.

Quisque nec scelerisque ligula. Morbi faucibus dignissim pulvinar. Duis a tincidunt magna. Curabitur fringilla orci dui. Sed et tincidunt diam. Cras tempor maximus risus, vitae lobortis quam viverra et. Sed volutpat blandit tortor, id maximus quam vulputate at. Vestibulum lorem orci, lobortis eu augue vitae, euismod ultricies orci. In risus felis, porttitor ut pulvinar non, mattis at arcu.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse ut sem lacus. Praesent porta mauris vel dignissim molestie. Nullam ac dignissim est. In augue nibh, feugiat eget placerat nec, semper et eros. Sed ut nisi eros. Nulla ac ex dui. Nam dignissim porttitor orci, id eleifend odio posuere nec. Duis ac hendrerit sem, non dapibus neque.

Quisque nec scelerisque ligula. Morbi faucibus dignissim pulvinar. Duis a tincidunt magna. Curabitur fringilla orci dui. Sed et tincidunt diam. Cras tempor maximus risus, vitae lobortis quam viverra et. Sed volutpat blandit tortor, id maximus quam vulputate at. Vestibulum lorem orci, lobortis eu augue vitae, euismod ultricies orci. In risus felis, porttitor ut pulvinar non, mattis at arcu.

Image at 200% it's container's size. Default for parallax.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse ut sem lacus. Praesent porta mauris vel dignissim molestie. Nullam ac dignissim est. In augue nibh, feugiat eget placerat nec, semper et eros. Sed ut nisi eros. Nulla ac ex dui. Nam dignissim porttitor orci, id eleifend odio posuere nec. Duis ac hendrerit sem, non dapibus neque.

Quisque nec scelerisque ligula. Morbi faucibus dignissim pulvinar. Duis a tincidunt magna. Curabitur fringilla orci dui. Sed et tincidunt diam. Cras tempor maximus risus, vitae lobortis quam viverra et. Sed volutpat blandit tortor, id maximus quam vulputate at. Vestibulum lorem orci, lobortis eu augue vitae, euismod ultricies orci. In risus felis, porttitor ut pulvinar non, mattis at arcu.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse ut sem lacus. Praesent porta mauris vel dignissim molestie. Nullam ac dignissim est. In augue nibh, feugiat eget placerat nec, semper et eros. Sed ut nisi eros. Nulla ac ex dui. Nam dignissim porttitor orci, id eleifend odio posuere nec. Duis ac hendrerit sem, non dapibus neque.

Quisque nec scelerisque ligula. Morbi faucibus dignissim pulvinar. Duis a tincidunt magna. Curabitur fringilla orci dui. Sed et tincidunt diam. Cras tempor maximus risus, vitae lobortis quam viverra et. Sed volutpat blandit tortor, id maximus quam vulputate at. Vestibulum lorem orci, lobortis eu augue vitae, euismod ultricies orci. In risus felis, porttitor ut pulvinar non, mattis at arcu.

Image at 250% it's container's size.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse ut sem lacus. Praesent porta mauris vel dignissim molestie. Nullam ac dignissim est. In augue nibh, feugiat eget placerat nec, semper et eros. Sed ut nisi eros. Nulla ac ex dui. Nam dignissim porttitor orci, id eleifend odio posuere nec. Duis ac hendrerit sem, non dapibus neque.

Quisque nec scelerisque ligula. Morbi faucibus dignissim pulvinar. Duis a tincidunt magna. Curabitur fringilla orci dui. Sed et tincidunt diam. Cras tempor maximus risus, vitae lobortis quam viverra et. Sed volutpat blandit tortor, id maximus quam vulputate at. Vestibulum lorem orci, lobortis eu augue vitae, euismod ultricies orci. In risus felis, porttitor ut pulvinar non, mattis at arcu.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse ut sem lacus. Praesent porta mauris vel dignissim molestie. Nullam ac dignissim est. In augue nibh, feugiat eget placerat nec, semper et eros. Sed ut nisi eros. Nulla ac ex dui. Nam dignissim porttitor orci, id eleifend odio posuere nec. Duis ac hendrerit sem, non dapibus neque.

Quisque nec scelerisque ligula. Morbi faucibus dignissim pulvinar. Duis a tincidunt magna. Curabitur fringilla orci dui. Sed et tincidunt diam. Cras tempor maximus risus, vitae lobortis quam viverra et. Sed volutpat blandit tortor, id maximus quam vulputate at. Vestibulum lorem orci, lobortis eu augue vitae, euismod ultricies orci. In risus felis, porttitor ut pulvinar non, mattis at arcu.

Image at 300% it's container's size.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse ut sem lacus. Praesent porta mauris vel dignissim molestie. Nullam ac dignissim est. In augue nibh, feugiat eget placerat nec, semper et eros. Sed ut nisi eros. Nulla ac ex dui. Nam dignissim porttitor orci, id eleifend odio posuere nec. Duis ac hendrerit sem, non dapibus neque.

Quisque nec scelerisque ligula. Morbi faucibus dignissim pulvinar. Duis a tincidunt magna. Curabitur fringilla orci dui. Sed et tincidunt diam. Cras tempor maximus risus, vitae lobortis quam viverra et. Sed volutpat blandit tortor, id maximus quam vulputate at. Vestibulum lorem orci, lobortis eu augue vitae, euismod ultricies orci. In risus felis, porttitor ut pulvinar non, mattis at arcu.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse ut sem lacus. Praesent porta mauris vel dignissim molestie. Nullam ac dignissim est. In augue nibh, feugiat eget placerat nec, semper et eros. Sed ut nisi eros. Nulla ac ex dui. Nam dignissim porttitor orci, id eleifend odio posuere nec. Duis ac hendrerit sem, non dapibus neque.

Quisque nec scelerisque ligula. Morbi faucibus dignissim pulvinar. Duis a tincidunt magna. Curabitur fringilla orci dui. Sed et tincidunt diam. Cras tempor maximus risus, vitae lobortis quam viverra et. Sed volutpat blandit tortor, id maximus quam vulputate at. Vestibulum lorem orci, lobortis eu augue vitae, euismod ultricies orci. In risus felis, porttitor ut pulvinar non, mattis at arcu.