I ran a few benchmarks on the server (modifying https://github.com/paulshen/react-bench to not use jsdom). The results were surprising, as the browserified react.js
was about 30% faster than the npm version, even with NODE_ENV=production
.
The performance ranking (test run time) was react.min.js
< react.js
< NODE_ENV=production node react
< node react
.
I suspect process.env
is not a regular js object but perhaps a getter and thus carries a penalty when you test for "production" !== process.env.NODE_ENV
everywhere.
Also the minified version might still perform best of all, as at least some time ago V8 used function source length (including comments) as a heuristic for function complexity / compilation time and thus affecting chances for optimization in some cases, but the effect might be negligible.
process.env
is indeed not a regular object, but defined from the native side, so that may be the culprit for server rendering regression:
https://github.com/joyent/node/blob/b922b5e/src/node.cc#L2326-L2335 and https://github.com/joyent/node/blob/b922b5e/src/node.cc#L2030-L2055
So this introduced a getter in quite time-sensitive places (around invariants etc) in your otherwise clean codebase. Can you reproduce the issue? /cc @petehunt @benjamn
Lately I have been simply using a wrapped module
var React;
if (process.env.NODE_ENV !== 'production') {
React = require('./react-with-addons-0.10.0.js');
} else {
React = require('./react-with-addons-0.10.0.min.js');
}
module.exports = React;
Works great. You could consider switching npm package to some such scheme if there are no downsides.
My first attempt was to just add var __DEV__ = process.env.NODE_ENV !== 'production';
to the top of every file, but unfortunately uglify isn't smart enough to do dead code removal then. I wonder if we should have vendor/constants.js essentially make two copies of the code -- one for dev, one for prod, so
function x() { return __DEV__; }
module.exports = x();
transforms into
if (process.env.NODE_ENV !== 'production') {
var x = function x() { return true; };
module.exports = x();
} else {
var x = function x() { return false; };
module.exports = x();
}
so we only pay the getter cost once (per module) at require time.
@benjamn Is doing this easy with recast? It's not obvious to me if this.replace
interacts well with cloning the AST.
I would love to see this get some attention. 30% improvement out of the box would be awesome. Is there a lot of work to do here?
I think eventually we will want people to do a build step for server rendering, i.e. webpack's --target node
mode. Especially if people start expressing static asset dependencies as require()
statements.
I'm using webpack and new webpack.DefinePlugin({'process.env.NODE_ENV': '"production"'})
was enough to solve it for me. Is there no such thing in browserify?
For browserify there's envify
transform which automatically activates for react
package. You just need to set NODE_ENV
before bundling:
NODE_ENV=production browserify app-which-requires-react.js
Thus browserify also can be used for bundling for server rendering.
Call me all crazy, just had a left field idea. Could the transformer that builds the npm module create 2 complete copies of react? one for dev and one for production?
In the the normal version of react var React = require('react')
could you there look at the NODE_ENV
and swap which module is returned? This way on server you only get the performance hit once;
That's not _too_ crazy. It's a bit annoying from a build process though. I'd love to know what's happening at a larger scale in the node community. It seems like we shouldn't be the first people to encounter this in a larger scale.
A couple other ideas…
What if we set global.__DEV__ = process.env.NODE_ENV
in our main module and leave __DEV__
in our code everywhere else. There's some obvious bad behavior there (setting globals). So we could do variants. Only set it when it's not already set. Change __DEV__
to something more unique at build time (__REACT__DEV__
) and then set that. If we switch to webpack for building the browser bundles, then the DefinePlugin
solution is super easy, and I'm sure there's a way to do this in browserify too.
Another idea: Have a RuntimeEnvironment
modules which memoizes process.env.NODE_ENV
. This is sounding like something node should do itself, but the code @plievone linked to uses ObjectTemplate (which if I'm understand a bit of V8 right doesn't do any caching because values can change). There would still be a lookup cost, but it would just be a property access, not a getter and not a call out to get current environment variables (getenv
). This one is trickier because it would mean more code and we won't do this internally. I'd prefer a different solution, even if it's more hacky.
If all you're using is React and nothing from react/lib/*
, then you could do something like @plievone mentioned above. We started shipping the browerified bundles in the npm package, so require('react/dist/react')
and require('react/dist/react.min')
will end up giving you the bundles which don't access process.env
.
I don't think we're going to do anything specifically for server rendering other than bundling the builds in dist
like @zpao mentioned, but we might change the npm package eventually to be prebuilt using closure or similar.
just curious why
if (process.env.NODE_ENV !== 'production') {
var x = function x() { return true; };
module.exports = x();
} else {
var x = function x() { return false; };
module.exports = x();
}
and not
module.exports = (process.env.NODE_ENV !== 'production');
?
It was just an example. If that was the actual code I'd do the direct assignment like you suggested.
ah, ok - thanks :)
any movement on this?
No movement, though you can already require the builds from react/dist as @zpao mentioned above.
@syranide since you mentioned Webpack, do you use Webpack for server side code?
If you're already using babel on your server-side code (say, via babel-node
or require('babel-register')
), you can add the inline env transformer to get around this problem.
I wonder why react just can't export from dist
for production env when react package ships with dist
.
Because then require('react/lib/whatever')
results in a different copy of code, which will have issues. As long as that is remotely supported (and it's currently how react-dom
and our addons packages work), we can't do that. we might do something like that in the future though.
@STRML true that it's not recommended to run babel on libraries? I've seen a few warnings about this. Have you tried the babel transform-inline-environment-variables plugin in production? Did it work?
@taitsmp Technically, you have to enable processing on node_modules (which is slow) to get this to work.
@zpao How do you feel about __DEV__
compiling not into "production" !== process.env.NODE_ENV
in non-dist builds, but rather require() some module that caches the env? This is just a guess, but I believe will optimize well (function call vs. getter (which doesn't optimize in v8) + call to getenv
).
I found the babel plugin in https://github.com/facebook/fbjs/blob/master/scripts/babel/dev-expression.js. I'll give it a try & profile.
@STRML thanks! However, I've seen a warning that babel/register
is not suitable for libraries. I've only used babel/register
on files in my app. I'm curious if people use it on node modules commonly in production.
@STRML The catch here is that people use envify to do dead-code elimination to remove non-__DEV__
blocks so changing our pattern here will force downstream changes.
Understood. I suppose it becomes a question of priorities unless we can think of something more creative.
Unfortunately with the introduction of react-dom, it's not possible to use react minified builds on the server anymore unless you use a packager or the __SECRET_DOM_SERVER_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
without duplicating 'react'
, due to the new halfway-shimmed packages that require('react')
directly.
var React = require('react/dist/react.min.js');
var ReactDOM = require('react-dom/dist/react.min.js');
should work, no?
Ah you're correct, since the deprecation notice will get taken out due to the production check.
Oh wait, you're right that react-dom does require react directly…
Hah got confused myself. You'd have to just require 'react/dist/react.min.js'
and pretend it was ReactDOM, knowing that the deprecation warnings would be removed. Doesn't seem portable to 0.15.
Initial bench results show a wider performance gap (last two runs) than the OP.
25000 iterations of Baseline (npm import): 4476ms
25000 iterations of dist/react, unminified, development: 3150ms
25000 iterations of Production (npm import): 1758ms
25000 iterations of Production (npm import, raw obj process.env): 1041ms
25000 iterations of Minified React (dist/react.min): 734ms
In bench 4, I replace process.env
with {NODE_ENV: 'production'}
, eliminating the getter.
I wonder if process.env
performance can just be improved in node core (by caching).
I've asked about that, but they're not interested because programs may be relying on catching ENV changes as it is possible to change the ENV of a running process.
I've been able to manage a great improvement in server-side rendering (about 100%) by hosting __DEV__
to the top of the file as a const
, so it is only evaluated once. The best part is, when uglifyJS sees a const, it replaces the member in a condition, leaving if ('production' !== 'production')
which is then removed entirely. So we maintain the same code size, no backcompat hacks, and better perf. Only issue is that we may need to then post-process replace the const
for a var
in case of use with Browserify with old browsers.
Can I ask why this was closed? This is by far the number one bottleneck for us on the server-side.
Having to change all code to require('react/dist/react.min')
, for example, is a leaky abstraction and very un-Node-like.
@mhart Technically you can fix the problem in your own code by doing process.env = JSON.parse(JSON.stringify(process.env))
, converting it from a live object that calls getenv
down a plain JS object. You'll get the same performance benefits, with the only downside that any code that was relying on live edits to the env (does anyone really do that?) will fail.
@STRML cool, good to know – neat idea!
Still, massively leaky abstraction right? Like, suddenly React pollutes your entire Node.js app
Yeah.
I've been working on https://github.com/facebook/fbjs/pull/86, but it requires a change in UglifyJS to allow marking var
statements with an annotation (e.g. /* @const */ var __DEV__ = process.env.NODE_ENV !== 'production'
) so it knows that it won't change and safe to eliminate dead code.
@STRML killer little hack btw – immediately increased our server throughput by 50%
I'd still love for this issue to be reopened, or tracked elsewhere (will https://github.com/facebook/fbjs/pull/86 improve things for all React code?)
Node.js should work on speeding it up as well (I believe they've made some improvements in 5.4.0), but having process.env.NODE_ENV
littered throughout hot code is just not ideal Node.js practice IMO – if not being able to hoist that up, cache it, memoize it, whatever, is prevented by build tools, then let's fix the build tools.
They're stuck. process.env
is meant to be live.
@STRML oh for sure it is – I completely agree with that – I don't think it should be cached by Node either, just the current implementation has (or had) room for improvement.
As I mention in that ticket, this is the main problem:
// This would work if it were a const
var NODE_ENV = process.env.NODE_ENV;
//...
if(NODE_ENV !== 'production') { // this will not be eliminated
// debug code
}
A /** @const */
annotation in UglifyJS will fix this.
That seems like a decent solution to me.
I'm personally not a fan of the use of a global __DEV__
everywhere in the React code – again, it feels really build-tool specific. I'd prefer it be imported from a config file or declared in each module or something similar. Principle of least surprise and all that.
@mhart I would be happy to do that, but it is important that we have a way to eliminate dev-only code in prod builds and that users of React from npm have a way to do the same. Obviously we can use whatever tools we want for our stack easily but for npm currently we suggest envify/webpack.DefinePlugin. Not inherently opposed to changing that but we'd need a good proposal and reason.
@spicyj yeah, I hear ya – there's a lot of code now in React that depends on that behaviour.
Not sure I fully understand your comment "for npm currently we suggest envify/webpack.DefinePlugin" – what do you mean "for npm"? React code is compiled before it's published to npm, right?
My general feeling is that it would be great if there were no FB/React-specific idioms in the code – so that it's treated as though, if v8 (or other engines) had all the ES2015/2016 features needed, then you could run the code on Node.js without even needing a compiler. Currently that's not really the case. (I'm sure you could do some sort of global.__DEV__ = false
trick or something before requiring any React modules, but still, that would be a polluting hack)
If there were a way such that __DEV__
was required, or declared, per module – and code elimination still worked as it does now for the dist
builds – would you be amenable to a PR along those lines?
I mean: if you use webpack or browserify in conjunction with react from npm, you should be able to eliminate React dev-only code from your prod builds. envify lets us do that easily in browserify as it copies the NODE_ENV from when you make your build, and webpack.DefinePlugin lets you configure your build to replace production.env.NODE_ENV
with a constant which then can get constant-folded and minified out. This use case is important.
If there were a way such that
__DEV__
was required, or declared, per module – and code elimination still worked as it does now for thedist
builds – would you be amenable to a PR along those lines?
Yes, if it works for the case of React devs using browserify/webpack too, not just our premade builds.
@spicyj as an aside, just looking through the code now, and aside from the global __DEV__
, I haven't seen anything that's _not_ supported out of the box in Node.js right now – is there a reason, aside from __DEV__
, that the non-dist
code is even compiled and transformed into a different structure before publishing to npm?
Well, it does set up the team to move to ES6 for src/
, which will be important in the near future.
I've submitted a PR to UglifyJS2 as you can see above. If it's accepted this will solve the problem.
@STRML We'd still need everyone using React to upgrade to that version of UglifyJS so I can't promise that we'll take it…
Just everyone using React in combination with webpack or browserify and not using the dist/
build.
Alternatively they could use the DefinePlugin to set __DEV__
to false to get the dead code elimination.
@spicyj just to clarify, do you mean "everyone using React who also currently uses UglifyJS with it"?
Just so I'm clear, there are a few users here that I can think of. Those, on the browser:
require('react/dist/...')
everywhere already, or as a global <script>
require('react')
to require('react/dist/...')
or window.React
So 1 and 4 are taken care of by the compilation in dist
. What's the main reason for even supporting 2 and 3? Is it because of things like require('react/lib/...')
?
2 and 3 are the most natural for many people. If you are using browserify then envify gets used automatically because of our config in package.json. Almost everyone minifies their code in prod.
It is true that we could recommend #4 instead for many cases, but it does fall apart in the case of requiring submodules. We don't support this for external users because we consider the modules private but the addons packages use this pattern. Various third-party projects (unsupported by us) also make use of this.
I think the only reason 2 and 3 are "most natural" is just because process.env
is used everywhere so you have to use something just to get it to even work out of the box. It would be equally easy to setup the config in package.json to use literalify
or browserify-shim
or whatever.
I use 4 whenever I can and it's no more complicated than 2 or 3 for normal usage – I guess it's just a pity that users are encouraged to "reach in" to react/lib/
for various addons.
Thanks for the clarification.
So it's those doing 3 who are expecting their current UglifyJS setup to eliminate any code using __DEV__
that you're concerned about?
I guess it's just a pity that users are encouraged to "reach in" to
react/lib/
for various addons.
To clarify: we recommend users require react-addons-transition-group
or similar, which (currently) reaches into react but that's an implementation detail.
So it's those doing 3 who are expecting their current UglifyJS setup to eliminate any code using
__DEV__
that you're concerned about?
Yes.
we recommend users require react-addons-transition-group or similar
Ah cool, good to know.
We have been bitten by this one too. Would be great if some note about this behavior was present on https://facebook.github.io/react/docs/environments.html , it's a large performance impact, and is hard to find this issue unless you have already done all the investigation work.
I would like to add that this issue also affects Electron applications.
Now the work arounds seem to be:
<script>
tag, it would only work in Electron and it isn't the node way, where one should be able to track every used object just seeing the requires
. It also makes it difficult to switch between dev/prod.process.env
with a regular object. Not good, as we are forced to overwrite node api just because we want to use React.Note that replace every require()
to react/dist/react.min.js
isn't a solution because react-dom
automatically requires from react
root, not from react/dist
.
Why is this issue closed?
It seems to me that React should be able to initialize its configuration using NODE_ENV
, then rely on its own objects, and optionally offer a React.setProduction(true/false)
function to change the value later.
Is there any reason why this wouldn't work?
Technically this could be fixed now as UglifyJS supports /* @const */
annotations, and it may be easier still as the team moves the build to Rollup in #7178.
In combination with Rollup, it would be simple to export a __DEV__
var from a constants file and have it effectively tagged by Uglify for dead code elimination in production mode.
Adding my two cents here, I was able to work around this by adding the following to the top of my server.js
if (process.env.NODE_ENV === 'production') {
require('react/dist/react.min.js');
require.cache[require.resolve('react')] = require.cache[require.resolve('react/dist/react.min.js')];
require('react-dom/dist/react-dom-server.min.js');
require.cache[require.resolve('react-dom/server')] = require.cache[require.resolve('react-dom/dist/react-dom-server.min.js')];
}
I had previously wrapped React using local modules file://...
as was suggested above, but this caused a lot of issues with npm shrinkwrap
. This seems to get the job done without too much overhead or complexity.
For future reference, the above snippet is broken per: https://github.com/facebook/react/issues/8788
Referencing https://github.com/facebook/fbjs/pull/86 which will fix this now that UglifyJS can properly eliminate dev code (even if it references a var
that never changes) with its new reduce_vars
option, which is a default.
See https://github.com/facebook/fbjs/pull/86#issuecomment-285204734 for more context.
Most helpful comment
@mhart Technically you can fix the problem in your own code by doing
process.env = JSON.parse(JSON.stringify(process.env))
, converting it from a live object that callsgetenv
down a plain JS object. You'll get the same performance benefits, with the only downside that any code that was relying on live edits to the env (does anyone really do that?) will fail.