adamhaile / surplus

High performance JSX web views for S.js applications
639 stars 26 forks source link

template tag function? #74

Open trusktr opened 6 years ago

trusktr commented 6 years ago

Is it possible to do something similar to eval(compile(\...`))` but with a template tag function?

For example, instead of

const div = <div {...props} />;

we could write

const div = s`<div {...${props}} />`;

(or similar) and both would compile to

const div = (() => {
    var __ = document.createElement("div");
    Surplus.spread(__, props);
    return __;
})();

Note how in the example I left the ... in the string, as I imagine Surplus needs to detect things like spread and function call parens to make it work properly. F.e.

const div = <div className={className()}/>;

becomes

const div = s`<div className={${className}()}/>`;

and notice the () is in the string.

But maybe the ${} syntax is too complicated in some cases? F.e. imagine writing this example as a template string:

const view =                     // declarative main view
    <div>
        <h2>Minimalist ToDos in Surplus</h2>
        <input type="text" fn={data(newTitle)}/>
        <a onClick={addTodo}> + </a>
        {todos.map(todo =>       // insert todo views
            <div>
                <input type="checkbox" fn={data(todo.done)}/>
                <input type="text" fn={data(todo.title)}/>
                <a onClick={() => todos.remove(todo)}>&times;</a>
            </div>
        )}
    </div>;

What's nice about template strings is that various editors now have support for syntax highlighting inside template strings, so if we could at least change

eval(compile(`...`))

to

eval(compile`...`) // without parens on the compile call

then we'd have support for syntax highlight. compile could still take a single string, just like in the codepen examples, and docs would tell us not to use ${} interpolation.


Another posibility could be to remove eval, and let us pass parameters in, maybe something like this:

const view = s`
    <div>
        <h2>Minimalist ToDos in Surplus</h2>
        <input type="text" fn={data(newTitle)}/>
        <a onClick={addTodo}> + </a>
        {todos.map(todo =>       // insert todo views
            <div>
                <input type="checkbox" fn={data(todo.done)}/>
                <input type="text" fn={data(todo.title)}/>
                <a onClick={() => todos.remove(todo)}>&times;</a>
            </div>
        )}
    </div>
    ${ { data, newTitle, addTodo, todos } }
`;

Notice it is taking advantage of object shorthand to make props that match the names of identifiers inside the string. Eval could then be used internally. Notice I didn't pass in todo, as the compiler be able to infer this from the map callback arg, plus on the outside I don't have a reference to any single todo.


Maybe

eval(s`...`)

is the simplest way to do it, a tag function that expects us not to use ${} interpolation.

trusktr commented 6 years ago

Thinking back on it, it would be possible that instead of

const div = s`<div {...${props}} />`;

we could write

const div = s`<div {${props}} />`;

but I think this might require changes to the parser/compiler, so it spreads an object without needing to detect spread notation.

For reactive calls, maybe instead of

const div = s`<div className={${className}()}/>`;

we could write

const div = s`<div className={${() => className()}}/>`;

where className() is an S.js var, which means then Surplus could run the function and detect whether or not it is reactive. If not reactive, it will just use that value once the first time. If reactive, it will know to call the function repeatedly.

So in the compiled output, that part might become something like:

          if (typeof templateArgs[whateverIndexItHappensToBe] === 'function') {
            S(() => {
                // re-runs if className() changes
                __.className = templateArgs[whateverIndexItHappensToBe]();
            });
          }

With some clever detection, you could prevent the use of S() if the function is not reactive, or if the value is not a function.

trusktr commented 6 years ago

These sorts of changes could eliminate the need for eval(), which would reduce the compile time from compile+eval to just compile!

adamhaile commented 6 years ago

Sorry for the slow reply. I somehow missed a batch of issues coming in and am just seeing them now :(

One small note, which you might know already: Surplus apps don't usually use eval at runtime. Usually all the codegen happens at compile time, via one of the build tool plugins. The codepen example uses eval only b/c CodePen doesn't support preprocessing with the Surplus compiler, so we have to do the codegen at runtime and pass the output to eval.

But other than that, your bigger issue, yeah, I spent some time thinking about an alternative strategy that used string literals, like lit-html does. I even forked lit-html and whacked in some support for S as a POC.

The big advantage of the string literal approach is that it doesn't require any preprocessing. We can do all compilation at runtime, so no build tool required. That definitely makes it easier to start with Surplus.

The disadvantages, and the reason I ended up sticking with current strategy, were four fold:

  1. Doing the compilation at runtime means our users pay the cost of the compilation, i.e. it delays render. It also limits the extent of optimizations that can be done, else the compilation takes too long. Surplus generally subscribes to Tom Dale's school of thought that compilers are the new frameworks. We should do as much as we can at development time, even at a small cost in developer experience, to improve user experience.

  2. The Surplus compiler currently wraps the code you put in {...} expressions in a function, so that it can detect if it reads any signals and do fine-grained updates if the signals change. This means you can write code like <div className={myClassName()} />: the read of myClassName() will be wrapped, detected, and re-run if it ever changes. String literals wouldn't let us do this wrapping -- we only get the result of the expression, not the source to wrap -- so you'd have to do it manually, like s`<div className={() => myClassName()} />`. If you forgot the extra () => wrapping, then a change to myClassName() wouldn't just update the div.className property, it would re-create the entire div (and maybe more). Basically, an ahead-of-time compiler lets Surplus be fine-grained by default, while string literals would be coarse-grained by default, fine-grained only if you remember to pass any values that might change as functions.

  3. The subset of JSX Surplus uses has pretty good support from other tooling, like editors and typecheckers. This means that we can do things like typecheck that you didn't typo <div classname={...}/> or somesuch. This more than makes up for the DX cost of needing to set up a build tool plugin in my experience. String literals would lose this support.

  4. For small apps, ones too small to use a build tool, there's surplus-toys. Come to think of it, I should probably convert the CodePen demo to use it.

All that said, it's an interesting enough idea that I still come back and think about it from time to time. Lit-html manages its runtime binding without too much overhead. There's a new experimental project out there called domc that's doing runtime compilation and has some exciting benchmark times. Still, 2) and 3) above keep me with dev-time compilation right now.

So that's my current thinking. Who knows what the future holds :)

Thanks for the thoughts, and sorry again for the slow reply.

trusktr commented 6 years ago

Interesting thoughts. In number 2, instead of writing myClassName(), in the template string one would just write myClassName and Surplus would run it.

s`<div className=${myDynamicClassName} />`

Would that work?

In light of the new Stefan Krause benchmark release, I noticed this new thing (speaking of using Babel): https://github.com/ryansolid/babel-plugin-jsx-dom-expressions. Looks cool!

asprionj commented 6 years ago

Concerning babel-plugin-jsx-dom-expressions, and the related Solid.js (https://github.com/ryansolid/solid): What's the difference to Surplus.js (except that the compiler is a Babel plugin)? Solid.js even uses S.js, but what does it provide in addition to S.js and the JSX compiler? I'm confused...

ryansolid commented 6 years ago

Sorry for the confusion. This is my fault a bit. In my perspective asking this here is like going on React's github and asking why Preact exists. Surplus and S are Adam's projects. They are both amazing and my appreciation for them has only grown over time.

Babel Plugin JSX Dom Expressions only was created because as primarily Knockout developer I saw Surplus and wanted to do the same thing with Knockout or any other fine grained library I used. I suspected what he did was generalizeable so I worked away at it. It was in my opinion so much better than the templating work I had been doing the past couple years I dove right into it. As of today the API and output is different in about a handful of different ways. There is a different way to do custom directives, the way the spread operator works differs, the computation context on node bindings is different, Surplus uses createElement, and Babel Plugin uses cloneNode. But it will likely never be as optimized for S as Surplus is as that is Surplus' intention. I had to not do all the same automatic optimizations to support other non-S libraries. More so Surplus is much more mature as I've sacrificed things like SVG support as a focus to work through other things.

Similar Solid was born from my desire to try a fine grained library with a different type of API. I wanted to take the best parts of React and RxJS implement it using fine grained change detection library (ones that use Computational context to do change detection). Originally I had written my own change detection library, but came to the inevitable conclusion S was just better than anything I was doing, or would likely ever do, and arguably better than anything else anyone else had written. So the use of S in Solid was simple because it was the best.

So what does Solid provide in addition. Nothing completely necessary. Solid is about an API and the removal deoptimizations to support Proxies not in Surplus currently. Basically there is a cost to the abstraction in Solid so I have to work harder to get similar performance in it, and it comes with it's own caveats. If I was just sold on S and Surplus as is I'd probably not look at Solid. I've been working to reduce the differences so perhaps Solid could use Surplus instead of Babel Plugin JSX Dom Expressions, but truthfully I think that plugin has it's own reason for existence outside of S.

I'd love to hear your thoughts on how I can reduce this confusion. But I don't believe the discussion belongs here.

asprionj commented 6 years ago

Wow, thanks a lot for the detailed explanation. Didn't want to accuse anyone of anything. It's just that, every time I see two very similar (or seemingly even identical) programs/packages/implementations, I want to understand why both exist. I'm not the one to judge whether a common solution would be preferable, but in these times of increasing "JS fatigue", partly due to the vast amount of new frameworks / libraries (and their possible combinations) appearing at a weekly pace, this could be a good thing IMHO.

trusktr commented 6 years ago

Nice thoughts by Ryan. There isn't one way of doing things, and by having an alternative we can all learn how to make something even better.

Arguably though, with the existence of the Stefan Krause benchmarks and libraries like these, at some point we're going to reach the maximum potential of optimization, and will be limited only by the progress of the browsers, with the guiding force being which DSL are most awesome to work with.

Handlebars' use if innerHTML was slow. Angular and React (and quite a few of others) showed that vdom was faster. Thanks to JSX, now it seems we have a way to compile declarative views into something faster than vdom-based libs. How much better can it get now?

What I guess is a custom element lib is gonna win over React, Angular, etc, at some point. It's just a matter of time.

Qix- commented 5 years ago

TLDR to the above: Solid is kind of like Surplus's functionality with a React-like API, right?

Anyway, to the original post, I'm :-1: on template literals for Surplus. The parsing overhead for such a minimal syntax enhancement (I'd argue it's worse syntax, even) isn't worth the runtime overhead IMO.

Stick to a compiler, I'd say.

ryansolid commented 5 years ago

@Qix, regarding Solid pretty much. Some difference in philosophy around rendering. But mostly I was sick of seeing Fine Grained approaches being treated like they couldn't adequately solve certain types of problems easily. React moving to Hooks which are eerily similar to S.js or Knockout just sealed the deal.

Yeah I went and did this with my libraries writing Tagged Template Literals (and a HyperScript runtime as well), and the syntax is definitely worse. You have to be so conscious of when you are wrapping accessors or not. I will say this though. The performance overhead on Tagged Template Literals is much less than you'd think. With fine-grained you only run out the compiled code on instantiation and then it falls back on your reactive graph. In so while there is a hit for that runtime compilation, outside of initial page load benchmarks (which are way too popular these days) you might not even notice the difference from the JSX.

I see the desire which is why I entertained requests like this, but JSX is sooo much better for fine grained approaches like this. All these hopes of shorter syntax like avoiding wrapping by passing straight Signals in don't really work in practice. What if you want to bind a function to a DOM element. I had to come up with silly conventions to address that. I think this is just the tradeoff. The price you pay for Fine Grained. Precompilation is big part of what makes these sort of solutions attractive.

Qix- commented 5 years ago

React moving to Hooks which are eerily similar to S.js or Knockout just sealed the deal.

It was a bit sad that the react team has been touting hooks as if they're some revolutionary new concept nobody has thought about, when S.js has been around for much longer and still works much better, both performance wise as well as ergonomically.

ryansolid commented 5 years ago

I will give them this. They managed to mostly mimic fine grained computations and composition in a top down rerender everything environment. As someone who has been similarly playing the put the square peg in the round hole game from the other side it's commendable. By showing others this is possible has produced an approach that can work for almost every DOM renderer as the vast majority are top down rerender. LitHTML or Incremental DOM with Hooks? No problem.

But, fine grained libraries Computations handle Hooks so much better. That's where things get interesting. Both approaches have never been so close. But I was surprised they didn't reference any of the Fine Grained libraries in prior art.

Qix- commented 5 years ago

Yeah :/ The few devs I know that have been trying to get started with hooks-only projects have been running into issues that clocks solve, for example.