loading...

Maybe I Should Have Just Used create-react-app

harri_etty profile image Harriet ・4 min read

I've recently been working on improving the performance of a personal side project I've been working on, Conju-gator.com, a little app for practicing verb conjugations in Spanish.

The app is built in React with webpack as the bundling tool, the static files are served from Amazon's S3 with CloudFront content delivery network in front. When developing with React it is common to end up bundling all your source files and dependencies into one single JavaScript file called a bundle. The amount of JavaScript you serve is known to be a probable cause of performance issues, since the bundle can grow quickly as you pull in more and more dependencies.

Originally I didn't give much thought to performance as it seemed like a tiny project, with very few source files and not many dependencies, and I thought performance would be something that I wouldn't need to worry about until later.

However, the site does rely on a fairly large quantity of verb data to produce the training questions, which initially was a JSON file that I imported and bundled along with the source code, and thus could potentially cause performance problems at some point.

I decided to run Chrome's Lighthouse performance audit (a brilliant tool) and get a benchmark for how my site was doing and to my horror it scored 0% on the audit!

Chrome's Lighthouse auditing tools

What I was doing wrong

The audit highlighted a few key areas where I could make improvements:

  • Code was not minified
  • JavaScript payload was excessive
  • Non-essential CSS was not being deferred
  • Files were not served with an efficient cache policy
  • Files were not zipped with gzip or equivalent before serving

The final two points were issues that I needed to fix at the S3/CloudFront level since they are server settings. The solution involved adding metadata to the objects I uploaded to S3 to ensure they were served with a max-age Cache Control header, and that they could be served gzipped. With these two fixes my audit improved about 50%.

The issue of non-essential CSS being loaded too early when it could be deferred I ended up solving with Google Web Font Loader although I also came across other approaches to loading async CSS which may also have been useful. The CSS changes didn't make a big difference in the audit.

Webpack Improvements

The first two issues, however, are the ones I want to talk about as they have to do with bundle size. I was serving a 3000kb JavaScript bundle, and when you think that the recommended size is < 250kb, you can see how off the mark I was.

Firstly, my code was not minified, which was an easy mistake to fix as there is a webpack plugin that'll do the job for you, or if you're using webpack in production mode then minification comes by default!

That's another issue - I wasn't using production mode when building my bundle for production. A single line: mode: "production" in my webpack configuration solved so many problems - it brought the bundle size down considerably by only including the parts of libraries that were needed for production, and also gave me minification for free. Webpack's guide to bundling for production is extremely clear and helpful and I wish I'd read it earlier!

Off the back of more research, I also decided to remove source mapping in production (the webpack guide suggests to keep it, for debugging purposes, but to use a lightweight version). Source mapping maintains a map from your bundled code to your original source files so that line numbers & file names in the console refer to your original files and not the bundle. However I wanted to cut down my bundle as much as possible so removed it completely and will bring it back if needed.

By using Webpack Bundle Analyser I was able to watch as my bundle size decreased, and see where its size was coming from.

Analyzing the bundle size

When the analyzer showed me that my node_modules were now taking up a reasonable amount of space compared to my source code, and my whole bundle size in production was under 250kb, I was pretty happy.

Finally, I decided to remove the verb data from the bundle and fetch it asynchronously, although I'd already got to about 98% on the audit at this point and although it reduced my bundle size further it didn't give me any Lighthouse performance audit improvements.

My final performance audit score

 Reflections

Looking back, the changes I made were quite straightforward and I feel foolish for not realising how bloated my bundle was in the first place. However, at the time it took me some solid hours of work to work through all my problems, and figure out the best solutions.

At one point, I thought "I wish I'd just used create-react-app in the first place!" since CRA will provide default webpack configurations which would have surely been optimised for production and included all the things I'd originally omitted, plus more.

However, the CRA webpack configuration is about 400+ lines long, which is one of the reasons I always shy away from using CRA in the first place. I like to know what my configuration is doing and be able to change it if I need to, and I've traditionally found the configuration of CRA apps hard to debug and maintain.

What are your thoughts? Do you prefer an opinionated/optimised configuration at the expense of customisability?

Have you had an experience of optimising performance on a single page React app?

Am I missing any more performance wins?

🙌

Posted on May 8 '19 by:

harri_etty profile

Harriet

@harri_etty

Software engineer, runner, yogi, language learner

Discussion

markdown guide
 

For small projects (or prototype or MVP) I would use CRA + react-snap + S3 (or netlify) + CloudFlare. I just don't want to spend time to configure webpack, eslint, postcss etc. I want to spend more time writing actual code.

 

Agree that time spent writing business logic > time spent writing config! But what happens when your project needs something changing? Or you want to do a major version upgrade of webpack, for example? Invest the time at that point to solve that problem when you get to it? I guess you can't plan for the future!

 

Or you want to do a major version upgrade of webpack, for example

Exactly the point of CRA. They upgraded from webpack 3 to webpack 4. I needed to bump only version of CRA, without changing anything else. Less than a minute of work. How much would you spend updating your handcrafted config?

Ah right! Thanks! So would you recommend not ejecting CRA in this case, or can this upgrade still be done if CRA is ejected? Would there be any other downsides to not ejecting CRA?

If you eject you are on your own, so they recommend to create separate git commit when you eject, so you can undo this. I had projects with CRA, I never needed to eject. With babel-macros need to eject is close to 0.

 

I think that in order to learn you need to go through all of these steps, after all if you just used create-react-app you wouldn't have realized what was going on under all those commands; but after you learn what happens, why it matters and how it works, doing it all by hand is just going to slow you down, specially when starting a project.

P.S.: Tu aplicación es genial :)

 

Thank you! I agree about needing to do the configuration yourself, at least a few times, to fully learn what it's doing. I feel sorry for people who learn React and begin with CRA - it's a great place to start but it's also overwhelming to see so much configuration and have no idea what's going on!

 

Well, once you create a webpack config you hardly need to change it between projects. This means the only thing you do is copy it when you start a project.

Aftr that it's much easier when you need to change something.

 

I love your Conju-gator app. Great for adults and adnvanced learners, but for kids or intermediate level learners, it’s too hard.

I would even pay a subscription for “kid-mode” if all the answers were multiple choice, then once they level-up enough, then send them to advanced levels where they have to spell it out.

 

Aw thank you! Yeah it's definitely the case of me building something which was precisely what I wanted and not really thinking about anyone else haha! Really appreciate your feedback, as I develop it I'll definitely be thinking about how I can make it more useful for other people too. That's a great idea about multiple choice questions!

 

Could you please share you config of how you got Webpack Bundle Analyser working with the latest versions of CRA?

I'm trying the command react-scripts build --stats but it's not generating the bundle-stats.json file needed for bunde analyser.

 

This feature was deprecated in CRA 3.0. The recommended way is to use source-map-explorer:
facebook.github.io/create-react-ap...

You may have to remove the single quotes if you are on windows:
npx source-map-explorer build/static/js/*.js

 

Yes, but I was hoping to get it working with the bundle analyzer as it has a better interface.

Thank you for confirming CRA 3.0 removed the bundle stats.

 

Hmm react-scripts is a create-react-app thing isn't it? In that case I'm not sure as I didn't use CRA and I had the option of adding the Webpack Bundle Analyser to my webpack configuration. Maybe this helps?

medium.com/@jbandi/you-can-use-cre...

 

I have a larger side project that I recently migrated from manual webpack to CRA and found it to be an overall win. My explicit configuration is now limited to eslint, everything else is free*.

What I would recommend is installing react-scripts in your project, running “react-scripts start” and if it works, just roll with it. I gradually migrated my tests to use it but had it managing my builds for a few weeks before I made that jump.

 

Thanks for the recommend of react-scripts, not really looked into this package before but will bear in mind for future CRA projects!

 

Sure thing! To clarify, CRA itself is an executable dependency that sets up a project, dependencies, and initial commit. React-scripts is what you wind up with when it’s done.

 

maybe you should have just used gatsby and netlify

 

hahaha yes maybe! Love gatsby and netlify but my app doesn't really have any content, just some verb data it needs to fetch, so a static site generator was probably overkill here!

 

your reflections seem to be just a part of the natural process of experiencing Webpack. But it's absolutely NOT recommended to go with CRA as-is for developing projects to be deployed. According to Facebook crew itself, CRA should only be used for prototyping and testing, as their Webpack configuration is extremely shallow and optimized only for development purposes.

Knowing Webpack is mandatory if you wish to achieve performace levels such as the ones mentioned above. However, nowadays it's better to start off with CRA as there are libs that allow to extend CRA's config "on the fly", and that would be the best option to use when creating new projects with CRA and that are intended to go live.

 

Hi, I'm one of the current maintainers for create-react-app (we don't work at Facebook), and I want to point out that CRA is absolutely production ready. All of us have real world apps built using CRA's webpack configs. One exception is when you need SSR in production then CRA might not be the best tool to use out of the box, but SSR could be supported in a future version (there's already a PR out for it).

 

Totally agree with your thoughts on CRA. While I like that it abstracts away build tooling, I think that the moment you have to eject you pay for it big time.

My preferred approach nowadays would be to initially start with something like Parcel and if a use case for something like Webpack surfaces, then configuring Webpack manually so that I know what goes into my Webpack config and how to maintain it.

 

Interesting, would love to take a look at Parcel!

 

That's the reason I love using create-react-app it takes care of the little things so you can focus on the bigger picture

 

There are a lot of situations where ejecting a CRA app is not enough.
It's a great tool, but just if you need a small app, which probably will be small in a final bundle.
If you go deeply within a webpack configuration and you use some tasks runner like ( Gulp or others ) you can achieve exceptional results.

 

I used ejected CRA on a substantial work project because we wanted to use postcss plugins and spread operators. We at some point switched to emotion and CRA built in some of the Babel niceties we wanted. Then in the last 6 months I unejected it. It’s really all you need.

I found everything I wanted to customize from CRA could be done with simple node scripts run after react-scripts build in yarn build. Like I didn’t like source maps. So I wrote a node script to remove them from /build.

 

You can turn off sourcemaps by setting GENERATE_SOURCEMAP=false yarn build facebook.github.io/create-react-ap...