Progressive Enhancement with WebGL and React
By: David Lindkvist
Tags: Development, React, HTML, Webgl, Threejs

These days, it’s very common to mix creative technologies like WebGL with traditional HTML/CSS when building websites. In this post, I will try to outline how we at 14islands approach this trend to ensure the content is still available to as many visitors as possible. All of these techniques were used on our new company website 14islands.com.

Thoughts on accessibility

Go back just a few years and responsive web design, progressive enhancement and mobile-first were all the rage. Here’s the thing — they are still valid practices, and just because we have access to new shiny toys doesn’t mean we get a free pass to only build websites for desktop screens and the latest browser technology.

Assumptions made by most WebGL websites

Everybody loves a cool loading animation — Having a preloader allows the browser to download all assets before showing the website. Large images are known to cause lag as they are uploaded to the GPU for the first time. For this reason, it’s common for WebGL apps to preload and pre-render textures before showing the website.

Everybody loves to scroll — Having only one long page means the WebGL canvas can create all objects for the whole website on page load (during the preloader animation), and there are no requests to load new pages or new assets to worry about.

Everybody has a modern browser — Building for advanced browsers and adding polyfills for missing features. Some websites even lock the user out if the browser lacks support for a modern feature.

Because of these assumptions, it’s common to create and position all content using Javascript (e.g. https://github.com/react-spring/react-three-flex). Sometimes, text content is rendered only as WebGL textures, which makes the content essentially images.

In reality, it depends™️

If you’re doing a cool campaign site or interactive experience, many of these assumptions might be correct. However, for many projects, having quick access to the content, a high level of accessibility and SEO are equally important to delivering a memorable creative experience.

Not all websites can afford a long loading animation. Visitors have short attention spans, and it can impact conversion rates and sales.

A progressively enhanced experience

We usually use Gatsby.js on our projects. It’s fast, has a good ecosystem of plugins to speed up common tasks, and offers server-side rendering out of the box.

As any fan of static websites knows, server-side rendering is great for SEO and accessibility since the content can be accessed without executing a bunch of Javascript. We also want to make sure text content is available even if WebGL isn’t supported.

In fact, our javascript heavy React and WebGL website even renders without Javascript support: 😲

With Javascript enabled but no WebGL support, you get some fancier hover states and the ability to play our showreel in a cool dark cinema mode: 😎🍿

And if the browser supports WebGL, you get all the gooey blob magic: 🎩🐇

When feature detection falls short

Unfortunately, some browsers will gladly say that they support WebGL, but they are simply too weak to run the code with good performance.

There’s no good way to feature-detect powerful devices. Some try to measure the frame rate over time or use a combination of other features, but none of them are very robust. In our case, we simply base it on screen size and assume that a larger screen corresponds to a more powerful device (which may or may not be true).

React & Three.js

We like to use the Three.js WebGL library. The pace at which it updates and introduces breaking changes is mind-blowing. Any third-party react library that tries to provide a 1:1 mapping of Three.js objects to React components will probably not be maintained for long.

react-three-fiber takes a different approach by hooking into the React reconciler and proxies JSX tags to Three.js objects. It doesn't even care what version of Three we are using, which makes it future proof. Having said that, there are definitely pitfalls and enough performance gotchas for a future blog post. 😬

Enhancing with WebGL

We created a framework on top of react-three-fiber that enables us to add WebGL functionality to any existing React component on the website.

At the core we have one global shared WebGL canvas that stays in between page loads:

export const wrapRootElement = ({ element }) => (

<>

{element}

<GlobalCanvas/>

</>

)

Each UI component can opt-in to use this global canvas using a custom useCanvas() hook:

const Image = () => {

const ref = useRef()



// add webgl component to global canvas while mounted

useCanvas(<WebGlImage image={ref} />) return <img ref={ref} src={...} />

}

When the <Image> component mounts, it tells the global canvas to render <WebGlImage> . It will also automatically remove the WebGL mesh and clean up resources when it unmounts.

This keeps our code modular and hides the details of the WebGL implementation from regular UI components.

The <WebGlImage> is responsible for hiding the normal image element and draw a WebGL mesh in its place.

Syncing DOM content with WebGL canvas 🔮

A big challenge is to sync the fixed canvas elements with the scrollable HTML content. The technique we use largely follows an approach that tracks “proxy” elements in the normal page flow and updates the WebGL scene positions to match them.

Since Javascript is inevitably slower than native scrolling, this can make the WebGL scene feel laggy. The most common way to fix this is to have some easing on the WebGL position update.

Here’s an example of a WebGL image with easing next to some normal content. Notice how the image all of a sudden feels more smooth than the DOM content because of the easing:

To fix this, most websites also introduce a virtual scroll on the HTML content with a matching easing function. This makes the whole page move with the same feeling, and while it’s for sure less performant than a native scroll, it still perceived as being smoother when all items move at the same speed:

A virtual scroll means that we move all HTML content with Javascript, and it’s a known source of many accessibility issues. For some projects, it’s worth the trade-off, while other projects might require a higher level of accessibility.

There are many libraries for virtual/smooth scrolling available. We suggest you pick one which at least retains the browser’s native scrollbar.

Closing thoughts 💡

To be honest, if load time and maximum device support is your highest priority, you shouldn’t use WebGL at all. But for some websites (like ours), there is a good middle ground.

By adding WebGL to existing HTML elements on the page, we can split up the work better. Some developers can focus on building a solid responsive website layout, and the WebGL can be added on top in a second iteration. Of course, everything needs to be thoroughly tested together, as there are many performance pitfalls when using this technique.

There are downsides to this approach. Loading large images might cause scroll jank, and it becomes harder to achieve smooth page transitions when everything isn’t preloaded. Accessibility also takes a hit due to having virtual scrolling of the DOM.

Let us know if you have ideas on how to improve this even further, we’d love to hear more how others approach this!