reactjs/react-future

React compiling down to raw application code

scottmas opened this issue Β· 24 comments

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.

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.

@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?

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.

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

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.

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.

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?

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.

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.

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.

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.

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.

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.

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

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 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.

@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?

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

@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.

@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.

@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!

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.