Optimizing React App performance using Web Workers
Web is a quirky platform, while very inviting, can cause serious heartburn when under stress.
One important thing to note in here is that the browser cannot refresh the screen when it’s main thread is busy performing some activity. What this means is the page looks stuck. And if it goes on long enough, we get this:
Needless to say, it kills UX.
Over time we came up with ways to tame the beast and it works for most. Performing time consuming operations asynchronously can solve some of the problems when the operations themselves are not resource intensive; as in long polling a server for updates or fetching fresh data based on user action.
All async means here is that it will allow the calling function to wait for the result. Like while fetching list of comments, show some loading gif. Once fetched, remove gif and paint screen with comments.
Here, the browser makes a request and simply waits for the server to respond while keeping the passed in callback function in the callback queue.
Once it receives a response, the JS engine will invoke the callback with the data and you can go about rendering it on screen.
Because the process of issuing an
XMLHttpRequest or rendering the results on screen is not resource intensive, we see no page freezes. So far so good.
One can play around with
All this will do is it will pop our sorting function from the callback queue and push it to the call stack whenever the call stack is empty.
It will then begin it’s operation blocking the main thread. And as the main thread is blocked, the page will feel unresponsive.
Say we have a React component called
App that handles the sort button like this:
And we have a
SortService as this:
Agreed, bubble sort with it’s
O(n^2) . But bear with me for a moment; I am trying to make a point.
As is evident, this sort method will be slightly slow when we have like 20000 items in the list and fall dead from the sky when we have like 40000.
Yes, one can always slice their list and there can be other modifications, but point being there are times when calculations can become heavy.
Like may be we are attempting to perform some image processing or long polling the server every 5 seconds ( no, we haven’t heard of
WebSockets :) ).
Every time we attempt to do any of these, the main thread will be blocked and users will not be able to interact with your app. Pretty bad for user experience.
Enter WebWorkers. According to the MDN page:
Web Workers makes it possible to run a script operation in background thread separate from the main execution thread of a web application. The advantage of this is that laborious processing can be performed in a separate thread, allowing the main (usually the UI) thread to run without being blocked/slowed down.
Let us create a file called
app.worker.js and place it in
As evident, we have simply copied our sort logic inside a
message event listener.
WebWorkers need a slight bit of help before it can be a part of our webpack bundle. Let’s create a file called
Now bring it all together in our
App component like this:
All of these have to be done because a
WebWorker needs to be instantiated as a file, but we want it to be a part of our
webpack bundle. As in, we don’t want to run the worker as a separate app with it’s own build process.
app.worker.js makes it a part of our project’s sourcetree and webpack will bundle it correctly.
WebWorker.js we simply
toString our worker code and make it into a web URL
App.js, we simply create an instance of worker once the components finish mounting and in the
handleSort , we issue a
postMessage to our worker.
Think of the
WebWorker as a logically separate app, like the way we treat
iframes . And the way to interact with an independent app is using
postMessage and adding an
message handler in the app being communicated with.
In the worker, we add a listener for
The worker accepts a payload of a list of users and performs some operation on it and finally, issues a
postMessage with the freshly updated data as the payload.
All the time required for performing the operation is handled in another thread keeping the main thread free for responding to user interactions.
I have put together a small demo.
Open https://react-webworker-demo-rohan-bagchi.surge.sh/ and open your browser console. The weird blue ball should be bouncing left and right.
Try hitting the
Sort Descending Number of Comments without WebWorker button. The ball will immediately stop bouncing.
Try clicking on
Some Button and monitor your
console. Nothing will be printed. In fact, the button itself won’t register your click. No click animations as we all have come to expect of buttons.
This is because the main thread is busy performing the bubble sort. All your actions be it registering your click or playing click animations are queued behind the sort function.
Once the list is sorted, then you can see the console logs getting printed and the ball start bouncing away.
Sort Ascending Number of Comments with WebWorker button.
The user list should go dim but the ball still is bouncing like nothing happened. Try clicking on
Some Button and monitor your
console. You will find the console logs getting printed almost instantaneously.
Finally the whole list is re rendered, sorted in ascending order of number of comments.
Result: you get a snappy experience. No lags.
The code is available here https://github.com/rohanBagchi/react-webworker-demo if you want to give it a spin on your own.
Thanks for reading.
Hit *clap* if you think this was worth your while :)
Find me on linkedin or revert back to me on comments.