Shimming CSS variables with Web Workers

Hanna Nygård Hollström
Geckoboard: Under The Hood
8 min readMay 9, 2017

We recently built a customization feature for our web app using CSS variables. The feature lets you tweak the look of your TV dashboards by changing the background and widget colors while we show a live preview of your changes.

We used CSS variables inside our React app to make this live preview, which turned out to work really well. See Dan’s post here for more details on the CSS variables solution.

The live-preview feature using CSS variables.

The problem

When deciding to use CSS variables (or custom properties as they are officially called), we knew we would be faced with another problem: Browser support.

Browser support for CSS Custom Properties

There is no support for CSS variables in IE, nor in earlier versions of Edge. We still wanted to offer users of these browsers access to our customization feature, so we researched various ways to do this. The goal was to have a fallback that, if possible, worked as well as our current solution with CSS variables.

This is how we use CSS variables in our SASS files:

.dashboard {
background: var(--theme-primary-color);
}

Our themes are stored as Javascript objects specifying the variables values. These variables get updated when the user makes changes to the theme:

const variables = {
'--theme-primary-color': '#8DC63F',
};

In browsers where CSS variables aren’t supported, we needed to go through the CSS and replace the variables with their new values when they were changed.

We considered creating a microservice to generate the new CSS server-side. We would send the variables to the service as they changed, and the service would replace them in the CSS with the new values. This would create a lot of overhead though and we were concerned about the many network requests we’d have to make.

One alternate idea we had was to update the CSS with the selected colors on the fly client-side instead. This seemed like a better solution to us, but we were worried about the performance, especially on lower-end machines. Ideally, the colors would change instantly as the user interacts with the theme selection, including picking a color from the color picker. This means updates could be triggered repeatedly — potentially making the UI unresponsive and slow to use.

To avoid making the UI sluggish while compiling the CSS, we decided to try this out in a web worker.

What is a web worker?

A web worker is a browser technology that’s been around for a while and is supported in all major browsers, including IE10 and above.

Browser support for Web Workers

Web workers provide a simple means to run scripts in background threads. This means the worker can do more computationally intensive tasks without blocking the UI thread and so keep user interactions fast and responsive.

Once created from the main Javascript thread, a web worker’s general workflow usually follows this sequence:

  1. Wait for a message with instructions from the main thread.
  2. Do some heavy lifting without blocking the UI thread once a message is received.
  3. Post the result back to the main thread, that can then do what it wants with it.

To create a new worker, you call the Worker() constructor, specifying a path to the script file to be run in the worker thread:

const myWorker = new Worker('worker.js');

To communicate with the worker from the main thread, you use the postMessage method and onmessage event handler:

// Post a message to the worker
myWorker.postMessage('Hello Worker!');
// Listen for messages sent by the worker
myWorker.onmessage = event => {
const dataFromWorker = event.data;
console.log('Message received from worker.');
}

So what exactly does this worker look like? A web worker is a script file that can use most of the Javascript language and its APIs. It doesn’t have access to the DOM however, so any DOM operations must be handled by the main Javascript thread. The one thing you’ll have to include in the web worker is the onmessage listener to be able to receive messages from the main thread. The listener is defined in the worker’s global scope:

// Listen to messages from the main thread
onmessage = event => {
console.log('Message received from main script');
const dataFromMainThread = event.data;

// Post message back to main script
postMessage('Hi, I got your message.');
}

That’s all you need to have a basic web worker set up and running.

Our solution

With all this in mind, we decided to build a web worker we could send the CSS variables to as they were changed by the user. The worker would get the CSS (containing variables) and a map of variables to values, it would replace the variables with their values, and send the result back to the main Javascript which would then apply it to the page.

Originally, we had the web worker fetch the CSS file, which seemed to work well at first. But after some testing, we realized this caused CORS issues in IE due to our app architecture. We decided to fetch the CSS in the main thread instead and send it as a string to the web worker along with the variables. This turned out to be a good decision as it made the web worker code simpler, also we didn’t have to bundle an ajax library with the worker. The web worker now accepts the CSS as a string and an object with the variable names and their new values.

Here’s a basic usage that creates the worker and updates a background color.

const worker = new Worker('worker.js')
const cssText = 'body { background: var(--bg-color); }';
const variables = { '--bg-color' : pink };
worker.postMessage({
cssText,
variables,
});
// After generating the CSS with the new colors, the worker will
// send a message back to the main thread:
worker.onmessage = event => {
const newCSS = event.data;
// In this case newCSS will be 'body { background: pink; }'
}

The actual worker code is pretty straight forward. It listens to messages from the main thread and uses a regular expression to replace variables in the CSS.

/*
* regEx Matches all instances of var(*), which is the syntax for
* CSS Variables
* e.g. "color: var(--text-color);" matches "var(--text-color)"
* as a group and "--text-color" as a sub-group
*/
const regEx = /var\(([^)]+)\)/g;
const generateCSS = (cssText, variables) => {
return cssText.replace(regEx, (match, variable) => {
// Return the value for the variable if present or
// leave it untouched.
return variables[variable] || match;
});
};
// Listener for messages from main thread
onmessage = event => {
const { variables, cssText } = event.data;

// Compile the new CSS
const newCss = generateCSS(cssText, variables);

// Post compiled CSS back to main thread
postMessage(newCss);
};

Web worker and React (actual implementation)

In our original solution using CSS variables, we built a React component to handle updating the variables. We created a new component in a similar fashion to send updates to the web worker when the theme was changed by the user. The component receives the updated CSS back from the worker and applies the new styles.

class CSSWorker extends Component {
componentDidMount() {
this.worker = new Worker('css-variables-worker.js');

this.worker.onmessage = event => {
this.setState({ css: event.data });
}
this.variablesDidChange(this.props.variables);
}
componentDidRecieveProps() {
this.variablesDidChange(this.props.variables);
}
variablesDidChange(variables) {
fetch('styles.css').then(response => {
// CSS was fetched, tell worker to generate new CSS
const cssText = response.text();

this.worker.postMessage({
variables,
cssText,
});
});
}
render() {
const { css } = this.state;
return (
<style>
{css}
</style>
);
}
}

That’s the basis for the worker component. All that’s left is to render it to the page when CSS variables aren’t supported. For this we use CSS.supports()to check if CSS variables can be used or not. When CSS variables aren’t supported, we’re replacing our original CSSVariableApplicator component with the new CSSWorker component.

const ThemeApplier = ({ variables }) => {
// Use lodash attempt() and CSS.supports() to check if CSS
// variables are supported
const supportsCSSVars = (
attempt(() => window.CSS.supports('--foo', 'red')) === true
);
const supportsWebWorker = window.Worker; if (!supportsCSSVars && supportsWebWorker) {
return <CSSWorker variables={variables} />;
} else {
return <CSSVariableApplicator variables={variables} />;
}
};

This turned out pretty well as a fallback to CSS variables in this specific case. Changing the colors feels only slightly slower than the original solution in the preview, which is to be expected.

The cons

Of course, using a web worker comes with drawbacks too. It creates more overhead and the code becomes harder to read and follow. Also, many of us haven’t had the chance to use web workers yet which means we might have to study how web workers function before making changes to this part of the app.

We also struggled a bit getting the web worker into our workflow with React and Webpack. Luckily, we found there’s already a webpack loader for web workers that helps with this.

But we can’t ignore the question: did we really need to use a web worker at all for this? Our initial assumption that compiling a new CSS file on the fly would be too much for the UI thread to handle made sense at the time. We assumed we’d need to do a full parse of the CSS in order to replace the variables, and that could potentially have been an expensive operation. However, once we built the web worker, we realized that all it really had to do was to perform a string replacement operation using a regular expression.

This made us wonder if doing that on the main thread would have been OK. We ran some tests using the main thread and found matching and replacing the full string, even on our large CSS file, took less than 1ms and probably wouldn’t have caused any noticeable UI blocking issues.

Of course, we should have done this first when looking for a fallback to CSS variables, not after already building a solution. Sometimes these things happen and it’s all part of building software. Now we know for future projects to run technical tests of all options before choosing an approach to invest more time in.

Right now, we would still have to do more thorough testing to know for sure that compiling the CSS in the main thread wouldn’t cause any performance issues. Since the web worker was already in production (we deploy small, incremental changes dozens of times a day), its performance is good, and it would only be used in the few cases where CSS variables couldn’t be used, we decided to keep it as is for now.

Web workers are a very interesting technology and we’ll keep them in mind as we encounter other relevant use cases in the future. If we ever notice our UI getting unresponsive due to intensive calculations, we’re now ready to tackle the problem with a web worker.

Here’s more info on our customization feature and how you can apply it to your Geckoboard dashboards.

Also, we’re hiring!

--

--