Javascript performance and site speed is something everyone is concerned about. Your boss, your clients, everyone will ask you to make their online store faster.
As an engineer, you’ll take on the task, but will often find yourself in a code mess. The truth is that there is no silver bullet for site speed. Everything you try only gives you a fraction of a second of improvement. Finding that big improvement is like a treasure hunt.
There are many resources out there that will help guide you to a solution, but every ecommerce platform has their challenges. In this article, I want to propose an opinionated way to approach a site cleanup or refactor, starting with the unseemly: code organization. These six steps will help you refactor a Shopify site to improve performance for Shopify merchants.
1. Figure out the most important assets
Put yourself in the shoes of your client’s customers. What is the most important asset that you need to see in order to have a buying intention? Is it the ability to hit the buy button as soon as the page loads? Or is it the images showing what the products look like? In the majority of cases for ecommerce sites, it’s the images showcasing the merchant’s products that are most important—customers want to see what they’re buying.
This does change if the layout of the site is built with scripts, such as with single page apps, but for the purposes of this article, I will consider images the most important assets on a site.
You might also like: 5 Ways to Improve Store Loading Times with Minification.
2. Keep Javascript to pre-determined places
Before we strip away any unused code or start moving things around, we need to first define where Javascript should be in any given document, using best performance practices. If you haven’t before, I suggest reading the following resources:
If you’ve read these recommendations and are still lost about how to apply these recommendations to a Shopify site, follow along.
It’s important to define where Javascript should be, so that we can spot where scripts shouldn’t be. There are only three document areas that Javascript should exist:
- Before the
</head>
tag - After the
<body>
tag - Before the
</body>
tag
Since we have identified images to be the most important asset, most Javascript should exist just before the </body>
tag, unless you have very good reasons that it shouldn’t. I will go through the reasoning for each script section.
"Be vigilant with where the script is placed even if you weren’t the one who put it there."
Scripts allowed in the <head>
tag
Every third-party vendor is going to tell you that their script needs to be at the very top of the <head>
tag, but you can give them the Terminator stop hand. Scripts in the <head>
tag are render blocking. This means visitors will not be able to see your beautiful website until the browser finishes parsing the content in your <head>
tag.
So what scripts are allowed to be in the <head>
tag?
None.
The Shopify platform is a server-side framework using Liquid templating. The moment the source document is downloaded, there should be enough to render the most critical first render view, without any Javascript library needing to fill the gap (other than external CSS styles).
This doesn’t mean that no website should have scripts in the head
tag. Here are some examples where the scripts has legitimate reason to stay in the head:
- Single page apps. This is even better if the single page app is hydrated with server side render.
- Inline scripts that don’t trigger the downloading of external scripts, and performs time sensitive operations, such as:
<script type=”text/javascript”>
window.performance && window.performance.mark('head_start');
</script>
If there are scripts that do need to be in the head, most of these should be just before the end of the closing </head>
tag. This ensures the site is optimized for browser resource prioritization optimization.
One more thing. There are misconceptions around the usage of scripts with async
or defer
. A script containing async
tells the browser that this script can be executed out of order, which makes this script non-render blocking. This means async
scripts execute the code as soon as the script resource finishes downloading. Scripts with defer
pushes the execution of the Javascript to the end. However, both types of script declaration still impact the network bandwidth as downloaded resources, which we can leverage for performance to download the most important assets for first render.
Scripts allowed just after the <body>
tag
Scripts in this area of the document are not the worst, but not the best either. We are still aiming for that first render view. The more we can push the download of external scripts later, the better. If you have a hero image or a collection of the latest products that you would like your visitor to see, these should take priority over scripts that your visitors will never see.
Browsers are systematic at resource prioritization. With scripts, browsers read the source document in a top-down order. If you have external scripts before the hero images, the external scripts will have higher downloading priority than the hero images.
Scripts allowed just before the </body>
tag
All scripts should be here.
This includes analytics scripts. What good are your analytics if your visitor’s browser can’t even get to this point of the page?
You might also like: How Lazy Loading can Optimize Your Shopify Theme Images.
3. Create site benchmarks
This is important. Without a benchmark, it is next to impossible to tell if anything you do improves performance at all. We can argue that there are improvements that would make a huge difference, but without proper measurements to understand what is happening, you could be making amazing upgrades that no one notices because they didn’t move the metric your boss is looking at.
To measure correctly, we will be using performance marks. This is a native Javascript API that is supported by most browsers in the world. Performance marks need to be in place right where they are executed. Specifically, we will be measuring browser parse duration: the time it takes for the browser to parse your Javascript.
We will also make use of performance metrics that Chrome specifically has already collected for us: paint.
performance.getEntriesByType('paint');
Let’s place the performance marks at the proper place. First, we need to understand how to measure parse duration. To do this, place performance marks like the following example in theme.liquid
.
You should also turn network throttling to Fast 3G and Disable cache in ChromeDevTools.
This should output a ton of metrics in ChromeDevTools. We’ll focus on the parse metrics that we just implemented.
The time measurements here all resulted from high resolution timestamps in milliseconds. We can see that the browser took 2563ms (2.6 seconds on Fast 3G) to parse whatever is in the head tag. We can also see that the first paint doesn’t happen until the browser finishes parsing the head at 3522ms (startTime 959ms + duration 2563ms).
What does this mean? This means that your visitor waited through two and a half seconds (minus network time) of white screen (aka nothing) before seeing your site.
Goal 1: Reduce parsing duration in the head tag
Assuming that you cannot remove any scripts on the site, your goal should be shifting metrics to optimize for performance. The first paint metric is an important number to reduce for performance. However, what’s holding it back isn’t just what’s in the head tag—parts of the body tag are also responsible for it, since the browser needs to parse it too before it can be rendered. So, let’s also place performance markers within the body tag, like so:
Once this is added, we should see the following:
We can see from here that first paint is sometime after parsing the head, but before finishing parsing the body. If we can reduce the parsing duration within the body layout section, it should help bring the first paint value down.
Goal 2: Reduce parsing duration in the body layout
With these two goals in mind, let’s understand a little math here. If we cannot remove any lines of code in the project, what can we do to reduce the parsing duration in the head and body layouts? The answer is to push the parsing duration to body scripts near the end of the body. Let the browser parse what is important first, so that it can render it as soon as it can.
|
Duration (Benchmark) |
Desired result |
Head |
2560 ms |
Less time spent here |
Body layout |
2107 ms |
Less time spent here |
End body scripts |
17 ms |
More time spent here |
First Paint |
A timing within body layout |
Faster |
We are trying to change the parsing duration within each section to ultimality reduce the first paint metric timing.
4. Clean up the scripts that don’t belong anywhere
Imagine you’re moving into a new house. You usually don’t start by packing up the clothes you need to use everyday—you start with stuff you hardly ever use. Likewise, before we go and move all the script to the bottom of the page, let’s start with the scripts that don’t belong anywhere: scripts in the middle of the body.
Some of these scripts have dependency on some Javascript libraries, which makes it really hard to optimize for performance. It puts us at risk of breaking the site if we move these scripts to the bottom of the page without considering these dependencies. In Shopify themes, scripts in the middle of body includes any Javascript sitting in any Liquid files except for the layout Liquid files. So don’t try to fix everything at once—pick a battle and start there.
To make things a little easier, pick a page and start with the very first script you encounter after the opening body tag that is not in a layout Liquid file. Relax, it’s like playing a game of Pokemon when you first enter a grassy area.
First encounter
You found your first enemy—I mean, script. First, I want you take out your measuring tape and magnifying glass and really understand what this script is doing.
Benchmark a script
We will be doing exactly what we did earlier with performance marks, except now we will measure specifically for a given script, like in the following example:
<script>
window.performance.mark(window.markNames.menuScriptParse.start);
<!-- REST OF SCRIPT CODE -->
window.performance.mark(window.markNames.menuScriptParse.end);
</script>
The above example will measure the parsing duration of this particular script.Don’t forget to add new performance mark names in the theme.liquid
file:
In the console log, we’ll see something like this:
We can see that the browser took about 12ms to parse. That doesn’t seem too bad. The key is determining how many of these we have. If we have 200 of these scripts roaming around, that adds two seconds of duration.
Finding script dependencies
Before we can migrate the location of this script, we need to find all its dependencies. These can be anywhere. Here are some clues to look for:
-
$('css_selector')
JQuery. - Function calls to nowhere. Search where this function is declared.
-
{{ * }}
Liquid tags.
Document the dependencies at the beginning of the script. This will come in handy when we start moving code around.
What to do when there are Liquid tags in the script
If the Liquid tag is a global tag, moving this piece of code to theme.liquid
will be fine. If the Liquid tag is associated with a particular template layout, wrap the script with the following:
{% if template == 'collection' %}
<script>
<!-- REST OF SCRIPT CODE -->
</script>
{% endif %}
If the script code involves complex dependency on the existence of code somewhere, this is where you would start refactoring the code so that you can move it out of the Liquid files.
Move that code!
Once you’ve identified all the script dependencies, it’s time to move the code. We’re aiming to move all the code to the end of the body tag. To maintain the original order of script execution(important in most cases, unless you figure out how to refactor whatever is there to be not order dependent), leave it just after the performance mark for body end scripts, like the following:
Progressively benchmark your changes
After you’ve moved your code, make sure you didn’t break the page by checking if what you expected to happen, has happened.
|
parse_head start time |
Parse head |
Parse_body layout |
Parse_body end_scripts |
First paint |
Room for improvement (ms) |
Base |
1090 |
2560 |
2107 |
17 |
4805 |
1155 |
Menu script |
946 |
2546 |
2046 |
24 |
4661 |
1169 |
Compared to the base measurements, we can see the parse duration for the body layout has decreased and the parsing duration for the body end scripts has increased. We know the menu script has a 10ms parsing duration, so this is expected. The first paint has decreased as well, but it is too early to be conclusive of anything. We know that first paint can happen the moment the browser finishes parsing the head.
The Room for improvement column is the number of milliseconds of delay from the end of the parsing head. It is a good indicator to tell us if we are optimizing between a 50 percent gain versus a 1 percent gain.
Room for improvement = First paint — ( parse head start time + parse head duration )
Rinse and repeat
Now, do the same thing for every script you encounter in the middle of the body.
|
Parse head start time |
Parse head |
Parse body layout |
Parse body end scripts |
First paint |
Room for improvement (ms) |
Base |
1090 |
2560 |
2107 |
17 |
4805 |
1155 |
Menu script |
946 |
2546 |
2046 |
24 |
4661 |
1169 |
Script 1 |
891 |
2569 |
2041 |
34 |
4622 |
1162 |
Script 2 |
928 |
2487 |
2039 |
78 |
4590 |
1175 |
Script 3 |
972 |
2540 |
2060 |
80 |
4690 |
1178 |
Script 4 |
1022 |
3147 |
2080 |
118 |
5066 |
897 |
Script 51 |
909 |
2561 |
1553 |
118 |
4716 |
1246 |
Script 6 |
923 |
2561 |
1428 |
128 |
4734 |
1250 |
Script 7 |
951 |
2557 |
1416 |
124 |
4746 |
1238 |
Script 8 |
921 |
2563 |
1325 |
152 |
4782 |
1298 |
Script 9 |
978 |
2556 |
1308 |
138 |
4825 |
1291 |
Script 102 |
1372 |
2568 |
1145 |
275 |
4052 |
112 |
This is truly a battle for the milliseconds. In the process of moving scripts to the bottom, some refactors were made so that the script in the middle of body is not dependent on the script’s location. In the results above, we can see that we have successfully reduced about one second of the parsing duration for the body and shifted some of that parsing to the body end scripts. This resulted in a relative 8 percent improvement for first paint time, which is about 320 ms.
5. The finishing move
It’s time for the final boss: moving all the scripts in the head to the end of the body, while maintaining the script order.
|
Parse head start time |
Parse head |
Parse body layout |
Parse body end scripts |
First paint |
Room for improvement (ms) |
Base |
1090 |
2560 |
2107 |
17 |
4805 |
1155 |
All scripts |
977 |
1193 |
3938 |
161 |
2199 |
29 |
Let’s understand what’s happening here. We moved quite a few external script requests from the head tag to the body tag. External resources that were initially render blocking are no longer blocking. This allows the first paint to happen much faster.
We have effectively improved the start render time by almost 40 percent, without losing any script functionalities. 🎉
You might also like: 4 Lesser Known but Powerful Web Developer Tools that Increase Productivity.
6. Double check
It never hurts to do this. We achieved amazing numbers simply by shifting script around the document. Give it a run through the web page test. In my case, I found a very interesting problem.
The page had decent first paint timing, but went back to blank until the ten-second mark. What happened?
Turns out, the site implemented the anti-flicker snippet from Google Optimize’s A/B experiment framework.
We have options to work around this. We can try moving this Google Tag Manager script back to the top of the head and see how it does.
Honestly, I don’t think it’s worth it. It added three whole seconds to the first paint, even if it’s the first resource to download, just for the potential that there is an experiment to run. There are better ways to instrument A/B experiments without hiding the entire document for potentially four seconds, which is exactly what this anti-flicker snippet is doing.
This issue actually explained why I was not seeing progressive improvements as I moved the scripts out of the body. It was always delayed by this anti-flicker snippet.
Bring speed to your webpage
Success! We have improved start rendering time, all without losing the scripts that we need to keep. Fixing site performance is up to all of us. As Shopify continues to improve site performance on the store front, every developer should do what they can to improve site performance as well. It benefits everyone.
Additional Resources
- Javascript Loading Priorities by Addy Osmani
- How Browsers Work by Tali Garsiel and Paul Irish
Read more
- Introducing Online Store 2.0: What it Means For Developers
- How Open Graph Tags can Optimize Social Media Experiences
- Working with Product Variants When Building a Shopify Theme
- How to Build a Shopify App: The Complete Guide
- How to Build a Customizable Related Products Section
- How to work with Metafields when building Shopify themes
- How to Use Math Filters with Liquid
- How to Use Liquid to Create Custom Landing Page Templates
- A Modern Approach to CSS Pt. 2: Building Resources
- Getting the Most Out of Chrome Developer Tools: 4 Modern Features You Need to Know