About 1.5 hours
Here are links to lessons that should be completed before this lesson:
We learned about minification in our previous lesson. Turns out the create-react-app tooling is able to do this for us via it's build
process and we'll take a brief look at that.
As you may have seen already with your Eventonica app, remote API requests can be slow! We're going to apply the caching optimization technique we learned in the previous lesson to help speed things up. We will do this in a couple of different ways based depending on specific use cases and constraints.
While React is pretty fast even if we use it naively, it's important to be aware of some of the details of how React works in order for your UI to stay snappy and responsive even when it becomes deeply nested and complex. We will learn a few techniques to optimize our components and measure the difference using react specific dev tools.
Participants will be able to:
- Have a useful understanding of what create-react-app provides in terms of minification.
- Measure application latency and using browser dev tools.
- Select appropriate caching strategies based on different user workflows and infrastructure constraints.
- Use react dev tools to inspect react component state and performance.
- Optimize how often React components are rendered.
- What happens when you run
npm build
in a create-react-app project. - How to measure AJAX request latencies with browser dev tools.
- Caching can happen in different layers (e.g. on the browser or on the backend)
- How to install and use React dev tools.
- The
shouldComponentUpdate
lifecycle method andPureComponent
class - The React rendering reconciliation process.
Review these materials:
- Building with create-react-app (10 min read)
- Using the react dev tools profiler (10 min read)
- Avoiding unnecessary re-renders in React (just the 'Avoid Reconciliation' section - 5 min read)
- React Reconciliation (15 min read)
Building (includes minification)
You've already configured building for heroku deployment. Let's run a build on our local machine, check out what gets generated, and then use our Node.js backend to serve up a "production" build locally.
- Run the
npm run build
command from somewhere in your repository. - Take a look at the contents of the newly created
build
directory in the repository root directory, specifically what's inside thestatic
andstatic/js
directories. You can read more about these files in the linked page in the Materials section. - You can modify your backend to serve up these built assets by adding these two lines:
// Serve any static files
app.use(express.static(path.join(__dirname, "rel/path/to/build-dir")));
// Handle React routing, return all requests to React app
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "rel/path/to/build-dir", "index.html"));
});
}
Keep in mind that you need to modify the rel/path/to/build-dir
to the actual relative path to your build directory--something like: ../client/build
.
- Once you've configured your express server to serve up your built assets, go ahead and open up your browser to
http://localhost:5000
(or whatever port your express app is using), and make sure that it works just like you expect.
Adding Eventful Search Caching to Your App
We're going to be adding a component to your app, along with a separate server-side search API app that allows us to search the Eventful API. The code for both can be found in this gist. Click the "Download ZIP" button on the gist page, unzip the contents, and then place the EventfulSearch.js next to all your other component files, and the eventful-api-server.js in your API repository folder. You should be able to run it with the following command:
$ EVENTFUL_API_KEY=<YOUR_KEY_HERE> node eventful-api-server.js
Import and place the <EventfulSearch />
somewhere in one of your app components that makes sense. You should now be able to see that component in your app. Don't worry about styling for now. You can add that later :)
Reload your Eventonica React app with the browser dev tools open. Notice the delay when searching for events, and use the network tab to see how long the AJAX requests are actually taking. Would be nice if we could cache results so that repeat searches are fast...
Let's optimize this for two different use cases:
-
We have very limited server-side resources, and so we don't want to cache the search results there. Could we cache the results on the browser? If so, how could we make sure that this doesn't consume too much of the browser user's memory? In what ways is this better than caching on the backend server? In what ways is this worse?
-
We have plenty of server-side resources, and we can cache results there. Let's use a server-side cache implementation that looks very similar to part 1 and just store our cache in the node application's memory. In the real world with an app that's serving millions of users, this wouldn't be a good strategy since we'll probably have multiple node processes ("web dynos" in Heroku) serving our app, and it would be wasteful for each process to have a separate copy of the cache. Instead, we would use a separate memory store service (like memcached or redis) that each of the processes would have access to.
Optimizing React
By default a React component will re-render whenever it receives new props or if setState method is called with any argument. This is usually what you want, but if your component always renders the same thing with the same props and state (and it probably should) then that means that whenever its parent component re-renders, then the child will re-render too, even if its props and state are the same.
There's a component lifecyle method called shouldComponentUpdate(nextProps, nextState)
that decides if a component will re-render. Its default implementation always returns true
. We could implement this method ourselves, and make it so that it checks whether the nextProps
and nextState
are the same as the originals, and returns false
if that's the case; however, React provides a component class PureComponent
that does this automatically for us. This means that we can just replace the Component
part of our component class definitions with PureComponent
, and we get all the benefits of props and state checking!
However, we must take into account that the shouldComponent
update method of PureComponent
does only shallow object comparison. This means that we need to be sure that we never mutate objects that we're using in our state or props because otherwise the changed objects will be considered to be the same, and our component won't re-render when it's supposed to. Instead, we need to create new objects whenever we call setState or pass new props. The React documentation has a great explanation of this here.
The other technique you may have already used when optimizing React rendering performance is the use of the key
attribute when generating a collection of sibling elements based on an array of data:
<ul>
{ widgets.map(w => <li key={w.id}>{w.name}</li>) }
</ul>
React uses the key
attribute to keep track of which sibling elements stay the same, so that it doesn't have to destroy and re-insert all the DOM nodes that haven't changed. When creating a series of React elements, be sure to use a key
that stays stable between re-orders, insertions, or deletions from the list. A good candidate for this is a unique ID that is associated with each of objects that are being used to generate the elements. A thorough discussion of this subject can be found in the React documentation link referenced in the Materials section.
Now let's put these React optimization techniques into practice!
- Install React Dev Tools in your browser.
- Reload your app with the tools installed, open the normal dev tools, and go to the "React" tab (all the way to the right).
- Once the "React" tab is open, select the a "Profiler" tab within that.
- Click the gray circular record button on the left, and this will begin profiling the render actions in your app.
- Perform a single action like creating, editing or deleting one of your events, and then click the orange "stop" button in the dev tools. A flamegraph will appear with all the component renders and their associated time cost.
- With a delete, create, or update of an event, only one
<Event />
element is really getting changed, the rest should stay the same. Compare that to how many (re-)renders are you seeing in your flame graph. Use the techniques we learned above to try and make it so that your app only renders the modified/deteled/created element.