Aravind Balla/writings

Non blocking updates in React

July 01, 2019

Sometimes, a few updates/computations take up a lot of time. They block the UI from updating, which makes it look as if things are slow. I am not talking about asynchronous data fetches which take time.

TL DR; We will be using web workers as a solution along with Hooks.

Consider this for example

1// Codesandbox - https://codesandbox.io/s/admiring-pond-ixp59
2import React from 'react';
3import ReactDOM from 'react-dom';
4
5const fib = i => (i <= 1 ? i : fib(i - 1) + fib(i - 2));
6
7function App() {
8 const [value, setValue] = React.useState('');
9 const [length, setLength] = React.useState(0);
10
11 // whenever `value` changes
12 React.useEffect(() => {
13 // we calculate the fibonnaci of the length of input * 5
14 const result = fib(value.length * 5);
15 setLength(result);
16 }, [value]);
17
18 const handleChange = async e => {
19 const { value } = e.target;
20 setValue(value);
21 };
22 return (
23 <div className="App">
24 <h1>Hello CodeSandbox</h1>
25 <h2>Start editing to see some magic happen!</h2>
26 <input value={value} onChange={handleChange} />
27 <p>{length}</p>
28 </div>
29 );
30}
31
32const rootElement = document.getElementById('root');
33ReactDOM.render(<App />, rootElement);

When we enter the input here, it takes time to update. And it waits for the update to show up until, till the result calculation is not finished. Fibonacci for large numbers is expensive. It even freezes your browser tab if the input is long.

Do we have a solution to this? Can we some how off-load this computation from the main thread? (Why is he talking about threads in javascript?)

Web Workers

Web workers act as threads which are handled/processed by our browser. We can start a worker as a thread and communicate with it in a particular way. React is after all Javascript UI library, and we are running it in the browser, so why not?

This is the worker, which has to be statically served. (Put in public folder)

1// thread.worker.js
2const fib = i => (i <= 1 ? i : fib(i - 1) + fib(i - 2));
3
4self.addEventListener('message', ({ data }) => {
5 let { type, payload } = data;
6 if (type === 'UPDATE') {
7 payload = payload > 11 ? 11 : payload; // upper limit we set
8 const result = fib(payload * 5);
9 self.postMessage({ type: 'UPDATE_SUCCESS', payload: result });
10 }
11});
12
13self.addEventListener(
14 'exit',
15 () => {
16 process.exit(0);
17 },
18 false
19);

We communicate with the worker using events. Look at the code here, we are listening πŸ‘‚ to message events. We process the data according to type passed and return the result as a message.

If you can guess right, we will have to listen to these messages from the worker in our component. Our component goes like this.

1// App.js
2import React from 'react';
3import ReactDOM from 'react-dom';
4
5import './styles.css';
6
7const worker = new Worker('/thread.worker.js');
8
9function App() {
10 const [value, setValue] = React.useState('');
11 const [length, setLength] = React.useState(0);
12
13 // when mount and unmount
14 React.useEffect(() => {
15 const listener = ({ data: { type, payload } }) => {
16 console.log(type, payload);
17 if (type === 'UPDATE_SUCCESS') setLength(payload);
18 };
19 worker.addEventListener('message', listener);
20 return () => worker.removeEventListener('message', listener);
21 }, []);
22
23 React.useEffect(() => {
24 worker.postMessage({ type: 'UPDATE', payload: value.length });
25 }, [value]);
26
27 const handleChange = async e => {
28 const { value } = e.target;
29 setValue(value);
30 };
31 return (
32 <div className="App">
33 <h1>Hello CodeSandbox</h1>
34 <h2>Start editing to see some magic happen!</h2>
35 <input value={value} onChange={handleChange} />
36 <p>{length}</p>
37 </div>
38 );
39}
40
41const rootElement = document.getElementById('root');
42ReactDOM.render(<App />, rootElement);

If you are using Webpack, you can load it into your component with worker-loader!thread.js. We are directly using Worker() to load it from the public directory.

Here is the codesandbox demo - https://codesandbox.io/s/funny-nightingale-5kxo1

Here is the Effect Hook documentation for reference.

We are adding the listeners for the messages in the first effect, where the dependencies are [], which means this will run when the component is mounting and unmounting.

And in the second effect, we send a message to the worker whenever the value changes.

We can see a huge performance bump with workers when we compare it to the first demo. The load is taken up by the browser now.

That’s how you can use web workers in React. Thanks for reading!

Keep on Hacking! ✌


Aravind Balla

By Aravind Balla who is a cool human, building things for himself, and sometimes for others. You should hit him up on Twitter!