Skip to content
This repository has been archived by the owner on Sep 21, 2022. It is now read-only.

React compiling down to raw application code #50

Closed
scottmas opened this issue Mar 16, 2017 · 24 comments
Closed

React compiling down to raw application code #50

scottmas opened this issue Mar 16, 2017 · 24 comments

Comments

@scottmas
Copy link

Ahead of Time compiling is becoming a thing in the ever evolving Javascript world. At the end of the day, web frameworks simply have to change (virtual) DOM nodes at the appropriate time. Framework specific logic, like change detection, re-rendering and diffing, etc, are simply ways of letting developers be able to reason about when they expect a change to occur in the DOM.

If a compile step can intimately understand all the code paths a certain application can take, web applications don't have to incur the overhead of implementing the entire framework logic.

Angular is doing it in a limited way and a recent library svelte is requiring AOT compilation.

I'm not sure if this is the right location for this discussion, but I think this is something that the React community seriously needs to begin considering.

@blainekasten
Copy link

This has been on my mind here and there. I think there are 2 sides to this coin.

  1. The code you write is not the code you debug.
  2. The weight of the code is dramatically increased by the need for framework and ceremony.

Frameworks like svelte seem promising, though it is early to know if the appeal is worth it. I personally have concerns about the code being hard to debug. But I think this is a very interesting concept and have tried to think about if it's possible to extract out enough logic into simple and concise code that the framework is unnecessary beyond development conveniences.

@styfle
Copy link

styfle commented Mar 17, 2017

@blainekasten What makes you say this?

The code you write is not the code you debug

Today, I debug ES6 code that has been compiled and running as ES5 code in the browser. Just include the source maps. Is there a reason you think source maps would not work for AOT compiling?

@scottmas
Copy link
Author

scottmas commented Mar 17, 2017

I'm also skeptical source maps will work in the AoT world, except in a very rudimentary form.

Source mapping was invented to solve a problem that is equivalent to translating Australian English (ES6) to American English (ES5). You'll need to change a couple idoms here and there (i.e. change var declarations), but it's pretty straightforward.

However, translating English (React) to Chinese (AoT compiled code) becomes orders of magnitude more complex. The "meaning" remains the same, but nearly all the implementation details are different.

This said, I think this is an acceptable tradeoff if the benefits are significant enough. Particularly since developers will still be able to perform development in React proper, not the AoT version.

It's unclear at this point how large the potential benefits could be, but for example Svelte was able to decrease code size about 12x for TodoMVC (4kb vs 45kb), be about 1.3x faster, and be probably significantly faster on initial boot time since there's far less code to parse. But it's unclear if these benefits scale to complex real world applications.

@scottmas
Copy link
Author

@gaearon @sebmarkbage @acdlite @trueadm @developit and other people working on the future of React (e.g. React Fiber) have opinions?

@gaearon
Copy link
Member

gaearon commented Mar 17, 2017

We might explore a limited version of this in the future. I don't think we'll ever go "full AOT" like Svelte because some things are inherently dynamic (e.g. diffing lists), and inlining that logic into components while preserving full React power would produce bloated bundles.

I'm not very familiar with Angular so can't comment on that comparison but just wanted to note React doesn't have a "template compilation" step in the first place.

@gaearon
Copy link
Member

gaearon commented Mar 17, 2017

You might be interested in these issues: https://github.com/facebook/react/labels/Component%3A%20Optimizing%20Compiler

@acdlite
Copy link
Member

acdlite commented Mar 17, 2017

Yeah, I think the tl;dr version is that we will likely compile to code that modifies React internal data structures (fibers), but not the DOM directly. Will always need a runtime.

@scottmas
Copy link
Author

scottmas commented Mar 17, 2017

Interesting. So just so I can clarify this in my mind... Right now there are three major code pieces that need to be loaded to run a react app: (1) react, (2) react-dom, and (3) the application itself.

So are you saying devs could create a compiled version of the application (3), but that (1) and (2) would remain the same?

@gaearon
Copy link
Member

gaearon commented Mar 17, 2017

Yes. To clarify, we're working on making the bundles smaller. However in our experience the size of React or ReactDOM is not the bottleneck—it's the application code. So we're interested in optimizing that for bigger and scalable wins.

@sophiebits
Copy link
Member

It might be the case that we could cut out some parts of react or react-dom if all components were AOT compiled, but it is a non-goal to have no runtime – just like in Babel it makes sense to have a shared helper for creating classes instead of writing out all of the logic every time.

@developit
Copy link

FWIW I agree about the runtime being a likely necessity, implementing AOT compilation of compositional components without a runtime seems like it would be either impossible or end up being N copies of a microframework for dealing with that case, which puts you in the same situation as babel helpers as mentioned.

@blainekasten
Copy link

However in our experience the size of React or ReactDOM is not the bottleneck—it's the application code

True, though it is semi-related to the way React application code is written. Trivial example here:

// React application code
import React from 'react';

class HideableDiv extends React.Component {
  state = {
    hidden: false,
  };
  
  toggleHide() {
    this.setState({hidden: !this.state.hidden});
  }

  render() {
    return (
      <div
        onClick={() => this.toggleHide()}
        style={{display: this.state.hidden ? 'none' : 'block'}}
      >Click To Hide Me!</div>
    );
  }
}

Now implemented in traditional JS:

const div = document.createElement('div');
div.innerHTML = 'Click to Hide Me!';
let hidden = false;

div.addEventListener('click', () => {
  hidden = !hidden;
  div.style.display = hidden;
});

The compared nature here is 17LOC to 7LOC. So the bottleneck may not be React or ReactDOM libraries, but may be the requirements of those libraries.

FWIW, I agree with almost ever sentiment here. Although I would be very interested to see this as a community project to gauge the viability of transforming stateful react application code into imperative runtime code while successfully source mapping back. If possible, that'd be pretty huge.

@gaearon
Copy link
Member

gaearon commented Mar 18, 2017

To be honest I find this particular example a bit of a straw man. This is not representative of most React components I worked with, which have more complex updates, lifecycles, and render child components depending on state.

As I mentioned earlier, you can find our thinking about this here: https://github.com/facebook/react/labels/Component%3A%20Optimizing%20Compiler. For example, "component folding" would inline trivial components into bigger components, reducing the output size.

@developit
Copy link

Having looked into svelte et al a little bit, I think the complexity comes in child and composition diffing, not attribute/prop diffing. Diffing a large list of components based on keys via AOT would produce fairly verbose output and likely not optimize as well.

@trueadm
Copy link
Member

trueadm commented Mar 18, 2017

@developit I was under the impression that @Rich-Harris added in shared helpers into Svelte to help deal with those cases?

@trueadm
Copy link
Member

trueadm commented Mar 18, 2017

I don't think AOT compilation to vanilla JS DOM operations is the ideal. More, transforming components and vdom into OP codes that are put into shared array buffers that can be operated on at a WASM level as well as JS. I've been hacking on some ideas around this actually and it looks promising.

@KyleAMathews
Copy link

@trueadm
Copy link
Member

trueadm commented Mar 18, 2017

@KyleAMathews very similar in some aspects, except the tricky parts is building a JSX+JS+Flow compiler that built these opcodes rather than working with Handlebars templates. From the opcodes you could then use OCaml, Rust or C++ to work with them and do all the diffing/hard work. Lifecycle events, refs and other user escape hatches would mean switching from WASM to JS to deal with them.

@developit
Copy link

@trueadm seems like the opcode approach you outlined avoids having to jump between JS and WASM entirely, which is nice. Component implementation stays in JS but the actual diffing is done in a separately optimized layer?

@trueadm
Copy link
Member

trueadm commented Mar 18, 2017

@developit Yep, there is no VDOM, it's just low level instructions that can better interop.

@scottmas
Copy link
Author

@trueadm, this approach is ages off though, right? Seems like in the near future of React, compiler optimizations of application code will be the order of the day.

@trueadm
Copy link
Member

trueadm commented Mar 20, 2017

@scottmas It's far easier to AOT compile templates, that's why all AOT examples have so far been from template based libraries/frameworks. React components with JSX aren't that constrained, so it makes it far harder to do that work at compile time. This is all definitely stuff for the future.

@Rich-Harris
Copy link

@trueadm thanks for @-ing me, now I know why @gaearon was tweeting about this! Since AOT is a bit buzzwordy and has some misconceptions around it, probably worth addressing a few things as I (creator of Svelte, for those of you who don't know me) see them.

Firstly, AOT doesn't necessarily mean 'having no runtime', it just means that some work is moved from the browser to the compiler. Whether or not you describe a particular framework as having a 'runtime' largely depends on your semantics. When Svelte talks about not having a runtime, it just means that your components get compiled to code that manipulates the DOM directly, rather than creating a data structure that requires an additional step (be it some form of key-value observation, or virtual DOM reconciliation) before the UI can reflect the app state. This is partly about making smaller apps, but it's also partly about performance — it's just less work for the browser to do.

Secondly, I don't see it as a binary thing — I think there are degrees of AOT. I would certainly consider babel-react-optimize to be a form of AOT. (The reason you won't find the AOT label on the Svelte website is because I don't want people to think Svelte is doing the same thing as Angular — it's not! Angular is just turning string templates into an intermediate representation, which other frameworks have been doing for years, rather than actual code.)

Thirdly, there's a perception that AOT doesn't scale, which is to say that the incremental cost of components is higher and will overtake the initial savings once your app reaches a certain size. That's theoretically true, and we don't know where that threshold is yet (and it's different for e.g. React and Preact). AOT frameworks can (and Svelte does) deduplicate shared helpers. But the key thing to note is that it's the initial cost that hurts the most, when the user is impatiently waiting for the app to become interactive. Today you can't visit Twitter without someone hectoring you about code-splitting, but if any of your entry points have a dependency on a framework, that's an immovable cost that you can't code-split away. Selectively including the bits of framework a given entry point needs — aka AOT — is the only way to eliminate it.

Fourthly (sorry, I'm rambling a bit!), debugging is nicer than you might imagine, at least in Svelte's case — decent sourcemap support, and when things do go wrong you're stepping through a short stack trace (and the generated code is pretty readable). And we have dev mode warnings. It's an important point though, and always something to improve.

Anyway, the tl;dr is that AOT encompasses lots of different ideas, and I'm excited to see how they impact the React ecosystem.

I don't think AOT compilation to vanilla JS DOM operations is the ideal. More, transforming components and vdom into OP codes that are put into shared array buffers that can be operated on at a WASM level as well as JS. I've been hacking on some ideas around this actually and it looks promising

Hope you can share something soon!

@sebmarkbage
Copy link
Contributor

Well said, @Rich-Harris.

TBH, the reason I haven't really gotten into this discussion is that I think it is a typical pattern of any new technology focus that we'll quickly get past. 1) We see a misconception about what it actually is. 2) We see a misconception of how magical it will be. 3) We see a misconception how hard it will be to build. 4) We see a misconception how hard it will be to debug. Always solvable.

I'm just excited to get into the details and having many people exploring this space. Particularly how this can be made to work with abortable scheduling, across component levels and what heuristic with regard to inlining vs. reusability ends up being the most efficient. How we can make whole program compilation or small subset compilation parallelizable and scalable so we don't hit cliffs when large companies start using (cough, Swift). Let's talk about those things instead.

It is clearly better to have some for AOT compilation if you can, and debugging is solvable.

FWIW, we have an internal compiler project at FB that I'm hoping will set the foundation that we can build on top. There's a lot of infra to be built for us to be able to actually take advantage of this stuff at scale.

It is also unclear to me whether compiling to JS is the best step. I think custom byte code with an interpreter runtime can be more efficient after a certain application scale - which wouldn't strictly be pure application code.

If you don't mind, I will close out this issue since I don't think that it will completely describe the goal here. Then I'll reopen another "umbrella" issue which can encompass a project with various incremental steps we need to take advantage of more advanced AOT compilation.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests