Skip to content
This repository has been archived by the owner on Jun 17, 2022. It is now read-only.

kurtextrem/parallax

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Perfecting Parallax Scrolling

This is part of a talk I gave at a Icelandic javascript user group meetup.

Slides from the presentation: http://dev.form5.is/parallax/slides.pdf

The demo code can be found in this repository and following is an article I wrote on the subject which has links to the demos from the talk.


Parallax scrolling has become quite popular in contemporary web design. This is understandable as it helps add a sense of depth and fluidity but most solutions are far from being perfect and are far too heavy on the cpu and noticeably choppy while scrolling.

TL;DR version

This is the search for better parallax scrolling. The best approach uses translate3d and a single ticking requestAnimationFrame method that will make your parallax scroll much lighter and smoother.

How do we measure performance?

To measure the performance of our methods we'll be using the performance profiling capabilities of the timeline tab in Google Chrome's developer inspector:

Timeline tab

Doing framerate optimizations is pretty much a limbo dance competition, touch the bars at your own risk! The green bars signal that rendering is being done by the CPU. Bars rising above 60fps are a clear indication of choppiness and those that touch 30fps or an even lower number are arrows aimed directly at a baby seal's heart. You don't want that on you conscience, do you?

The demonstration

Following are three examples of different parallax techniques, the first two being common solutions and then we present the third method — Asparagus.

The (perhaps much too familiar) hero image is a very common design pattern these days, being a default with popular front-end frameworks like Twitter Bootstrap and Zurb's Foundation. Love it or hate it, it serves as a great example for showing the difference in performance between the most common parallax techniques and Asparagus.

Our demonstration design

Technique 1: The Background Position Method

The first method is the background-position method where the background image placed on the #hero element. This is probably the most straightforward way of implementing parallax scrolling and it has been demonstrated in various tutorials around the web.

The Markup

<div id="hero">
  <div class="hero-content">
    <h1>background-position</h1>
    <p>This parallax method updates background-position and is the slowest of all. Rendering takes place on the CPU.</p>
  </div>
</div>
#hero {
  height: 750px;
  background: url('bg.jpg') 50% 0 no-repeat;
  background-size: cover;
}

This markup is nothing out of the ordinary and the actual parallax functionality takes place in the JavaScript where updatePosition() is fired on every scroll event which changes the hero area's background-position attribute. This creates the parallax effect while the user is scrolling.

var updatePosition = function() {
  var hero = document.getElementById('hero');
  var scrollPos = window.pageYOffset / 2;
  hero.style['background-position'] = '50% ' + scrollPos + 'px';
};

window.addEventListener('scroll', updatePosition, false);

Results: Very, Very Bad

View the demo

The use of the background-position method, where rendering is handled by the CPU, results in terrible performance as can be clearly observed in the timeline measure we mentioned above.

background-position performance

Technique 2: The Relative Top Positioning and translateY Methods

Here we'll actually be showing two different methods (but both share the same markup) where the background is moved to a separate element and the position of the whole element is changed when scrolling (rather than updating the background position).

  <div id="hero">
    <div id="hero-bg"></div>

    <div class="hero-content">
      <h1>translateY</h1>
      <p>This parallax method is probably the most common one. It has the background image on a seperate element and 2d translates that element onscroll. We can do better than this.</p>
    </div>
  </div>

On the CSS side, we're absolute positioning the background element.

#hero {
  position: relative;
  height: 750px;
  overflow: hidden;
}

#hero-bg {
  position: absolute;
  width: 100%;
  height: 750px;
  top: 0;
  bottom: 0;
  background: url('bg.jpg') 50% 0 no-repeat;
  background-size: cover;
}

In the JavaScript we have a function that updates our translate settings on every scroll event.

On one hand, we can move the new background element with relative positioning using the top attribute.

On the other hand, we can make use of the translateY attribute. The latter delivers better performance as translateY takes rendering to the GPU level. Both methods can be observed below but we'll be using the latter for this demonstration.

updatePosition = function() {
  var heroBg = document.getElementById('hero-bg');
  var newPos = window.pageYOffset / 2;

  translateY(heroBg, newPos);
  // We could use relative top positioning here instead
  // but that will always be slower
  // heroBg.style.top = newPos + 'px';
};

function translateY(elm, value) {
  var translate = 'translateY(' + value + 'px)';
  elm.style['-webkit-transform'] = translate;
  elm.style['-moz-transform'] = translate;
  elm.style['-ms-transform'] = translate;
  elm.style['-o-transform'] = translate;
  elm.style.transform = translate;
}

window.addEventListener('scroll', updatePosition, false);

Results: It's Alright - But We Can Do Better

View the demo

translateY performance

The translateY performance for this technique is much better than the one we saw for technique 1 (background-position) but we're still seeing spikes of slow rendering. We need to take this to the next level.

Technique 3: Asparagus

We could settle for the other techniques but Asparagus is where we Bump the Lamp".

'But why?' you may ask, feeling that the other techniques are good enough. We've discussed the individual performance issues above but lets focus on the two general problems. First, as the performance profiling indicated (the green bars, remember), the GPU isn't being utilized nearly as much as it could with the most common methods. Secondly, calculations are being done at a much higher rate than is actually needed, causing constant reflow and repaint in the browser.

This is where requestAnimationFrame and translate3d come to the rescue.

To limit the rate at which calculation is being done we'll be using the awesome requestAnimationFrame (rAF) API. Without going into too much detail, rAF collects your constant rendering updates into a single reflow and repaint cycle, and this ensures that your animation calculation is being done in a balanced 'sweetspot' of constant calculation and smooth rendering. To learn more about rAF I recommend reading this article by Paul Irish and that article by Paul Lewis.

The markup is the same as we used above in technique 2 but we'll be using translate3d(x,y,z) instead of translateY(y) for the actual translation of the background element. This will do wonders for our rendering even though we're only going to be using the y paramter of translate3d with 0px given for the x and z axis.

So lets take a look at what's happening under the hood.

We start off by attaching a simple function to the window's scroll event:

var lastScrollY = 0,
    ticking = false,
    bgElm = document.getElementById('hero-bg'),
    speedDivider = 2;

// Update scroll value and request tick
var doScroll = function() {
  lastScrollY = window.pageYOffset;
  requestTick();
};

window.addEventListener('scroll', doScroll, false);

As you can see, the lastScrollY variable is being updated for each scroll event and requestTick() is being called. This will pass our updatePosition function to the the rAF API. What it also does is ensure that the background position isn't being updated multiple times concurrently:

var requestTick = function() {
  if (!ticking) {
    window.requestAnimationFrame(updatePosition);
    ticking = true;
  }
};

translate3d is used in the function rather than translateY which allow the true power of the GPU to be unleashed.

var updatePosition = function() {
  var translateValue = lastScrollY / speedDivider;

  // We don't want parallax to happen if scrollpos is below 0
  if (translateValue < 0)
    translateValue = 0;

  translateY3d(bgElm, translateValue);

  // Stop ticking
  ticking = false;
};

// Translates an element on the Y axis using translate3d
// to ensure that the rendering is done by the GPU
var translateY3d = function(elm, value) {
  var translate = 'translate3d(0px,' + value + 'px, 0px)';
  elm.style['-webkit-transform'] = translate;
  elm.style['-moz-transform'] = translate;
  elm.style['-ms-transform'] = translate;
  elm.style['-o-transform'] = translate;
  elm.style.transform = translate;
};

Technique 4: "The Web has moved forward"

Since the last version, the web changed a lot. A new CSS property has been given birth, its name is contain. It basically tells the browser we do not want to affect other elements, when we modify the hero.

Also, when inspecting the timeline, we could notice all other techniques move the element even if it's outside the viewport. Does it make sense?

Chrome Dev Timeline

We can clearly see, that the browser has to calculate (and paint, but the browser must always paint and composite on scoll) - unnecessarily!

To mitigate this, we add a new variable to updatePosition. The drawback: clientTop and clientHeight cause reflows. But we set the height only once and pageYOffset sadly causes a reflow every frame anyway.

if (!elemY) elemY = bgElm.clientTop + bgElm.clientHeight; // causes reflow only once

In addition, background-size: cover is a culprit. We want it for aesthetic purposes, but resizing huge images is not a cheap task. This post explains how to fix it. TL;DR: We promote the element, so it gets its own GPU accelerated layer. At the same time, we tell the browser that we will transform it, so the engine is prepared.

Last but not least, we take a look at z-index. We don't want to accidentally create new layers, as this makes text blurry and higher VRAM usage. See here. This means we apply z-index: 1 to the hero element and z-index: 2 to the content above it. If you set z-index elsewhere on your page, adjust the z-index of the parallax element to be higher than your always visible body content.

The Results: We Have a Winner!

The performance optimization can be seen very clearly by looking at the demo but we'll also need objective measures to see whether requestAnimationFrame combined with translate3d is the silver bullet for doing parallax animation as we hope.

The timeline now shows a different picture. The green bars have almost vanished and are now being replaced with unfilled bars. This means that rendering is no longer being done by the CPU and has moved to the much faster GPU.

Asparagus performance

About The Author

Olafur Nielsen is a web developer with a huge passion for good user experience. He is a Co-Founder of Form5, an interactive studio based in Reykjavík, Iceland. Check us out at twitter or even GitHub.

About

Demos and an article on performant parallax scrolling

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • HTML 68.1%
  • JavaScript 24.4%
  • CSS 7.5%