When I visit your web app for the first time, what code will I download? That question is pretty broad, so let me try to be a bit more specific.
Say I’m visiting the most popular entry point of your app — perhaps a login, signup, or landing page — which JS libraries, such as React
or lodash
, will I load? What custom first-party code, such as UI components, routing, and data fetching, will be required to build the page? Finally, which third party scripts, such as Google Analytics or Intercom, will be fetched?
If you’re not quite sure of the answer, you’re not alone.
While popular modern JavaScript bundling tools, like webpack, are powerful ways to package vendor libraries and custom first-party code, the output they create can be a bit mysterious. When we build our JavaScript, we all hope that optimizations like tree-shaking and code-splitting are working optimally to reduce the amount of JavaScript we send to the client, but it can be tough to know for sure.
The situation can get even more confusing when it comes to third-party scripts, such as those that execute tracking and analytics. Sometimes these scripts are added to the page via a tag manager, which means that frontend developers often don’t have much control over, or awareness of, which third party scripts are being loaded on the page.
I’m going to show you a small tool that I built called bundle-wizard
to make it effortless to answer the question of precisely what JavaScript is actually making its way into your users’ hands. But before we do that, let’s take a moment to review why exactly it’s important to know what JavaScript is being loaded in the first place.
JavaScript is a costly asset for a few reasons. While it’s pretty obvious that a large JavaScript file will take longer for the browser to download than a small one, a large JavaScript file is also potentially more expensive for the browser to process (parse and compile) than a corresponding image of a similar size.
Once the JavaScript is downloaded and compiled, the browser has to execute it. There are two potential user experience issues to look for here. First, unless you’ve server-rendered your app, the user will see a blank white screen or loading view for a potentially long time as your app works to build a page from scratch.
But even if you’ve had the foresight to pre-render the page you send down to the client, your JavaScript will still have to be downloaded, parsed, and executed before the user can interact with the page. If any of the JavaScript takes a long time to run—creating so-called long-tasks—your users could be faced with a completely non-responsive app that does not register clicks or keyboard input on desktop, or taps on mobile, for as long as it takes for their browsers to complete the JavaScript start-up work.
Many developers use powerful laptops on high-speed internet connections, and their apps tend to load quite fast on their development machines, obscuring performance issues. But for users on mobile phones dealing with occasional connectivity issues, especially those on lower-end devices with less-powerful CPUs, excess JavaScript can pose a real problem.
Now that I’ve hopefully established how important it is to be aware of the code you’re sending to your users, let’s look at how bundle-wizard can help.
By running the command npx bundle-wizard [site name]
, you can generate an interactive visualization that allows you to explore the JavaScript loaded by any entry point to your production site. As an example, running npx bundle-wizard reddit.com
creates the following visualization of the code loaded to display Reddit’s mobile site:
(You can check out a live version of this visualization here created from the Reddit mobile site as it was on 03/31/20).
Let’s take a quick look at how to interpret this view. First, we can see two very large bundles along with some smaller ones. The large bundles are divided between Mweb.b4e4245f311b33152097.js
on the left, which contains much of the page’s custom code, and vendors~Mweb.9ef0d432dd704f4f0943.js
to the right, which contains JavaScript libraries like React and the polyfill library core-js
.
Before even getting into the other information that bundle-wizard
provides, we can begin to see some interesting avenues for further exploration. First, is it really necessary to load 84kb of core-js
polyfill even for users with modern browsers? Second, given that bundles should generally be less than 100kb for best performance, would it be possible to chunk these two big bundles down into multiple smaller ones?
If you click on a rectangle, for instance, the src/app/components
square inside Mweb.b4e4245f311b33152097.js
, you can see a detail view:
As you might expect for a message board site, the Post
component is the largest of all the components contained in the major bundles.
The background colors of all the boxes represent how much of the code was actually run by the page on startup:
One thing that’s helpful to remember as we scout for optimizations is to focus on the easy wins first. You might see, for instance, that the Register/index.js
is largely untouched on initial page load. This makes sense—registration is a thing most users will only need to do once. But since the component is only 13kb minified, it might not make sense to optimize right away. (One exception is if other code surrounding registration flow would push the combined size up. But from a preliminary look through the bundles, that doesn’t seem to be the case).
Bundle-wizard
provides the ability to toggle the visibility of all scripts, not just bundles with sourcemaps, by unchecking the “only show JS bundles with sourcemaps” option. This will help you compare the size of third-party scripts, such as analytics and tracking scripts, with the JavaScript module code that was bundled up and sent to the browser:
Once we render all scripts in the visualization, we can see that an ad script, https://securepubads.g.doubleclick.net
, takes over as the third-largest JS bundle loaded on the page. At only 17% coverage, it seems like this script is possibly loading a fair amount of unnecessary code. To be fair, however, Reddit seems to be loading a fairly small amount of third-party scripts compared to other sites.
When a user loads your site, their browser loads JavaScripts files in a certain order based on what it perceives to be the urgency of each request. Any bundle that is part of the critical request chain should have a high priority, while bundles containing code for initially unseen views, and third party scripts containing subsidiary functionality, should typically have a lower priority. If you visit the “Summary Tab” on the Reddit bundle-wizard example, you’ll see two lists of bundles—a “high priority” list that, as we would expect, shows mostly first-party bundled code, and a “low priority” list that is mostly third party scripts.
If we, on the contrary, found some critical path bundles that were in the low-priority list, it would be worth it to explore using priority hints to load them earlier. Conversely, we might come across some large or computationally expensive third-party scripts that had a high priority. In that case, we could experiment with adding a defer
attribute to the script tag to load it with a lower priority, or even removing them entirely if it was determined that their business benefit didn’t counteract the performance hit they caused.
So far, we’ve looked at optimizations that are mostly aimed to reduce download time and script parse time. We haven’t touched on script execution time yet, although that is potentially the most expensive step of all.
Helpfully, bundle-wizard warns you of long tasks it detected on app startup that were initiated by JS script execution In the Reddit example, we can see a little 🚨 icon next to the large Mweb.b4e4245f311b33152097.js
bundle. When we hover over it, we see just how long the long-task was in the tooltip:
Unfortunately, while it can alert us to the problem, bundle-wizard isn’t much help in helping us figure out how to fix it. For that, we can fire up the Chrome Devtools in an incognito window and run a performance profile on the reddit.com
mobile site.
As expected, the profile we create has a long task generated by an “Evaluate Script” action on Mweb.b4e4245f311b33152097.js
, which we can see when we select the yellow bar underneath the gray long task indicator and view the summary tab on the bottom:
To look into which functions might be contributing to this task time, we can take the following steps:
(Note: this step usually works best on a localhost development build, as it tends to be easier to jump right to the offending lines of code by clicking the links on the right-hand side).
You might notice other long tasks in the profile as well, not directly attributable to one of the JS bundles – those can be explored in a similar manner.
Please give bundle-wizard
a spin on your own projects and see what you learn! For full details of how to use the tool, including instructions on how to use it to measure apps running locally, check out the project’s README.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.