Progressive Enhancement with WebGL and React
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.
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.
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:
Each UI component can opt-in to use this global canvas using a customuseCanvas() hook:
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 drawing 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.
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:
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!