The obvious one is that you’re introducing an “invisible” layer to your code-base in order to simplify implementation. This isn’t all that bad if that’s your intention, but it can make changing implementation more difficult down the road.
The other, less obvious one, is poor first load performance.
ERV uses @babel/register to first compile the React tree before converting it to a string to be served as the response. Instead of pre-compiling this code however, it uses babel’s require hook in order to compile it on-the-fly. This is great for simplicity. All you need to do in order to implement it is require it in your code. It will then compile anything that comes afterwards. The down side is that because it’s done on-the-fly, the compilation doesn’t occur until that particular file is loaded into memory. This causes a significant delay in execution time while the source is compiled.
When it comes to continuous delivery, this is definitely a problem!
But what can we do about this?
Well, we actually considered a couple of options...
At Indigo, we recognize that Docker is the future! Any new application that we create has to be run in a container. Due to this, deploying changes to production is the simple task of spinning up new containers to replace the existing ones. We could just add an extra step to our deployment process where we, either manually or in some automated fashion, make a request to the endpoint to force compilation to occur. After compilation, the resulting bundle is stored in memory and referenced on each subsequent request, resulting in a much faster load time.
Unfortunately, although we’re diligently working towards it, our continuous delivery process isn’t currently at a state where we are able to do this. Additionally, this solution doesn’t scale well. If we’re deploying 10 instances of a container, we don’t want to have to do this for each one.
The other option, the one that we decided was right for us, is to pre-compile the React tree. This involves running a build tool such as webpack, before starting the server to create a JS bundle. Then reference the compiled bundle in ERV instead of the source code. We chose this option for a couple of reasons. First off, it scales well. The code changes will apply to any new container that we spin up. And second, we already use webpack for creating our client-side bundle. This means that a lot of the work is already done!
Fortunately, webpack allows for multiple configurations by exporting an array. We just need to go from something like this…
To something like this…
For the server configuration, don’t forget to set
target = 'node' and
output.libraryTarget = 'commonjs2'. The second one is especially important because it adds a
module.exports to the bundle itself so that it can be “required” elsewhere. ERV needs to require the bundle in order to serve it up as HTML.
Executing webpack with the second configuration above will result in 2 bundles. 1 for the client, which is output to our public JS folder
Of course, you could create a completely separate webpack configuration file and scripts which run webpack with this alternate configuration. The example above just allowed us to create a common object and re-use it for both the server and and client configs.
Configuring Express React Views
There’s a few changes we need to make here, but they’re all pretty straight-forward.
- Update your ‘views’ path to the server bundle
- Update our view engine suffix to match the bundle file generated by webpack (‘js’).
- Set the transformViews flag to
falseand remove any babel configuration (if any) from our ERV configuration.
Depending on how you’ve customized your configuration, there may be other changes that you’ll need to make here, but the 3 above will apply to most.
…and that’s it!
Pretty easy, huh?
When compared to our initial first load times, we reduced our time to first paint by more than 33 seconds in development and our time for the bundle to be served by more than 7 seconds in production. That’s quite dramatic!
For us, the main reason for implementing ERV in the first place was a low config option for SSR. Now that we’ve got webpack handling the server bundle, we really don’t need ERV anymore. The next logical step for us is to remove it entirely and replace it with a straight-forward implementation of ReactDOMServer.renderToString() or ReactDOMServer.renderToNodeStream().
The only reason that we don’t do this right away, is that currently all of our HTML, even that which is static, is written in React and compiled to HTML with ERV. The static, non-React portion of the code-base will require some refactoring in order to remove ERV entirely.