Last reviewed: Sept 2023
1. Introduction
If you've been following this post series closely you'll now have arrived at a point where the user interface for your Javascript/React/React-router webapp is delivered pretty much entirely through code that runs on your users' own hardware. Sure, somewhere on your Firebase server, Javascript functions and database CRUD operations may be active, but production processing load is largely handled by the cpu on the user's screen device. Users for the most part, are likely to be happy with this arrangement and, from your own personal point of view as a developer, your software is an absolute joy to work with. What's not to like?
Well, this is a high stake industry and there are still opportunities for large, complex application to make themselves more attractive by improving their performance.
Consider, the build
folder for a simple, static, React/React-router site. The image below shows the build/static/js
sub-folder for the index page to this podcast series.
The most interesting component here is the main.43ec45fd.js
file that contains the application's minified Javascript. At just 183KB, this is surprisingly small when you consider that it contains not only the the application's own code but also the react-dom/client
and react-router-dom
libraries. But you may easily imagine that in more complex cases, build
files like this will become very large indeed.
When your application runs for the first time this file will be downloaded. Subsequent references will retrieve the file from local cache and so will be a lot faster, but this initial access is the one that provides users with their initial impression of your system - not the best moment to slow things down.
Interesting asides:
If you're wondering how you're going to override this local cache when you update your application, observe the curious
main.43ec45fd.js
name of the minified javascript file. The43...
bit is generated from a hash of the content - change the content and the filename changes. The arrangement is completed by the code referencing the new filename that the React build will have placed in thebuild/index.html
file. When you come to run your app, local cache will be searched in vain for the new filename and the new copy will be downloaded. Neat!If you've been working with a default configuration for your project, you'll find that there are
.map
files in your directory as well as your minified.js
files. These are what makes it possible to debug your code in the Chrome inspector. If you now start to wonder whether the presence of these .map files is going to affect performance (not to mention making your precious code that bit more accessible to third-party prying), the answer is "yes". You can suppress the production of.map
files by creating a.env
file at the root of your project containing the following assignment :GENERATE_SOURCEMAP = false;
(but remember to change this next time you want to do some debugging!):
However, the network download element of the response time is only part of the story. A React application like this will also create quite a burden on the local cpu every time that it runs. This is because it has to negotiate with its React libraries in order to render its embedded JSX. Even though React is smart enough to streamline re-render operations this can be a considerable task.
While none of this matters for small projects like mine, it matters very much indeed for large, complex, commercial operations.
The bottom line is that it would be better for users (and for the health of the internet generally) if the server simply returned raw, hand-crafted html rather than complex instructions on how to generate it via Javascript and React.
But this would entail ditching much of your Javascript and React code and the productivity gains this delivers. Is there a way of obtaining better performance without making your life as a developer miserable again?
Welcome to the twin worlds of Next.js and Content Distribution Networks.
2. What is Next.js?
The Next.js website describes the software as a "framework system" but that doesn't tell you very much. Its probably easier to talk about what it does via a simple example.
Again, this series' index webapp is as good an example as any because it is a simple "static" site whose content is entirely defined by its own code. This of course makes it relatively easy to deliver pre-rendered html. Whenever I add a new entry to the index, I just add another lump of content. The webapp could quite easily have been written in pure html from the outset, but I've chosen to add Javascript and React and React-router structures simply because this made writing it quicker (and a lot more fun).
Next.js is software designed primarily to build projects on a remote server. Here it will take your webapp's Javascript and React code (notice I've dropped the reference to React-router - Next.js has its own arrangements for "paging") and produce the html code that you haven't the time to write yourself. It also introduces new language features (in particular, special image-handling arrangements) that you can deploy to ensure that this code is super-efficient and user-friendly without you having to arrange this yourself.
When the url that has been created by this procedure is referenced, the remote server now downloads the pre-rendered html code generated by the Next.js build. The theory is that, because this doesn't contain all those libraries and has minimised the size of embedded image files, its a lot smaller than it might otherwise have been. And when it runs, it doesn't burden the cpu with all those complicated "rendering" arrangements. The theory is that this should translate into increased user-satisfaction.
Of course a remote Next.js build procedure isn't going to be very convenient during the development process. You'll be relieved to hear that Next.js provides a local installation very similar to the one you've been using for React development. This enables you to launch a server that automatically refreshes a webapp page running in localhost
every time you amend and re-save a file in your project.
This post isn't intended to be a complete tutorial on how to work with Next.js - for this I strongly recommend you invest 30 minutes or so in looking at Next's excellent introduction at From React to Next.js. In fact, if you've got a bit more time and would appreciate a revision exercise, you might start with From JavaScript to React, which is effectively a fast-forward version of this whole series. But I'd like to elaborate here on the way that Next.js delivers the amazing image-handling facilities mentioned above - an area in which I think the platform really excels. These are quite confusing and, if you're not already an expert in this area, React's own documentation isn't too forgiving.
3. The Next.js <Image>
component
Most websites these days are dominated by image content. Image files are large and complex so quite a large percentage of a webapp's overall response time will be consumed here. It is vital that this aspect of the website code works as efficiently as possible.
The good old native html <img>
has served us well in the past, but you need to write a lot of code in order to apply it effectively . A degree of automation is indicated and Next.js achieves this by providing a new <Image>
component to generate efficient <img>
code during the build process. Here's a list of the automations that <Image>
provides:
- Tailored fixed-image files
- Tailored responsive-image files
- "Lazy loading" automation
Now that's a whole eyeful of hopeless jargon so let me elaborate:
Tailored fixed-image download files
Imagine that you've got to populate the product-description content of an e-store. Inevitably this will mean that your site contains a lot of graphics files. The e-store is only going to work efficiently if these files are sized optimally. For example, if product thumbnails are being displayed in a container that is 200 pixels wide, you'll ideally want them to be rendered from a graphic file with 200 pixels resolution. But here's the snag - the webapp may be required to display the image for any given product at all sorts of different resolutions in all sorts of differently sized containers. That implies that you're going to have to create lots of individually-tailored files, not to mention lots of individually-tailored code to deliver them. And then somebody will decide that the container widths need to be changed ....
Wouldn't it be nice if you could just create one file for each product with the highest resolution imaginable and have the software create and reference appropriate container files automatically from the webapp code.
Here's the code for a typical React.js <Image>
component:
<Image
src={'/thumbnails/giant-about.jpg'}
alt="North Gate"
width="200"
height="250"
layout="fixed"
/>
This requests the display of a graphics file called giant-about.jpg
stored in the public/thumbnails folder of a Next.js project. The <Image>
is to be rendered with a width of 200px
in Next.js's fixed
format.
As it happens, giant-about.jpg
is a 3024*4032 high-resolution file weighing in at nearly 5.8mB. If I'd coded the request with a conventional <img>
element, as follows:
<img src="/thumbnails/giant-about.jpg" style={{width: 200}}/>`
the Navigation tab in Chrome System Tools would have shown that the webapp incurs the cost of downloading the full 5.8mB file in order to render the 200px display
By contrast, however, here's the Navigation tab for the <Image>
version.
Weirdly, this shows that a file called image%2Furl=%2Fthumbnails%2Fgiant-about.jpj&w=256&q=75
containing just 28.3kB bytes was downloaded by the webapp in order to render a graphic. Equally strangely, the webapp seems to have received this in webP format (a modern format that demonstrates better compression). All of this is obviously a welcome improvement, but what is going on here?
It seems that the Next.js server has instructed the webapp to render the image via a parameterised url. One of these parameters is an optimally selected width. The server seems to have used this to either create a new file to service the download or retrieve from cache one that it created earlier. The provision of the compact webP format is just part of the service!
This is a huge improvement on the <img>
result, but things aren't quite perfect though. You'll note however that the url parameter is specifying a width of 256px rather than the 200px that would optimally be used here.
This takes a bit of explaining. The "parameterised url" mechanism that Next.js is deploying here has really been developed for responsive design arrangements that we'll see in just a moment. Here it will use a set of responsive breakpoints
(see Post 6.2) to create a srcset
of optional image download files, leaving the browser to decide which of these is appropriate for the device that's actually being used. For fixed
<Image>
components. These breakpoints are defined in two arrays: deviceSizes
and imageSizes
. You can specify values for these explicitly by creating entries in the project's next.config.js
file or you can leave Next.js to use its default values which are:
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
It seems that rather than generating a download file specific to the container that's being targeted, the default action is to choose from these predefined sizes - a coarse one to cover larger container sizes and a finer one for smaller ones in order to restrict the number of files that might build up in cache.
In the above example, since I'd chosen to use the default settings, Next.js has looked at the 200px width of my <Image>
containers and has chosen the smallest imageSize that will service this - in this case 256.
If it had been really important to maximise performance and receive 200px files from Next.js, all I'd need to do would be to add a value of 200 to the defaultimageSize
array and add this to next.config.js
as shown below:
const nextConfig = {
images: {
imageSizes: [16, 32, 48, 64, 96, 128, 200, 256, 384],
},
}
The point is that, one way or another, Next.js will always do its best to serve an appropriate file for an <Image>
component without any effort on your own part.
If you want to check any of this out yourself, please note that you need to restart the Next.js
development server every time you change the config file. Also, when you're working with Windows, you'll find that observed results will only match theory when you remove any scaling from your display settings. The <Image>
component does work on scaled displays but the generated code is a bit more complicated and the browser's actions aren't too easily understood. I suggest you use the Chrome Inspector to view the generated <img>
code.
Tailored responsive-image files
Getting Next.js to create responsively code - ie to serve images at resolutions appropriate to the dimensions of the device on which they are to be rendered - is equally straightforward.
If you were trying to do this with an <img>
element you would provide your own srcset
of optimally-sized files for the breakpoints defining target device widths. For the giant-about.jpg example, this might look something like the following:
<img srcset="giant-about-400w.jpg 400w,
giant-about-800w.jpg 800w"
sizes="(max-width: 600px) 400px, 800px"
src="giant-about-800w.jpg"
alt="North Gate">
Here, if the viewport width was less than 600 pix the browser would load a giant-about-400w.jpg
file; if not, it would load the giant-about-800w.jpg
version.
It must be immediately clear that if you wish to support a wide range of different device widths using <img>
you're going to have to create quite a heap of code - not to mention the folder-full of the tediously sized files that you'll need to provide to support this.
Using <Image>
, however, all you need to write would be the following specification:
<Image
src={'/thumbnails/giant-about.jpg'}
alt="North Gate"
width="200"
height="250"
layout="responsive"
/>
If you were now to define breakpoints in your next.config.js
file, you would find that Next.js
builds appropriate <img>
parameters for you automatically.
For example, if your defined a set of breakpointsin your project's next.config.js
file, as follows:
const nextConfig = {
images: {
deviceSizes: [200, 400, 1200],
},
}
then the Chrome inspector on your webapp code would show you that Next.js had automatically generated a srcset as follows:
/_next/image?url=%2Fthumbnails%2Fgiant-about.jpg&w=200&q=75 200w,
/_next/image?url=%2Fthumbnails%2Fgiant-about.jpg&w=400&q=75 400w,
/_next/image?url=%2Fthumbnails%2Fgiant-about.jpg&w=1200&q=75 1200w"
Further, if you run the webapp and open the chrome inspector's network tab, you'll see that with the viewport less than 200 pix, the webapp is loading the "w=200" version of the file (with a size of 17.6kB). With the viewport between 200 and 400 pix it uses the "w=200" version, and at all other sizes it is using the "w=1200" version (with a size of 586kB). In other words, the Next.js component is serving responsive images without any effort on your part whatsoever. This is seriously powerful technology!
Note that when using a "responsive" layout, the "width" and "height" parameters (200/250 in this example) for the <Image>
component are there just to fix the aspect ratio of the image. If you want to set the width as a percentage of the <Image>
's container, say, you need to embed the <Image>
in a styled container. For example, :
<div style={{width: '25%', margin: "auto"}}>
would center a scaled version of an image embedded in the <div>
.
"Lazy loading" automation
Another effective way of optimising webapp performance is to ensure that the display of a long,scrollable display is not stalled while images below the current view-window are downloaded. The so-called "lazy-loading" approach downloads only those images which are actually going to be visible in the viewport.
In the past you might have accomplished this in raw Javascript by initially loading "place-holder" graphics and using complicated event handler techniques to monitor scrolling activity. When you're using Next.js however, you may be startled to find that <Image>
elements are lazy-loaded by default. You get this service automatically!!!
If other approaches are preferred, Next.js also provides options for creating placeholders and for giving selected images special priority. Techniques like this help you avoid "layout shift" problems in which unexpected jumps in the display can lead to accidental user error and distraction. Not only will this make your site unpopular but it will also lower your Google SEO score.
For a fuller description of the way that <Image>
works, see Konstantin Komelin's excellent account in Next.js image optimization techniques
5. Deploying a Next.js webapp - Content Distribution networks
As indicated earlier, the build for a production system needs a version of Next.js running on your remote server. While installing Next.js
in a local development project is a perfectly straightforward procedure, establishing remote arrangements is a different story. How might you organise such an arrangement on the Google Cloud platform? I'm afraid the answer seems to be "not easily". It is possible to contemplate configuring a Cloud function to run a Next.js server, but a moment's thought will make you see that this is going to involve you in some pretty advanced software engineering.
Next.js is an open source development led by Vercel, a company that specialises in the provision of commercial, networked storage. Their core service is Content Distribution - servers designed purely to return static files containing pre-rendered html. You won't be too surprised to find that deployment onto the Vercel platform is smoothly packaged. You need to do little more than open a Vercel account. Additionally, you'll find that the deployment procedure itself is a very interesting arrangement. Here's how it works.
With an eye to the fact that standard practice for professional developers will be to first commit a copy of the new deployment's source code to a Git repository, Vercel have arranged things so that this also becomes the launchpad for deploying your code and performing a production build.
If you follow the setup procedure described in Vercel's Starter Guide, you'll find that the Github repository for your VScode project is linked seamlessly to Vercel's system.
Once you've finished testing your code, all you have to do is commit your changes to Git. If you now open your repository you'll find that the bottom right-hand of the root page contains a new section headed Environments.
All being well there will be a Vercel icon here alongside an entry alongside a button labelled "Production". Click this and you should see the "Deployment History" page for your Vercel project and an entry at the top recording the addition of a new deployment for the code that you've just committed. Yes, it's as simple as that - commit your code and your live system is updated. Things work particularly smoothly when you've got your code in a VSCode project where integration with Git is built into the workbench.
Of course, in practice things may go wrong - for a start the Next.js build on Vercel may pick up coding issues that didn't affect the development build and so your Deployment History may contain new error messages. Additionally, it's unlikely that you will want a commit to automatically go into production until you've reviewed it. Check out Vercel's documentation for advice on how you achieve this.
Taking it all together however, once you've had a chance to play around with the systems and developed some mastery over the inevitable quirks and glitches I think you'll agree that this is some seriously impressive technology. Here we have a workflow pattern delivering a high-performance webapp literally at the click of a button!
You can try all this out for free at Vercel but I think you'll quickly find that their "pricing plans" are generally rather more ruthlessly commercial than Google's.
6. Conclusion
On reflection, it seems to me that React's <Image>
component offers developers a hugely attractive opportunity to deliver faster webapps without sacrificing their own productivity. Faster webapps are a good thing not just because they make your products more commercially attractive but also because they contribute to the overall health of the network. There is a social obligation here and React's elegant <Image>
concept will ensure that valuable bandwidth isn't squandered on unnecessarily large image downloads.
The problem is that delivering the <Image>
component requires a remote build. Currently it is not immediately clear where you might go to obtain this service. Vercel is the obvious choice from the point of view of image-handling, but many factors clearly also need to be weighted. For advice on alternatives to Vercel, you might find Ondrej Polesny's Whatβs the best place to host Next.js site? post helpful.
So it seems to me that this series has now reached the outer limits of the well-charted parts of the technology ocean. Beyond lie a perfect maelstrom of competing ideas and opportunities. Although Next.js offers many attractive features over and above those described here (for example it can also handle cases where code cannot be fully pre-rendered because the site isn't static) I think that, currently, only seriously high-stake, image-rich developments are likely to take you in this direction.
But who knows what the next few years may bring? Exciting, isn't it? I hope that this webapp series has given you enough basic understanding to face the future with some confidence. I'll be watching developments myself with the keenest interest!!
For a full index to the post series see the Waypoints index at ngatesystems.com.
Top comments (0)