[placeholder]

How we use WebGL at Squarespace

How we use WebGL at Squarespace

Customers who use Squarespace may not have images of their own. We have long catered to this by offering integrated stock photo search, as well as templates that do not primarily rely on imagery. More recently, we introduced Background Art as a new way for customers to add graphics to their websites. This feature leverages WebGL to generate abstract animated graphics client side. These graphics can be seamlessly added to a web page, offering an alternative to images and videos for section backgrounds.

We chose WebGL for this feature because it offers the widest range of possible visual output. Moreover, these types of graphics are typically found in high-end websites created by specialized agencies. Offering this level of fidelity to our customers aligns with our design-centric product philosophy.

With this in mind, we created five distinct generative backgrounds which can be richly customized through user-facing options, using the site’s color palette out of the box. This increases cohesion with other graphical elements compared to abstract stock photos or videos.

Generative background art inside a Squarespace template

The backgrounds themselves are simple by design. Most offer some combination of gradients and organic shapes that aim to call attention to particular content without causing too much distraction. Still, even a simple WebGL implementation runs into unique challenges, particularly when it needs to be integrated within a complex system like the Squarespace website editor.

To understand these challenges and our solutions, we should start with an overview of how WebGL is used in web development today.

WebGL Usage Patterns

To use WebGL in the browser, you first create a canvas element. Then, you retrieve that canvas element’s WebGLRenderingContext. This context provides the methods and constants used to draw on the corresponding canvas. The main constraint here is the one-to-one relationship between a canvas and its context. You cannot draw on different canvas elements using the same context, and you cannot use objects (like buffers or shaders) created with one context on another.

Retrieving a WebGL context

Because of this, the most straightforward way to use WebGL is to place a canvas element in the DOM and use its WebGL context to draw on it directly. This approach is often used to create a single WebGL centerpiece behind other DOM elements. Sometimes, the canvas element will have a fixed position. As the page scrolls, the graphics on the canvas update. This creates a consistent experience where WebGL and the DOM work together. A great example of this is The Sea We Breathe, a website where many modern web technologies are used to create a narrative experience about the world’s oceans.

The direct approach works well for a small number of canvas elements. However, if you try to add too many, you will run into a problem: there is a limit to how many WebGL contexts can be active at any one time. This limit is not fixed. It depends on your browser, device, and any other active processes that use the GPU. In all, the limit may be as low as 2, or as high as 16.

Each WebGL context is essentially a standalone graphics application. No matter how you use it, it will consume some amount of resources. The closer you get to the context limit, the worse the page performance will become. To compound the problem, crossing the limit causes older contexts to be lost. If you want to render to a lost context, it must first be recreated, which will cause another context to be lost. This puts further strain on performance and cooling fans.

A number of solutions to this problem have emerged over the years. The most common is to create a single transparent WebGL canvas with a fixed position, sized to cover only the browser viewport. Instead of placing numerous canvas elements in the DOM, you select certain elements you’d want to render using WebGL. Then, you use their client-bounding rects to position objects in a WebGL scene. As the page scrolls, the canvas itself remains fixed, but the graphics are updated to match the scroll progress. The original elements are hidden and replaced with their WebGL counterparts.

Composition using a transparent canvas overlay

This approach is used to push the presentation of particular elements beyond standard browser capabilities. Common use cases include image and text effects, as can be seen on the homepage for Homunculus, a Tokyo-based digital agency. Another example is the site for the game studio Shape Farm. Here, WebGL is used for image and text effects, as well as a pleasant assortment of decorative elements.

The upside of this approach is that you can use the DOM for layout, including responsive media queries, while cherry-picking which elements get the WebGL treatment. The downside is that all WebGL objects must be on the same z-index, limiting layout options. A bigger concern is that the native page scrolling is faster than the requestAnimationFrame loop that renders the WebGL content. This causes the WebGL objects to visibly lag behind the DOM elements, causing undesired layout shifts. The only reliable way to get around this is to take over the native page scroll, often referred to as scrolljacking. This technique is often criticized for causing usability and accessibility issues. The wide variety of Squarespace templates, as well as the direct editing functionality of the CMS, make this approach further impractical for our use case.

Luckily, modern web technologies offer an alternate approach by way of the OffscreenCanvas API. An Offscreen Canvas shares many properties and methods with a canvas element, but it is not an element itself. As such, it cannot be added to the DOM. As part of its API, you can create an ImageBitmap representing whatever is currently drawn on the Offscreen Canvas. Then, you can transfer this bitmap to other canvas elements using the special ImageBitmapRenderingContext.

A single Offscreen Canvas can render to multiple canvas elements in the DOM. This offers the best of both worlds: full use of the browser’s layout capabilities without manual scroll sync, as well as a minimal number of active WebGL contexts.

OffscreenCanvas with bitmap transfer

The adoption of the browser APIs that this approach relies on is still ongoing, with Safari and Firefox being the major holdouts. Fortunately, established APIs offer a viable, if less performant, fallback. Instead of the Bitmap Rendering Context, we can rely on the trusty canvas 2D drawImage using the WebGL canvas as the source. This approach can be seen in Google’s <model-viewer> Web component, which aims to make displaying 3D models as ubiquitous as displaying images or video. To cover the use case of many <model-viewer> instances existing at the same time, the Model Viewer uses drawImage to copy pixels from an internal canvas to any Model Viewer on the page.

Fallback using drawImage and a 2D context

WebGL in the Squarespace CMS

When we first started thinking about implementing WebGL features in the CMS, we looked at a number of WebGL libraries to do the heavy lifting. However, popular libraries tend to have a larger scope than we strictly require. They aim to support a wide variety of complex 3D scenes, lighting models, animations, and even WebXR features. This comes at the cost of bundle size and runtime overhead. When you do indeed want to render a complex scene, these costs are warranted. But our use cases are far simpler. We may want to render a 2D or 3D scene consisting of a handful of primitive shapes, or just a single texture with a shader effect applied to it.

With this in mind, we decided to create our own WebGL library, which is now used to power the backgrounds. This library supports the simplest possible use case at its core, and any other functionality is layered on top in a modular way. This allows us to optimize for both bundle size and runtime overhead.

Next, we created a library of high-level components that encapsulate their WebGL rendering logic and provide a consistent lifecycle API. One such component exists for each background. When the component is initialized, it is given a DOM node to mount on. An internal manager assigns it a canvas, which is appended to the node. It also receives a WebGL rendering context. Depending on Offscreen Canvas support and the number of other active components, one of the following cases occurs:

  • If Offscreen Canvas is supported (Chrome, Edge), and there’s a sufficient number of active components, the canvas will have a bitmap context, and the rendering context will belong to the Offscreen Canvas. In this case, we can use the optimized bitmap transfer.

  • If Offscreen Canvas is not supported (Firefox, Safari, IE), and there’s a sufficient number of active components, the canvas will have a 2D context, and the rendering context will belong to a regular canvas with a WebGL context. This WebGL canvas is never added to the DOM, so it is effectively an offscreen canvas.

  • If there is a small number of active components, the canvas will have a WebGL context, and the rendering context will belong to that same canvas. This bypasses the offscreen canvas copying mechanism entirely in cases where it's not needed for performance reasons.

In all cases, the components themselves behave identically. They expect a canvas element and a WebGL context, but they don’t know or care where either comes from. When a component wants to render a frame, the internal manager first clears the offscreen canvas. Then the component executes its rendering logic using the provided context. The manager then transfers the resulting pixels to the component’s canvas using the appropriate method. Effectively, this setup separates the on-screen canvas from the rendering context used by the component and provides us with the flexibility to support all modern browsers.

For the Background Art feature, WebGL components are used in the website template to create graphics inside section backgrounds. As an accessibility consideration for persistent animation, each background has a button to pause and resume the animation. If we detect that the visitor has a reduced motion preference, the animation is paused by default.

In the CMS UI, the same components are used to render dynamic previews of each background. In earlier versions, background selection was controlled by a dropdown, but a text label lacks the pizzaz of a representative thumbnail. Moreover, we are able to render the various presets using the user’s site palette colors. Based on user testing, this alone greatly increases the usage of the feature.

CMS UI with live preview thumbnails

The big win here is that we were able to create these previews without forking the WebGL rendering logic, or involving some sort of static image service. The WebGL context resolution is humming along internally, and the consumer of the library is free to create any number of components. This is further optimized by using an Intersection Observer to actively update only the components that are inside the browser viewport.

The Future

As the adoption of the Offscreen Canvas API improves, we will be able to get rid of the less efficient fallback and rely fully on the optimized bitmap transfer path.

Further down the road, the WebGPU API offers some exciting prospects. WebGPU is a more modern graphics API with a wider scope and more robust capabilities than WebGL or WebGL 2. Of particular interest is the ability to render to several output buffers (canvas elements) from a single context. Since our component architecture already supports context sharing, any migration will be easier to complete. The consumers of the components will remain oblivious to any internal changes as well.

In terms of product features, we will continue adding additional backgrounds over time. We will also explore other ways of using WebGL to enhance the presentation of our customers’ content. Wherever that road will lead, we have a solid technical foundation to build upon.

A Better Way to Upload Images

A Better Way to Upload Images

Engineering Manager Forum