Skip to the content.

Posted by Chris on 28 Feb 2022

Background

Unmade’s Editor allows users to apply a custom design to a preconfigured garment, within strict parameters defined by the brand that provides that garment. For example a brand might allow users to apply team logos to specific areas of a jersey, apply patterns to other areas and choose from a defined palette of colours. We call this curated customisation, and it’s intended to make it as easy as possible for a customer with no experience of design software to create an attractive, custom garment that looks the same when manufactured as it did in the web browser.

When designing, the user sees a mixed-reality rendering of their design on a photograph of the garment - possibly on a mannequin or a human model. This is a valuable sales tool, as it helps the user to visualise what this garment will look like when manufactured.

A screenshot of the UnmadeOS Editor

The Problem to Solve

Recently, some of our customers have expressed a desire to use this visualisation system to render designs they’ve produced outside our Editor - so their internal, experienced designers can create designs without facing the restrictive parameters set for home users.

To facilitate this, we decided to build a browser app that would convert the manufacturing templates we already hold for each garment into more user-friendly templates that can be opened and edited in desktop graphic software (eg. Adobe Illustrator), then saved and uploaded to generate the same visualisations as the Editor would.

The user would be presented with a list of the available products and, when one is selected, the browser would fetch the appropriate manufacturing SVG and modify it, adding instructional content, removing redundant manufacturing information and embedding some codes to help us identify which product the template came from.

At Unmade we make heavy use of SVGs, so we’re already familiar with them, comfortable working with them and have a good understanding of what’s possible in the browser in terms of manipulating them. Having said that, I can’t think of a better time to try something new than when I’m about to start a project that looks easy.

Unlike most web graphics, the SVGs we’re working with would not be rendered to the users screen - they’d be handled in memory and then downloaded. To my mind, this project was a perfect opportunity to learn about web workers.

Web Workers

Traditionally, the browser allowed us, as developers, access to a single thread upon which to execute code. This essentially means only one thing can be done at a time. Whenever you see janky scrolling or stuttering animations, it’s usually because the browser is doing something hard and has less resources free to do stuff like paint frequent page updates. Web workers allow us to offload tasks to background threads, freeing up the main thread and ensuring that our code is less obstructive to the user. This is a nice example showing how delegating tasks to a worker leaves the UI free to make updates (change the button state, cursor, show a loader etc.).

In our new project, the main thread remains responsible for the UI, but when the user clicks our download button, rather than the main thread fetching and modifying the SVG, we create a new web worker and fire it a message telling it which SVG to fetch. The worker sends a return message when it has completed its operation, contained in which is a link to the generated SVG.

The user is presented with a loading spinner which animates smoothly whilst the worker beavers away, because the main thread is free to paint each animation frame. Additionally, the user can request templates for other products at the same time without experiencing any visible slowdown, as we spin up a new worker whenever another request is made (within reason - too many concurrent workers can have negative effects on performance).

This makes it sound easy, but there were a few limitations we had to face when implementing this functionality in a web worker.

The Worker Environment

Scripts running in a worker are operating in an isolated environment. Worker scripts do not have access to the DOM, window or document properties, so any information they need from the page must be passed in via a postMessage. Many, but not all, browser APIs are available in workers, and much of the functionality we required was not available by default.

Task Required API Available in a worker?
API calls / load remote SVG fetch y
Add instructions to SVG / Remove elements from SVG DOM methods (createElement, removeElement, querySelector etc) n
Create a link to modified SVG URL.createObjectURL() y

DOM in a worker

The worker environment is DOM-free, you cannot create a DOM in a worker - so manipulating a document (eg. an SVG) is not easily achievable. We could try wrestling with our SVG as a string, maybe try (and fail) to write some regex, but I’m not into that kind of torture.

Of course, the worker environment is not the only JavaScript environment without a DOM - many packages already exist to solve these problems in node. After reading up on how JSDOM works, I was knee-deep in trying to implement parse5 with css-select when I saw this tweet from Wes Bos about linkedom - a DOM implementation for DOM-less environments.

Linkedom implements near-identical syntax to that you’d use in regular browser code, which makes it ideal for picking out existing browser code and moving it into a worker.

In a browser I would use window.DOMParser to construct a DOM from the string contents of the fetched SVG.

const svgString =
  "<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><circle cx='50' cy='50' r='40' stroke='green' stroke-width='4' fill='yellow' /></svg>";
const parser = new DOMParser();
const doc = parser.parseFromString(svgString, "image/svg+xml");
const svg = doc.documentElement;
const circle = svg.querySelector("circle");
const circleColour = circle.getAttribute("fill");

In my worker, where there is no window, I can use linkedom.DOMParser with exactly the same syntax, and get back an SVGDocument upon which I can use linkedom implementations of other DOM APIs include createElement, querySelector, appendChild etc.

import { DOMParser } from "linkedom/worker";

const svgString =
  "<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><circle cx='50' cy='50' r='40' stroke='green' stroke-width='4' fill='yellow' /></svg>";
const parser = new DOMParser();
const doc = parser.parseFromString(svgString, "image/svg+xml");
const svg = doc.documentElement;
const circle = svg.querySelector("circle");
const circleColour = circle.getAttribute("fill");

It’s a huge benefit to be able to use the same syntax as standard DOM APIs and removes a barrier to entry for this work. There’s almost nothing new to learn to get this running in a worker, which is fantastic, and means anyone on my team can easily pick this up and make changes to it whenever necessary.

Passing the SVG to the main thread

Once my worker is finished modifying the SVG, I need to pass it back to the main thread so the user can download it. Rather than create a postMessage with the entire SVG source as a string, I use URL.createObjectURL to create a local URL pointing to a blob representing the SVG.

const svgToObjectURL = (svg: SVGDocument): string => {
  const string = svg.toString();
  const blob = new Blob([string], { type: "image/svg+xml" });
  return URL.createObjectURL(blob);
};

Because this is a URL, I am then able to create an a tag with the href set to this link, give it a download attribute with my chosen filename and fire it’s click event to have the file download to the user’s machine.

Where next?

This is the first project we’ve built that uses web workers, and the ease of implementing them has really motivated me to identify functionality in our other projects that could be offloaded to a worker.

One of our core products is a JavaScript project that also generates SVGs that are never shown directly to the user, but instead rasterised and warped by WebGL before the user sees them. This seems like a prime candidate for workerisation and I’m interested to see what performance gains that could provide us. As an added bonus, this project is also run server-side, currently using Puppeteer. When it’s sufficiently modified to work in a web worker, it should also run in node and we can drop the browser requirement entirely which should give us some drastic performance improvements on the back end.

Back to home