ByteSize: App Optimization with Flask + React

Tips and tricks to get to sub-100ms responses

Spencer Porter
7 min readNov 26, 2020

This is the last in a series of articles where we have been putting together a fully functional infinite scroll component using React + Flask, with some help from Material-UI and the Unsplash API. You can check out the previous articles below, or if you just want the code you can find the repository here.

  1. Setup and Infinite Feed
  2. Infinite Grid and Styling
  3. Dynamic Search

In this article we’re going to be talking about optimization.

In my own personal experience, latency is a massive part of a user’s experience on your application. I could go on about the number of studies that have shown that even a few seconds can mean the difference between someone checking out your site or closing the tab and moving on.

With that in mind, there are some relatively simple tips and tricks I’ve found that can make a huge difference in putting together production ready apps for deployment.

The Road So Far

Coming from our last tutorial, our component is almost perfect. Users get a beautiful infinite grid photo grid, as well as the ability to search for specific photos with natural language. There are just a few things to do before this component is ready for production.

Right now, the latency for downloading the component is relatively small, on the order for a few hundred milliseconds. Once you start building more and more elaborate applications with multiple components, those extra milliseconds can mean everything.

Opening up our network tab, we can see that our app makes quite a few requests before it’s rendered. Breaking them down into groups:

  1. The React bundle
  2. The API request
  3. The Unsplash Images

We can use a few different strategies to minimize the size and latency of those requests and mitigate their impression on the end user.

  1. Caching
  2. Reducing Media Size
  3. Smoke and Mirrors
  4. Bundle Splitting

Caching

Very basically, caching works by storing the results of a function or endpoint locally, and serving them in subsequent requests. Since we know that the images are unlikely to change drastically from one request to the next, we can use caching to speed up our service.

Setting up a cache is also incredibly simple. First we install the package with:

pip install flask-caching

Next we will create a new Python file for our extensions to import it in the application initialization (in app/extensions.py)

Then we import and initialize it in our create_app function. (in app/__init__.py)

Next, we need to add the cache type in our config file (in config.py)

Then we can use the extension as a decorator on our view (in app/main/views.py)

The decorator works by automatically storing the results of the last request for 60 seconds as a key:value pair (like a Python dictionary). This stops us from having to request the same data every time from the Unsplash API. Opening Postman, we can see that currently the request takes ~200ms.

~200ms response time

On each subsequent request (within 60 seconds of the last), we can see that the response time drops down to ~8ms.

~8ms response time

Huge.

If you go to run the app now though, you’ll quickly discover a problem. You get the same page over and over again. That’s because the cache decorator doesn’t distinguish between arguments made in each request. To do that we are going to have to make our own custom cache key.

To understand this problem we have to do a little more background on caching. I mentioned before that the cache acts as a key:value store, but the decorator simply takes the route name, not the argument string that is contained in the request. Because of that we need to create a new (non-generic) key to reference in the request to make sure we get the proper response. The code above works by concatenating a string including both the path and the items of the request argument to make a unique key for the response value.

Caveat Emptor: Be aware that caching will be dramatically less useful by the amount that your responses change. If each request and response is slightly different (like lat/long coordinates) the less helpful caching will be as a strategy

We can now implement this new cache key in the cache decorator by using the key_prefix argument (in app/main/views.py)

And with that, we have caching built into our app!

Media Sizes

The biggest problem that we have is with our images. This stems from two issues:

  1. The image files are large.
  2. We have to wait for the images to load.

A key change we can make right away is changing the size of the images that we’re returning. Simply by adjusting our list comprehension to extract the ‘small’ image size instead of ‘regular’ from the Unsplash response, we can reduce their size by more than half.

Smoke and Mirrors

Even with the reduced size though, the images will take time to download and render. Rather than having to wait for them to though, we can play a little slight of hand to give our users something to look at while they wait (milliseconds) for their pictures. It may seem silly, but those milliseconds make all the difference in making your site appear more professional.

We can use what are known as blur hashes to generate a low resolution approximation of the underlying images.

This means that we can have a picture in place in under 10ms, rather than waiting until the image is fully downloaded to show something to the user. Unsplash provides the blur hash of each image as part of its response, but you can generate your own using the blur hash algorithm.

Our first step is adjusting our views endpoint to return the blur hash.

Now that they are returned with the response, we’ll need to set them up to be read on the React App.

First up is installing the blur-hash package.

npm install blurhash react-blurhash

Now we will import them at the top of our InfiniteScroll file (in app/static/src/InfiniteScroll.jsx)

While the idea of blurhashes is simple, implementing them in this context will take a little bit of thought and planning. We want to make sure that we only show the blurhash for as long as it takes for the image to load, at which the image replaces it.

Img tags provide an onLoad event which we can use in order to toggle a loaded state which will then hide the blurhash to reveal the image (in app/static/src/InfiniteScroll.jsx)

The sizing of the width and height in the BlurHash element has a bit of hackiness to it, as the GridList component handles multiple elements somewhat strangely and doesn’t size them correctly, so there was a bit of trial and error in finding the correct height and width for the BlurHash component. But with that code, our images are now ready.

Bundle Splitting and Compression

Our last step is taking a look at our bundle and static assets that we’re serving to our users. Right now we are currently passing the entirety of our node modules to the user, resulting in bundle over 5mb. Seeing as we are only rendering a few elements, 5mb is pretty hefty.

Webpack

Webpack does a great job of providing optimizations with a tiny amount of code.

We’ll need to make sure that we import the new split bundle within a script tag in the index.html file. By adding defer to the script tags as well, we can make sure that the loading of both are non-blocking and can be done asynchronously.

The bundle splitting and optimizations done by webpack actually reduce the combined bundles to only ~346kb, a pretty far cry from over 5mb.

The best part?

We’re not done yet.

We can still compress them more on the backend using Flask-Compress. Flask Compress compresses all files in transit, including our JSON responses. We can install it with:

pip install flask-compress

Next we import it in our extensions file (in app/extensions.py)

Then we initialize it (in app/__init__.py)

and finally we add a few basic Config variables specifying the compression level, minimum size and MIME types we want to be compressed (in config.py)

Finished!

With that, we have drastically reduced the loading time and size of our application, by around 50–100x. Don’t believe me? Just take a look:

All of our requests are significantly under 100ms each, and our largest file (1.bundle.js) is sitting at just over 100kb.

Our infinite scroll is now ready for production!

As always, thank you for reading, and I hope this will help you in whatever exciting new project you’re working on.

Also, be sure to follow as next we’ll be working on creating a nifty link preview component!

--

--