Skip to content

App-level compilation #749

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

Closed
Rich-Harris opened this issue Aug 4, 2017 · 1 comment
Closed

App-level compilation #749

Rich-Harris opened this issue Aug 4, 2017 · 1 comment

Comments

@Rich-Harris
Copy link
Member

This isn't exactly a feature idea or proposal, more just some general thoughts that I've been meaning to get written down.

At the moment, the compiler can only operate on a single file at a time. This has the virtue of simplicity, but we're leaving some low-hanging fruit on the tree. (Well, maybe not that low-hanging. But certainly ripe and tasty.)

Take an example like this:

<!-- App.html -->
<Widget foo='bar'/>
<Widget foo='baz'/>

<script>
  import Widget from './Widget.html';

  export default {
    components: { Widget }
  };
</script>

<!-- Widget.html -->
<p>foo is {{foo}}</p>

Without us needing to specify that {{foo}} is static (see #364 for a relevant proposal), we can see that it won't ever change. So the generated code could be a bit simpler:

function create_main_fragment ( state, component ) {
  var p, text, text_1_value, text_1;

  return {
    create: function () {
      p = createElement( 'p' );
      text = createText( "foo is " );
      text_1 = createText( text_1_value = state.foo );
    },

    mount: function ( target, anchor ) {
      insertNode( p, target, anchor );
      appendNode( text, p );
      appendNode( text_1, p );
    },

-   update: function ( changed, state ) {
-     if ( text_1_value !== ( text_1_value = state.foo ) ) {
-       text_1.data = text_1_value;
-     }
-   },
+   update: noop,

    unmount: function () {
      detachNode( p );
    },

    destroy: noop
  };
}

If the app template only had one instance of <Widget>...

<Widget foo='bar'/>

...it could even become this:

function create_main_fragment ( state, component ) {
-  var p, text, text_1_value, text_1;
+  var p, text;

  return {
    create: function () {
      p = createElement( 'p' );
-      text = createText( "foo is " );
-      text_1 = createText( text_1_value = state.foo );
+      text = createText( "foo is bar" );
    },

    mount: function ( target, anchor ) {
      insertNode( p, target, anchor );
      appendNode( text, p );
-      appendNode( text_1, p );
    },

-   update: function ( changed, state ) {
-     if ( text_1_value !== ( text_1_value = state.foo ) ) {
-       text_1.data = text_1_value;
-     }
-   },
+   update: noop,

    unmount: function () {
      detachNode( p );
    },

    destroy: noop
  };
}

Those savings (in bytes, and update performance) could add up quite nicely over an entire app. But we could go much further. In this situation we know that that the Widget doesn't need observers, event handlers and so on. In fact, we could probably do away with the component constructor altogether, and just treat <Widget> as a fragment. (We would bail out of that optimisation if we saw <Widget ref:whatever>, or event handlers that we weren't sure about, or anything else that meant someone could get a reference to the component.)

Bindings could become much simpler in this world. Code like this...

<!-- App.html -->
<Widget bind:foo/>

<script>
  import Widget from './Widget.html';
	
  export default {
    components: {
      Widget
    }
};
</script>

<!-- Widget.html -->
<input bind:value='foo'>

...results is code like this:

// App.js
function create_main_fragment ( state, component ) {
  var widget_updating = false;

  var widget_initial_data = {};
  if ( 'foo' in state ) widget_initial_data.foo = state.foo;
  var widget = new Widget({
    _root: component._root,
    data: widget_initial_data
  });

  function observer ( value ) {
    if ( widget_updating ) return;
    widget_updating = true;
    component.set({ foo: value });
    widget_updating = false;
  }

  widget.observe( 'foo', observer, { init: false });

  // ...
}

// Widget.js
function create_main_fragment ( state, component ) {
  var input, input_updating = false;

  function input_input_handler () {
    input_updating = true;
    component.set({ foo: input.value });
    input_updating = false;
  }

  // ...
}

In other words, changing the input value causes input_input_handler to run, which causes the <Widget> component's set method to be called, which dispatches observers, which includes the observer function in App.js, which causes the <App> component's set method to be called, which updates the root-level state object and causes a top-down update (by-passing <Widget> because it's already updating).

That's okay, but it would be much nicer if input_input_handler could bypass the observer mechanism altogether and directly set data on the <App> component. That's possible, but only really if we consider the entire application in one go.

I'm not entirely sure what that would look like in practice. Presumably, the compiler would still operate on individual files, but would be passed information about the current component's environment and needed features. Gathering that information would be a little tricky — we can see how App.html is using Widget.html before it gets compiled (e.g. rollup-plugin-svelte can figure this out, as long as the appropriate component metadata is exposed by the compiler when App.html is compiled), but if we later discovered that a different part of the app was using Widget.html in a different way, it would be too late — compilation would have already happened. So we'd effectively need to trace component definitions ourselves, which could be tricky and probably leads to all sorts of wacky edge cases.

But worth it, if we can figure out how.

@Rich-Harris
Copy link
Member Author

Just realised this is basically the same as #1102 — closing in favour of that issue

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

No branches or pull requests

2 participants