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

Implement custom async primitive #17

Merged
merged 14 commits into from Feb 8, 2017
Merged

Implement custom async primitive #17

merged 14 commits into from Feb 8, 2017

Conversation

divmain
Copy link
Contributor

@divmain divmain commented Feb 8, 2017

This PR removes reliance on Most.js streams in favor of a custom Sequence implementation.

I also experimented with a generator-based approach (see here). This drastically improved readability, but damaged performance by 25-40% compared to the original. In comparison, this PR makes things somewhat less readable :( but improves performance by another ~50% over the original implementation on concurrent renders and also closes #7.

This also brings a breaking change to the public API - but, seeing as basically nobody is using this yet, that seems a non-issue. And I'd say that the new API is just as nice as the original.

API changes are reflected in the README.

/cc @kenwheeler @aweary

@mhaagens, would also like your eyes on this since you're the first user!

Copy link
Member

@ryan-roemer ryan-roemer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code quality and idioms look great!

I'll leave it to @kenwheeler and @aweary for the substantive correctness details.

It would be awesome if we could get jsdoc-style comments on the major functions and classes (maybe a future ticket?) as I'm not deeply versed in this stuff and having a bit of difficulty just following the literal code. (Maybe it's just me).

Otherwise, just a couple of minor nits!

README.md Outdated

This function translates [Most.js](https://github.com/cujojs/most) streams to Node streams. You'll be able to pipe the output of these Node streams to an HTTP Response object or to disk.
This function evalues a React VirtualDOM Element, and returns a Node stream. This stream will emit string segments of HTML as the DOM tree is asynchronously traversed and evaluated.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: s/evalues/evaluates/

README.md Outdated

However, you may want to change this number if your server is under heavy load. Possible values are the set of all positive integers. Lower numbers will be "more asynchronous" (shorter periods between I/O processing) and higher numbers will be "more synchronous" (higher performance).
Note that the template composes both a stream of HTML text (`componentHtmlStream`) and a function that evaluates to the store's state - something you'll often want to do with SSR.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: I think it reads better as:

  • "the template is composed of" OR
  • "the template comprises"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

package.json Outdated
"test": "npm run lint && npm run test-only",
"test-only": "mocha spec/exec.js",
"check": "npm run lint && npm run test",
"test": "mocha spec/exec.js",
"lint": "eslint ./src ./spec",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GLOBAL NIT: If you remove ./ prefixes, the npm tasks will be windows compatible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really... well that's useful info. Will do!

@divmain divmain mentioned this pull request Feb 8, 2017
1 task
Copy link
Contributor

@aweary aweary left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a few comments! As far as the renderer goes it looks pretty good to me. The streaming/caching modules were a little hard to follow so I'm not entirely sure I can provide any substantial feedback on those yet.

@@ -12,16 +15,38 @@ const alternateColor = text => {
return _color(text);
};

const time = (description, fn) => {

const time = (description, fn, baseTimeOrFn) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth using an existing benchmark library like benchmark.js instead of rolling our own.

import { time } from "./_util";


// Make sure React is in production mode.
process.env.NODE_ENV = "production";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% sure if it matters, but we probably want to set this before we require in React/ReactDOMServer. Maybe just add it to the npm script?

 "benchmark": "NODE_ENV=production babel-node benchmark/benchmark.js"

Copy link
Contributor Author

@divmain divmain Feb 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renderToString performance was considerably different after I made this changed, compared to before. I'm not entirely sure what do do with the benchmark/ dir - its not all benchmarks, its really just a collection of manual tests that have helped me along... But since there is a script entry for it in package.json, that seems like a reasonable change to make!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it seemed like it made the appropriate change, but I figure setting the ENV variable before anything executes would future-proof it in case there were any future changes to React (or any consumed module in benchmark) that implemented conditional exports based on process.env.NODE_ENV, which isn't unheard of.

"lint": "eslint ./src ./spec",
"check": "npm run lint && npm run test",
"test": "mocha spec/exec.js",
"lint": "eslint src spec",
"benchmark": "babel-node benchmark/benchmark.js",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

babel-node might be skewing the results of the benchmark. It's not meant for production as it does a lot of in-memory caching, and that might affect the results.

If we remove the ESM imports/exports and just use React.createElement instead of JSX I think we can run it with node directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can double-check to make sure, but I think this is okay as-is. babel-node will transpile the benchmark.js source and all of its dependencies when node originally starts. That shouldn't effect benchmark times (since Node is compiling the transpiled bytecode by that point), and the extra memory usage shouldn't be an issue either.

I'll switch the benchmark over to use React.createElement just to be sure - if the numbers don't change, I'll leave it as-is.


function toPromise (gen, batchSize) {
const arrayBuffer = [];
return new Promise(resolve =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How performant are native promises in Node these days? Could using bluebird instead potentially increase performance?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... good question. Since this is a simple promise, the performance difference is going to be negligible. bluebird really shines when composing lots of promises, relying on them for everything async (like in Interlock). So... probably fine? I could make the change to the benchmark, see if it makes a difference.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#25

}
}

return attrString.join("");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered using string concatenation instead of arrays? I benchmarked it and string concatenation seems to be much faster: https://esbench.com/bench/589b83a999634800a03477e7

I have a renderer I was working on and found string concat. was much faster in Node as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really... I thought the opposite was true! In that case, I'd be very interested in switching it out. Thanks for the tip!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow! That's a crazy difference. I'll make this change for sure!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's implementation specific and string concat IIRC is going to be better than arrays in all modern JS / browser engines.

Potentially worth testing on all node's that we intend to support...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#27

function evalComponent (seq, node, context) {
const componentContext = getContext(node.type, context);
// eslint-disable-next-line new-cap
const instance = new node.type(node.props, componentContext);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this throw if the function is not a constructor, e.g., an arrow function? You can easily determine if a React component should be constructed by checking type.prototype.isReactComponent.

I think doing that check would remove the need for isNullComponent too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting... I haven't run into a circumstance where it throws, but I was completely unaware of isReactComponent. That might even perform better...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@divmain have you tested with SFCs written using arrow functions? Since arrow functions dont have the internal [[Construct]] method it should throw if you try to new it.

Copy link
Contributor Author

@divmain divmain Feb 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the SFC returns a React element, the behavior is identical to cases where the function was invoked directly (Type() vs new Type()). The only exception to this is isNullComponent, which is why that check is there. But if there's a simple boolean check that avoids some of that rigmarole, all the better!

Copy link
Contributor

@aweary aweary Feb 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if the SFC is defined using an arrow function then doing new Type is an illegal operation as arrow functions cannot be constructed. I think the tests/benchmarks might be missing this because babel is compiling arrow functions to regular ones.

Check out this example: https://jsfiddle.net/ydpcmo57/

var SFC = () => React.createElement('div')
var element = React.createElement(SFC)
// this throws 
new element.type()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow! You're absolutely right.

return;
}

if (isString(node)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should treat numbers as text as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#26

src/renderer.js Outdated


class Renderer {
constructor (jsx, sequence) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it's not really JSX being passed here as its just a syntax extension and we're passing the result of the JSX call. It might be better to call it element or something.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, agreed.

@divmain
Copy link
Contributor Author

divmain commented Feb 8, 2017

Thanks @aweary! I may make some of the changes you recommended in a subsequent PR, since none of these are blocking and I have a couple of other PRs stacked up on top of this one; #19 and #20.

The Sequence code might be easier to follow once the JSDoc PR is merged.

@divmain divmain merged commit 74cdb27 into master Feb 8, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Determine how to handle backpressure for streaming interface
3 participants