The Problem

Our designer has been working with the frankly amazing plugin Slider Revolution for years, but like a lot of powerful tools it delivers the bloat in boatloads. We were using Revolution Slider to create an appealing parallax effect on our homepage, but once we added in the needed tools for SEO and UX tracking the performance was unacceptable for a company that sells optimization as one of its core pillars of web design. We optimized around it for a good long time, but a recent update broke our responsive styling and it just became too much friction for us to work around.

We wanted the maintain the effect but still gain a bunch of speed.

Here’s a video of the performance, and don’t mind the number of tabs that’s just the life of a developer:

As you can see the ultimate effect for our users was less than optimal, and didn’t lend us the most professional aesthetic for a development and marketing company.

The Solution

There are a couple of approaches to parallax effects. The simplest is to use pure CSS to achieve what you’re looking for. This method is severely limited and would mean we have to lose our rocket flying out to the stars as you can’t do effects counter to the scroll direction with pure CSS. The most common method, and what Revolution Slider uses by default is to use JavaScript to manipulate the positional CSS of the DOM elements. This is the easiest to test and visualize and until recently was the way with the most cross-browser compatibility, but it has a huge drawback. Only some CSS properties can be manipulated without triggering a full redraw of the entire page. This is the reason most websites using complex parallax effects feel slightly “laggy” as you are scrolling down them, especially on less powerful devices.

There is another approach that we’ve been playing with for a few years. Modern browsers include a JavaScript API that is designed for very quickly drawing to elements outside of the normal DOM render process: Canvas. By developing a custom JavaScript implementation we could rebuild our effect in an HTML Canvas and greatly reduce the performance cost.

In order to make the change as seamless as possible it would be great if the positioning of the elements could be managed by CSS so that the developer can easy test changes in a browser’s built in developer tools rather than having to hot-swap JavaScript variables via the console. The code will have to be dynamically responsive and adjust appropriately with the user viewport, even if the user resizes in the middle of a session. And finally while it’s possible that an effect like this might only contain decorative elements, we don’t want to lose valuable SEO opportunities by removing images from where web crawlers can process them.

To facilitate the CSS and SEO concerns we’ll need to work off an existing DOM structure for our effect. Our JavaScript will analyze that DOM and inject a Canvas using those elements to generate our final effect. Since we’re working with DOM anyway, we’ll place any needed information such as direction, relative speed, and other properties on the tags themselves to ensure the code we write is as reusable as possible.

Based on the constraints above, we needed to write code that does the following:

  1. Analyzes the targeted DOM elements to pull in Images, animation data, and positional CSS
  2. Injects a new canvas element to then use that data to draw our effect to
  3. Draws the effect to the canvas in relation to the user’s scroll position on the websites
  4. Detects resize events and re-analyses positional CSS in the new state to adjust the effect to the new window size.

The first point wasn’t overly difficult. jQuery is a built in library for all WordPress installations and offers easy functions for determining the relative positioning of elements that with respect to their loaded CSS. The only complication with this step is that in order to make actually styling these effects easier for future developers we decided to allow layout elements inside the declared effect DIV, which meant that we had to shift from a relative declaration of position to an absolute declaration. Additionally, while our current effect starts at the top of the page we want this script to be as reusable as possible for our clients should the need arise again. This meant we had to take into account exactly how far down the page our canvas element is, and offset our “start position” so that it happens within a user’s viewport.

The second point was also very easy. jQuery provides an easy way to access the innerHTML properties of the existing DOM api, so adding our element in was a simple call. There was some complexity in how we structured the canvas element that we inserted. Canvas elements are rasterized and so need a declared resolution. Once this resolution is declared, it can be manipulated with CSS without declaring a new resolution, but the browser scaling can have unintended effects as we will discuss later when we talk about the responsive styling.

The third point is where we got into some fun positional math. Drawing to a canvas is done inside a declared Draw function that is attached to the window.requestAnimationFrame API. This API works independently of the main DOM render, so it can be called at a rate independent of the rest of the browser’s render process. This means that if our effect is taking too long to render on a lower powered device, it will be called at a lower framerate minimizing the impact on user interaction. It also means that if you have a powerful device with a high refresh rate monitor you will see the effect take full advantage of that. The speeds that we move items are simply a product of our current scroll value times our declared speed ratio and then added or subtracted from our X and Y positional values based on our declared direction. Future implementations of this script may allow independent declarations of vertical and horizontal speeds to allow more complex movement directions and speed easing based on the viewport to add more controls to the designer of the effect.

And forth, we want our effect to be responsive. Under perfect circumstances this would mean that we would poll the browser’s “resize” event which is supposed to be fired when the user resizes the window. For some reason we are not aware of, it seems that the Firefox browser never stops firing this event, so attaching our positional analysis to it resulted in abysmal performance in the Firefox browser. For this reason we had to switch to doing the analysis at the beginning of every frame. The end result is the same for the user, but moving that analysis out of every draw call is an area where we could optimize even further if this behavior is ever changed in the Firefox browser.

The Results

See it in action below or on our homepage (https://lvlup.marketing):

 

At first glance the replacement is seamless (if you discount the white space in the original video). There are small changes that can be noticed if you know what to look for, but the overall feeling of the effect has been maintained. Resizing the window we can tell that there are no longer any fragile points on the responsiveness which was our final friction point and why we decided to make this change. The page loads noticeably faster and there is no longer any sort of layout shift as the parallax effect loads in, it simply fades into view once it’s loaded and ready to function.

We use a number of performance tests to gauge our optimization, by making these changes we reduced our First Contentful Paint and Time to Interactive by more than 2 seconds (which is an eternity for a potential customer first loading your website). This reduction was also seen in our fully loaded metric because we avoided loading in all the JavaScript and CSS Revolution Slider relies on to facilitate all its features.

To better facilitate this effect we did have to turn off image lazy loading, so we could gain even more performance benefits if we can find a lazy loading solution that plays well with our parallax effect. Additionally, the default canvas image scaling algorithm does not seem to result in as good a final image when it is asked to reduce images from their initial size. We could switch to using a source-set declaration to scale the loaded image based on the current viewport size to avoid having to scale the image.

Overall the change was a marked improvement, but we do have several more areas where we can focus our optimization efforts in the future.

Do you want to evaluate if a custom solution like this could improve your website? Contact Us.

Interested to see what else we know? Why not read: The Misunderstood World of Online Copyrights.