New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Webworkers #3092

Closed
SanderSpies opened this Issue Feb 9, 2015 · 28 comments

Comments

Projects
None yet
@SanderSpies
Contributor

SanderSpies commented Feb 9, 2015

There has been discussion about webworkers before, @petehunt made already an implementation and @sebmarkbage has some ideas on how he wants to accomplish it within the current code base. If I remember correctly, React should not just run in a webworker but both in the "main thread" and a webworker.

So, what are we waiting for? @sebmarkbage would be great if you could give a more detailed outline of your ideas so we can get this moving :-).

@sophiebits

This comment has been minimized.

Show comment
Hide comment
@SanderSpies

This comment has been minimized.

Show comment
Hide comment
@SanderSpies

SanderSpies Feb 17, 2015

Contributor

So I'm currently thinking of taking the following steps:

  • implement prerender as a function of ReactClass
  • implement createCallbackElement next to createElement which initially just returns an empty dummy element that can be updated by the webworker
  • create a WebWorker wrapper component (<WebWorker />)
  • optimize render call with using prerender

Not exactly sure yet about:

  • starting webworkers (wonder what the performance penalties are)
  • syncing UI changes
  • dealing with events
  • passing React inside the webworker, perhaps it should be something smaller?
  • deal with non-webworker browsers

Feedback wanted :-)

Contributor

SanderSpies commented Feb 17, 2015

So I'm currently thinking of taking the following steps:

  • implement prerender as a function of ReactClass
  • implement createCallbackElement next to createElement which initially just returns an empty dummy element that can be updated by the webworker
  • create a WebWorker wrapper component (<WebWorker />)
  • optimize render call with using prerender

Not exactly sure yet about:

  • starting webworkers (wonder what the performance penalties are)
  • syncing UI changes
  • dealing with events
  • passing React inside the webworker, perhaps it should be something smaller?
  • deal with non-webworker browsers

Feedback wanted :-)

@sebmarkbage

This comment has been minimized.

Show comment
Hide comment
@sebmarkbage

sebmarkbage Feb 17, 2015

Member

@cecilemuller The problem of just using the DOM is that you can no longer integrate with synchronous APIs. So you have to build high level wrapper APIs on the main thread. IMO, the best way of building high level APIs on the main thread is to build them as React components that expose asynchronous APIs. Which is why I think that the solution is react->react rendering.

@SanderSpies The top section requires invasive changes to the core but we're working on doing them anyway. What we need help with is all the basic stuff.

  • How do you start the worker?
  • How do you load modules in a worker?
  • How do you determine the root to render into?
  • How do we unit test the system?
Member

sebmarkbage commented Feb 17, 2015

@cecilemuller The problem of just using the DOM is that you can no longer integrate with synchronous APIs. So you have to build high level wrapper APIs on the main thread. IMO, the best way of building high level APIs on the main thread is to build them as React components that expose asynchronous APIs. Which is why I think that the solution is react->react rendering.

@SanderSpies The top section requires invasive changes to the core but we're working on doing them anyway. What we need help with is all the basic stuff.

  • How do you start the worker?
  • How do you load modules in a worker?
  • How do you determine the root to render into?
  • How do we unit test the system?
@SanderSpies

This comment has been minimized.

Show comment
Hide comment
@SanderSpies

SanderSpies Feb 18, 2015

Contributor

How do you start a worker?
For now I can see the following solutions for this problem:

  • separate file that includes only what is necessary for the worker, which would require extra build steps
  • create a worker on the fly (blob), which will not work in every browser and I expect would have performance penalties. Also resolving dependencies here for the worker is going to be painful - if not impossible without extra build steps.
  • start the entire build in multiple workers, still this would still require the usage of a build tool

So yeah, for now I don't see this working without a build tool. My preference would go to the first one.

How do you determine the root to render into?
I would expect the "main" React to always start in the main thread, and components leaving stubs in this thread to which they can write when they want to. Of course writing to the DOM still needs to be done via the normal React reconciliation mechanism.

It should be possible to have a single worker which is used for multiple components, which makes it a bit more challenging. Probably an extra id needs to be given to communicate to the right component.

How do we unit test the system?
If you would be testing a render function, it would initially only show the webworker stubs - and testing the result of a webworker would be something different. Something like a callback for a webworker result could work here (waitFor(webworkerId) comes to mind).

If there are other options here or I'm missing something, I would definitely like to hear it!

Contributor

SanderSpies commented Feb 18, 2015

How do you start a worker?
For now I can see the following solutions for this problem:

  • separate file that includes only what is necessary for the worker, which would require extra build steps
  • create a worker on the fly (blob), which will not work in every browser and I expect would have performance penalties. Also resolving dependencies here for the worker is going to be painful - if not impossible without extra build steps.
  • start the entire build in multiple workers, still this would still require the usage of a build tool

So yeah, for now I don't see this working without a build tool. My preference would go to the first one.

How do you determine the root to render into?
I would expect the "main" React to always start in the main thread, and components leaving stubs in this thread to which they can write when they want to. Of course writing to the DOM still needs to be done via the normal React reconciliation mechanism.

It should be possible to have a single worker which is used for multiple components, which makes it a bit more challenging. Probably an extra id needs to be given to communicate to the right component.

How do we unit test the system?
If you would be testing a render function, it would initially only show the webworker stubs - and testing the result of a webworker would be something different. Something like a callback for a webworker result could work here (waitFor(webworkerId) comes to mind).

If there are other options here or I'm missing something, I would definitely like to hear it!

@bluejamesbond

This comment has been minimized.

Show comment
Hide comment
@bluejamesbond

bluejamesbond Jun 17, 2015

What progress has been made towards this?

bluejamesbond commented Jun 17, 2015

What progress has been made towards this?

@sophiebits

This comment has been minimized.

Show comment
Hide comment
@sophiebits

sophiebits Jun 18, 2015

Member

@bluejamesbond None explicitly for web workers, though we've done a lot of refactoring recently with the goal of making it possible to have multiple reconcilers and make things more serializable, which helps with this goal.

Member

sophiebits commented Jun 18, 2015

@bluejamesbond None explicitly for web workers, though we've done a lot of refactoring recently with the goal of making it possible to have multiple reconcilers and make things more serializable, which helps with this goal.

@pfrazee

This comment has been minimized.

Show comment
Hide comment
@pfrazee

pfrazee Oct 24, 2015

Any new work in this area?

pfrazee commented Oct 24, 2015

Any new work in this area?

@sophiebits

This comment has been minimized.

Show comment
Hide comment
@sophiebits

sophiebits Oct 24, 2015

Member

No, same as my last message.

Member

sophiebits commented Oct 24, 2015

No, same as my last message.

@pfrazee

This comment has been minimized.

Show comment
Hide comment
@pfrazee

pfrazee Oct 24, 2015

Ok. I may be able to work on this later. Are there technical blockers, or is it just a matter of getting the code written?

pfrazee commented Oct 24, 2015

Ok. I may be able to work on this later. Are there technical blockers, or is it just a matter of getting the code written?

@bluejamesbond

This comment has been minimized.

Show comment
Hide comment
@bluejamesbond

bluejamesbond Nov 11, 2015

You can always have a "proxy dom" in workers which emits messages to the DOM on the main thread. It is trivial to render elements, but handling events will require some work. A proof-of-concept (only rendering) can be found here

bluejamesbond commented Nov 11, 2015

You can always have a "proxy dom" in workers which emits messages to the DOM on the main thread. It is trivial to render elements, but handling events will require some work. A proof-of-concept (only rendering) can be found here

@axemclion

This comment has been minimized.

Show comment
Hide comment
@axemclion

axemclion Dec 2, 2015

I wrote a custom renderer for ReactJS using Web Workers to try and get some performance numbers. Here is the demo page and the repo.

I noticed that for small applications, the cost associated with postMessage seems to make the WebWorker approach slower than using React-dom. For applications like DBMonster when the number of nodes is high, this pays off. The performance also depends on the batching the messages when sent between the web worker and the main UI thread and the batch size needs to be chosen carefully.

Here is the detailed analysis.

axemclion commented Dec 2, 2015

I wrote a custom renderer for ReactJS using Web Workers to try and get some performance numbers. Here is the demo page and the repo.

I noticed that for small applications, the cost associated with postMessage seems to make the WebWorker approach slower than using React-dom. For applications like DBMonster when the number of nodes is high, this pays off. The performance also depends on the batching the messages when sent between the web worker and the main UI thread and the batch size needs to be chosen carefully.

Here is the detailed analysis.

@jimfb

This comment has been minimized.

Show comment
Hide comment
@jimfb

jimfb Dec 2, 2015

Contributor

@axemclion Thanks for the writeup, that's pretty cool! Your writeup mentioned that the bottle neck appears to become the mutations on the UI thread. Presumably this is because you are mutating the values in every row as frequently as possible, but this is obviously not representative of real-world use cases. It is my belief that most rerenders result in very few changes (That is, the majority of a typical application remains unchanged from one frame to the next). Under the assumption that a typical rerender would do a lot of "work" (calculating diffs) but affect a small number of dom nodes, I wonder if batching becomes less significant and the overall performance benefits of running in a worker become more evident. Especially with respect to things like scroll performance while running a rerender in the background.

Contributor

jimfb commented Dec 2, 2015

@axemclion Thanks for the writeup, that's pretty cool! Your writeup mentioned that the bottle neck appears to become the mutations on the UI thread. Presumably this is because you are mutating the values in every row as frequently as possible, but this is obviously not representative of real-world use cases. It is my belief that most rerenders result in very few changes (That is, the majority of a typical application remains unchanged from one frame to the next). Under the assumption that a typical rerender would do a lot of "work" (calculating diffs) but affect a small number of dom nodes, I wonder if batching becomes less significant and the overall performance benefits of running in a worker become more evident. Especially with respect to things like scroll performance while running a rerender in the background.

@axemclion

This comment has been minimized.

Show comment
Hide comment
@axemclion

axemclion Dec 2, 2015

@jimfb You are right about the real world use case - most of the time, there would be expensive dom-diffing, but would result in very little change to the browser DOM. The batching would still be an interesting problem since postMessage seems to be another issue.

In the experiment, when I set the batch size to 1 (i.e.) just call postMessage for every DOM mutation, the speed is worse than when setting the batch size to something a little bigger (say 10 or 50).

I think there is scope for some clever batching, where the UI thread handing the DOM mutations tells us how long those mutations take, and based on that the worker determines the batch size. This way, we don't send so many mutations that we take more than 16ms, or too less that we start building a backup.

For scroll, I also tried running the DOM mutations in a requestAnimationFrame, and that did seem to make it better.

axemclion commented Dec 2, 2015

@jimfb You are right about the real world use case - most of the time, there would be expensive dom-diffing, but would result in very little change to the browser DOM. The batching would still be an interesting problem since postMessage seems to be another issue.

In the experiment, when I set the batch size to 1 (i.e.) just call postMessage for every DOM mutation, the speed is worse than when setting the batch size to something a little bigger (say 10 or 50).

I think there is scope for some clever batching, where the UI thread handing the DOM mutations tells us how long those mutations take, and based on that the worker determines the batch size. This way, we don't send so many mutations that we take more than 16ms, or too less that we start building a backup.

For scroll, I also tried running the DOM mutations in a requestAnimationFrame, and that did seem to make it better.

@pfrazee

This comment has been minimized.

Show comment
Hide comment
@pfrazee

pfrazee Dec 2, 2015

Forgive me if this is an obvious suggestion, but have you tried benchmarking transferrable objects?

pfrazee commented Dec 2, 2015

Forgive me if this is an obvious suggestion, but have you tried benchmarking transferrable objects?

@axemclion

This comment has been minimized.

Show comment
Hide comment
@axemclion

axemclion Dec 3, 2015

@pfraze That is a good idea, i could try that. Converting DOM mutations into transferrable objects may be hard though.

axemclion commented Dec 3, 2015

@pfraze That is a good idea, i could try that. Converting DOM mutations into transferrable objects may be hard though.

@axemclion

This comment has been minimized.

Show comment
Hide comment
@axemclion

axemclion Jan 5, 2016

@pfraze I tried transferrable objects, and more simply, just use JSON.stringify - the perf is actually pretty good. It continues to stay that way (better than react-dom), even as the number of objects increase. I am currently running the tests on my machines and will post out the results.

Thanks for the suggestion :)

axemclion commented Jan 5, 2016

@pfraze I tried transferrable objects, and more simply, just use JSON.stringify - the perf is actually pretty good. It continues to stay that way (better than react-dom), even as the number of objects increase. I am currently running the tests on my machines and will post out the results.

Thanks for the suggestion :)

@pfrazee

This comment has been minimized.

Show comment
Hide comment
@pfrazee

pfrazee Jan 5, 2016

@axemclion good deal. Have you given any thought to using react-workers as a plugin sandbox?

pfrazee commented Jan 5, 2016

@axemclion good deal. Have you given any thought to using react-workers as a plugin sandbox?

@axemclion

This comment has been minimized.

Show comment
Hide comment
@axemclion

axemclion Feb 10, 2016

Update to the experiment - using JSON.stringify does indeed make web workers implementation faster than react running in a UI thread - here are the numbers - http://blog.nparashuram.com/2016/02/using-webworkers-to-make-react-faster.html.

I was also able to get events working, and created a simple todo app

@ pfraze, did not understand your comment about react-workers as a plugin sandbox. Can you please explain ?

axemclion commented Feb 10, 2016

Update to the experiment - using JSON.stringify does indeed make web workers implementation faster than react running in a UI thread - here are the numbers - http://blog.nparashuram.com/2016/02/using-webworkers-to-make-react-faster.html.

I was also able to get events working, and created a simple todo app

@ pfraze, did not understand your comment about react-workers as a plugin sandbox. Can you please explain ?

@pfrazee

This comment has been minimized.

Show comment
Hide comment
@pfrazee

pfrazee Feb 11, 2016

@axemclion workers run in a different thread and VM than the page, making them better for sandboxing than iframes. The thread-independence 1. scales better, and 2. prevents a DoS attack (while(true){} capturing the thread). But workers havent been good for UI work because they didnt have DOM access, which you've solved using React. I think you could build a good plugins framework for react apps with this, though you'll need to evaluate the worker-sandboxing somewhat first (CSPs should help)

pfrazee commented Feb 11, 2016

@axemclion workers run in a different thread and VM than the page, making them better for sandboxing than iframes. The thread-independence 1. scales better, and 2. prevents a DoS attack (while(true){} capturing the thread). But workers havent been good for UI work because they didnt have DOM access, which you've solved using React. I think you could build a good plugins framework for react apps with this, though you'll need to evaluate the worker-sandboxing somewhat first (CSPs should help)

@sebmarkbage

This comment has been minimized.

Show comment
Hide comment
@sebmarkbage

sebmarkbage Feb 12, 2016

Member

My latest thinking on workers:

We definitely want to solve concurrency. It is a major issue to let some work get out of the way. Particularly scrolling and other high-priority interaction that requires immediate feedback.

One way to solve that is through cooperative scheduling. I.e. making React and everything around React more "yieldy". Another is by adding preemptive scheduling (OS threads / workers) which adds additional guarantees about hitting frame deadlines.

One initial step we could take is move everything into a Worker as proposed here. We can reimplement/replace the DOM. There is, however, one major limitation. Text measurement. Browser layout is currently only available from the main thread. Even if we implement our own layout algorithm ( https://github.com/facebook/css-layout ), we wouldn't have access to text measurement. Even if we implemented our own text measurement, we wouldn't have access to the browser font files for internationalization fallback. Unless browsers are willing to give us text measurement in a Worker, our only option is to do layout on the main thread.

React Native doesn't have that limitation because it can get text measurements in the Worker thread.

That means that as soon as any of your work in the Worker is dependent on layout, you're going to be sending blocking work to be executed on the main thread. You'll have to wait for the round trip and probably stall the animation/scrolling while doing so.

However, luckily for us Compositor Workers are moving along as a spec:

https://github.com/w3c/css-houdini-drafts/blob/master/composited-scrolling-and-animation/Explainer.md

This would allow us to keep executing on the main thread, but also put code on the Compositor thread. We could build a React for the Compositor to allow us to easily build components for that thread.

If we had that, then it is not clear if React in a Web Worker buys us much more than some potential parallelism at the cost of lost scheduling ability, module initialization overhead (shared code executed twice), serialization overhead and added complexity.

If Compositors weren't happening, or if we wanted to render an entire page in WebGL, then we would wait for text measurement in Workers before moving into that model.

We could potentially use them to some effect as an intermediate step but it is going to be less than ideal. It would also require a lot of work to port our own code and move the community over to that model completely. Seems safer to wait for compositor workers. I suspect that its implementation will go quick once one browser has it and popular websites are utilizing it. Spec might take a while and it might be fatally underpowered in its first version. So we'll see how long it takes. In the meantime, we have React Native.

(Relay in a worker on the other hand seems very plausible.)

Member

sebmarkbage commented Feb 12, 2016

My latest thinking on workers:

We definitely want to solve concurrency. It is a major issue to let some work get out of the way. Particularly scrolling and other high-priority interaction that requires immediate feedback.

One way to solve that is through cooperative scheduling. I.e. making React and everything around React more "yieldy". Another is by adding preemptive scheduling (OS threads / workers) which adds additional guarantees about hitting frame deadlines.

One initial step we could take is move everything into a Worker as proposed here. We can reimplement/replace the DOM. There is, however, one major limitation. Text measurement. Browser layout is currently only available from the main thread. Even if we implement our own layout algorithm ( https://github.com/facebook/css-layout ), we wouldn't have access to text measurement. Even if we implemented our own text measurement, we wouldn't have access to the browser font files for internationalization fallback. Unless browsers are willing to give us text measurement in a Worker, our only option is to do layout on the main thread.

React Native doesn't have that limitation because it can get text measurements in the Worker thread.

That means that as soon as any of your work in the Worker is dependent on layout, you're going to be sending blocking work to be executed on the main thread. You'll have to wait for the round trip and probably stall the animation/scrolling while doing so.

However, luckily for us Compositor Workers are moving along as a spec:

https://github.com/w3c/css-houdini-drafts/blob/master/composited-scrolling-and-animation/Explainer.md

This would allow us to keep executing on the main thread, but also put code on the Compositor thread. We could build a React for the Compositor to allow us to easily build components for that thread.

If we had that, then it is not clear if React in a Web Worker buys us much more than some potential parallelism at the cost of lost scheduling ability, module initialization overhead (shared code executed twice), serialization overhead and added complexity.

If Compositors weren't happening, or if we wanted to render an entire page in WebGL, then we would wait for text measurement in Workers before moving into that model.

We could potentially use them to some effect as an intermediate step but it is going to be less than ideal. It would also require a lot of work to port our own code and move the community over to that model completely. Seems safer to wait for compositor workers. I suspect that its implementation will go quick once one browser has it and popular websites are utilizing it. Spec might take a while and it might be fatally underpowered in its first version. So we'll see how long it takes. In the meantime, we have React Native.

(Relay in a worker on the other hand seems very plausible.)

@josephsavona

This comment has been minimized.

Show comment
Hide comment
@josephsavona

josephsavona Feb 12, 2016

Contributor

Relay in a worker on the other hand seems very plausible.

Yup, and we're actively exploring this.

Contributor

josephsavona commented Feb 12, 2016

Relay in a worker on the other hand seems very plausible.

Yup, and we're actively exploring this.

@slorber

This comment has been minimized.

Show comment
Hide comment
@slorber

slorber Feb 12, 2016

Contributor

Hi,

Not sure to understand the full problem, but what I understand is that message passing between worker and main thread is expensive.

@sebmarkbage instead of focusing on using more powerful workers, shouldn't we focus on making message passing between worker and main thread more efficient? Like for example supporting native support for immutable values, and allowing to pass to worker an immutable object by reference without any serialization?

Contributor

slorber commented Feb 12, 2016

Hi,

Not sure to understand the full problem, but what I understand is that message passing between worker and main thread is expensive.

@sebmarkbage instead of focusing on using more powerful workers, shouldn't we focus on making message passing between worker and main thread more efficient? Like for example supporting native support for immutable values, and allowing to pass to worker an immutable object by reference without any serialization?

@fdecampredon

This comment has been minimized.

Show comment
Hide comment
@fdecampredon

fdecampredon Feb 12, 2016

Even if we implemented our own text measurement, we wouldn't have access to the browser font files for internationalization fallback.

@sebmarkbage Just a little question about that, would it be completely silly to think that we could be able to use an embedded font that would work on pretty much all language (From what I remember there is a version of arial that has been developed for that purpose), and to embed corresponding font metrics in js ?

fdecampredon commented Feb 12, 2016

Even if we implemented our own text measurement, we wouldn't have access to the browser font files for internationalization fallback.

@sebmarkbage Just a little question about that, would it be completely silly to think that we could be able to use an embedded font that would work on pretty much all language (From what I remember there is a version of arial that has been developed for that purpose), and to embed corresponding font metrics in js ?

@sebmarkbage

This comment has been minimized.

Show comment
Hide comment
@sebmarkbage

sebmarkbage Feb 12, 2016

Member

@Slober It's not terrible expensive and there are ways around it. We should also be focusing on that, and I am, through TC39 but that's an orthogonal problem to this issue. (Edit: Also note that starting up modules and code in each worker isn't free neither. Increases start up time if you have a lot of overlap.)

@fdecampredon There are 120,000 unicode characters. When you turn them into glyphs a large number of them have ligature combinations which makes that number explode. Each CJK language have their own font/glyph design for the same character. That explodes that set but most of those are fixed size so maybe you could optimize/compress those. An embedded font, even if it only has all glyph sizes, would be quite large. For proper sizing and shaping you need more information than that. Even if you did that, you would still not have the same exact font combination that that particular browser has. I.e. that gets rasterized on the screen. So you would end up win cases of incorrect measurements.

Member

sebmarkbage commented Feb 12, 2016

@Slober It's not terrible expensive and there are ways around it. We should also be focusing on that, and I am, through TC39 but that's an orthogonal problem to this issue. (Edit: Also note that starting up modules and code in each worker isn't free neither. Increases start up time if you have a lot of overlap.)

@fdecampredon There are 120,000 unicode characters. When you turn them into glyphs a large number of them have ligature combinations which makes that number explode. Each CJK language have their own font/glyph design for the same character. That explodes that set but most of those are fixed size so maybe you could optimize/compress those. An embedded font, even if it only has all glyph sizes, would be quite large. For proper sizing and shaping you need more information than that. Even if you did that, you would still not have the same exact font combination that that particular browser has. I.e. that gets rasterized on the screen. So you would end up win cases of incorrect measurements.

@fdecampredon

This comment has been minimized.

Show comment
Hide comment
@fdecampredon

fdecampredon Feb 12, 2016

Ok it was silly :p thanks for the answer.
I developed a very simple word wrap algorithm for svg element in the past thanks to canvas measureText, perhaps the OffScreenCanvas could help if it gets adopted by all major browsers.

fdecampredon commented Feb 12, 2016

Ok it was silly :p thanks for the answer.
I developed a very simple word wrap algorithm for svg element in the past thanks to canvas measureText, perhaps the OffScreenCanvas could help if it gets adopted by all major browsers.

@sompylasar

This comment has been minimized.

Show comment
Hide comment
@sompylasar

sompylasar Jan 21, 2017

Contributor

@sebmarkbage Could you please clarify how is this related to the Fiber architecture? Thanks!

Contributor

sompylasar commented Jan 21, 2017

@sebmarkbage Could you please clarify how is this related to the Fiber architecture? Thanks!

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jan 24, 2017

Member

This might be helpful: #7942 (comment)

Member

gaearon commented Jan 24, 2017

This might be helpful: #7942 (comment)

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Oct 2, 2017

Member

I’ll just close this as we’re not actively exploring web workers for the above reasons.
We are exploring async work on the main thread with cooperative scheduling though.

Member

gaearon commented Oct 2, 2017

I’ll just close this as we’re not actively exploring web workers for the above reasons.
We are exploring async work on the main thread with cooperative scheduling though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment