Here’s a step-by-step case study on how we optimized a Shopify ecommerce site with a 24% improvement in page speed, through the use of MachMetrics speed tracking and the expertise of Eugene Chekan, one of our recommended speed consultants.
This challenge is unique since it’s a Shopify site we don’t have any control over server settings, and are very limited in what we can edit on the page – due to Shopify’s theme structure. Yet we were still able to move things around to get a decent speed improvement.
Our target today is Authority Kitchen, a Shopify-driven e-commerce store. We focused our efforts on two types of pages to inspect and optimize – a main page and a product page. Let’s start with high level performance overview and then dive into specific optimizations.
High Level Analysis
Let’s take a look at primary page load metrics:
Fairly low Time To First Byte (TTFB, orange line) trending flat around 0.7–0.8 seconds. This is a good news for us: authoritykitchen.com is built on Shopify platform and we can’t affect back-end server performance much. Quite a gap between Document Load (black line) and Page Completion (blue line) though! This is most likely due to lots of resources like JavaScript, CSS and images being loaded, and browser needs some time to process and execute all this stuff. Let’s see if that’s the case:
Wow, that’s a lot of JavaScript! Its parsing and execution by browser affects our Page Complete metric for sure, and we can’t do much about this except removing some scripts from the page completely. But does it affect our First Render Time?
JavaScript Analysis
Let’s take a look at how scripts are affecting page render with Chrome Developer Tools:
There are 3 scripts that are loaded before browser begins to render page. Luckily, two of them — ga_urchin_forms and shopify_stats — are loaded asynchronously and don’t block page render in any way. Real problem here is the jQuery <script> tag: there’s no async or defer attribute and browser loads script content in a blocking fashion, postponing page render until script is fully downloaded and executed.
What is render-blocking?
Here’s how browser renders a page: it loads HTML and turns tags into Document Object Model (DOM), builds CSS Object Model (CSSOM), DOM and CSSOM are then combined and all visible elements are rendered. When browser stumbles upon JavaScript or CSS during initial HTML parsing it pauses parsing and rendering until JS/CSS is downloaded and parsed. Why? Because JS can change HTML elements — even delete them or turn them into HTML comments — and CSS can make elements invisible or change their position. So browser has to download all external resources before proceeding and actually render any content. That’s why those external resources called render-blocking. The easiest way to ‘unblock’ rendering is to set async or defer attribute at the <script> tag. For CSS, it would be inlining critical parts of CSS right into HTML with <style> tag and loading all other styles from external stylesheets after the page is rendered.
So how should we solve jQuery being render-blocking issue? We can’t make it load asynchronously because some inline scripts are using jQuery and we’re not in control of those scripts. Although we can move jQuery as far down from <head> as possible so browser could render some content before blocking because of jQuery. In this particular case jQuery library isn’t used until main content section which gives us opportunity to render page header and menu.
There’s one caveat with this approach: browser will not download jQuery until HTML parser reach jQuery <script> tag. And we moved it quite far in the document. Sure, we improved first render time, but we’re still wasting a lot of time waiting for jQuery to be downloaded and parsed. You might think if there only was a way to tell browser to preload some important resources right away! And there is such a way: <link> tag with rel=’preload’ attribute.
To wrap up, here’s what we should do:
- Move jQuery as far down the document as possible.
- Place <link rel=’preload’> that points to jQuery as close to the start of the document as possible.
Let’s do this and take a look at how things are:
Cool, looks like jQuery being preloaded right away without blocking render until it is used later in the document! Note how the browser prioritized resources: the one we explicitly stated as important with <link rel=’preload’> is getting high priority, while others that are loaded asynchronously are having low priority.
Low-priority JavaScript
There’s another thing we can do with JavaScript to speed things up, those low-priority asynchronous scripts. We can move a whole bunch of them to the end of the document, that is, right before closing </body> tag. This way browser will not waste time on executing scripts before page is fully rendered.
CSS
There’s another side of render-blocking resources issue, CSS. It is much easier to deal with compared to JS optimizations though: move styles to the end of the document, adding them to DOM with requestAnimationFrame if possible, and inline critical CSS in the <head> tag. Let’s start with the easiest one, critical CSS.
Critical CSS
Simply put, critical CSS are the styles needed to render above the fold content correctly. You then inline these styles in document’s <head> allowing browser to render content without blocking on waiting for stylesheets download whatsoever. There’s a plenty of tools for critical CSS generation, critical being a weapon of choice for me.
So, we have our critical CSS now, let’s inline it and move forward with postponing external stylesheets until page is rendered.
RAF hack
Here’s a small snippet that will add external stylesheets to the DOM only after something was rendered:
<noscript id="deferred-css"> {{ 'theme.scss.css' | asset_url | stylesheet_tag }} {% include 'google-fonts' %} </noscript> <script> var loadDeferredStyles = function() { var addStylesNode = document.getElementById("deferred-css"); var replacement = document.createElement("div"); replacement.innerHTML = addStylesNode.textContent; document.body.appendChild(replacement) addStylesNode.parentElement.removeChild(addStylesNode); }; var raf = requestAnimationFrame || mozRequestAnimationFrame || webkitRequestAnimationFrame || msRequestAnimationFrame; if (raf) raf(function() { window.setTimeout(loadDeferredStyles, 0); }); else window.addEventListener('load', function() { window.setTimeout(loadDeferredStyles, 0)}); </script>
There are Shopify-specific styles inside <noscript id=’deferred-css’> tag, but you can replace those with your <link rel=’stylesheet’> and this snippet will work for you. What this snippet does:
- All styles are placed in <noscript> tag, so they will be automatically loaded if browser doesn’t have JavaScript enabled, i.e. no broken styles for AdBlock/NoScript users.
- If browser is modern enough (in this particular case, newer than IE10, see browser compatibility table at MDN), then styles will be appended to the DOM when browser will be ready to render something.
- If not, styles will be appended when the page and its resources are loaded.
Let’s take a look on how’s the page rendered now:
Great! The page begins to render right after jQuery is loaded and no single CSS file is needed for this. We now have only one render-blocking resource, jQuery, and that’s it. We can call it partially render-blocking because browser now can render part of the content without any external scripts or styles. This way of rendering is called progressive rendering and is considered a good thing, because users love seeing content ASAP, even its parts. Here’s the comparison between rendering filmstrip to illustrate this point:
Note how page was rendered before: browser has to wait until external render-blocking resources are loaded and after that the whole page is rendered. Now, after we optimized render-blocking resources, the page is rendered step-by-step: header with menu, product info, product image. To further understand progressive rendering let’s combine Visual Progress graph with a filmstrip above while taking notes when resources are loaded:
Have you noticed that jQuery was loading earlier before optimizations? How come that page starts render faster now? Bonus points if you thought “well, that must be those CSS optimizations helping out”. You are correct on critical CSS moving the needle here, but there’s another trick: deferred scripts.
Asynchronous JS optimization
One important thing about Shopify: there’s a way for Shopify App vendors to add their scripts to the page alongside default Shopify ones. These scripts are added with async attribute, which is a good thing. But here’s the main difference between defer and async:
async scripts are evaluated at the first opportunity after they finished downloading. That is, browser will prefer evaluate asynchronous JavaScript rather than render some part of the page. On the other hand, browser will wait until DOMContentLoaded event before executing any deferred script.
So here’s what happened when low-priority scripts were loading in async fashion:
- Browser loads HTML and starts to parse it, requesting synchronous scripts with high priority and asynchronous scripts with low-priority.
- Rendering is blocked until all critical resources like CSS and synchronous JS are downloaded and parsed.
- While browser parser critical resources, low-priority asynchronous scripts are starting to arrive, getting at the front of the browser’s thread queue and postponing rendering tasks.
- Browser parses and executes asynchronous scripts.
- Rendering task is now getting some attention from browser and page is rendered finally.
Simply put, rendering is not blocked per se, it’s just browser doesn’t have any spare time to care about rendering because of async scripts.
To fix this, we replaced async attribute with defer explicitly saying to the browser “don’t bother about these scripts. Like, really. Do some render, load stuff you need, and get onto those deferred scripts when important stuff is loaded and rendered”.
Shopify doesn’t provide any interface to make these changes, but there’s always a number liquid shenanigans you can use. Just remember to test out things carefully.
CDN
Now, when most impactful issues are resolved, we’re left with other issue that are hard to approach on Shopify platform: back end speed. Generally speaking, Shopify back end performance is not bad at all, but it may vary from time to time. One way of fixing this is to serve content via CDN: this will give us early traffic termination and, as a bonus, some other tweaks like correct HTTP headers, namely, cache-related ones. Early traffic termination will reduce roundtrip times globally: US users will get content served from US CDN servers, Asia users — from asian CDN servers, and so on.
We chose Cloudflare for its wide edge servers network, overall robustness and some cool features like Page Rules that are available even in free-tier plan.
Interestingly enough, the only one resource that is served from authoritykitchen domain is the HTML page itself, all other resources are served from Shopify’s CDN domain. And the page itself is not cacheable by content delivery networks like Cloudflare, yet there’s a noticeable boost in TTFB:
Explanation for this is simple: there’s a lot of Cloudflare edge servers and their connectivity to surrounding data centers is much much better than that of the average network provider. It’s like you’re sitting with a laptop right next to the server, no matter where in the world you actually are.
Let’s talk about this a little further – because many people do not realize that CDN’s can improve TTFB. Cloudflare has a performance-oriented infrastructure and brilliant engineers, so they care a lot about transfer speed between data centers. Here’s what happens when there’s no Cloudflare in place:
Your pc <-> your data provider <-> unknown routes on the net (fair amount of them) <-> Shopify CDN and edge servers data center (likely, closest to your)
Your pc <-> your data provider <-> unknown routes on the net (lesser amount of them) <-> Cloudflare edge servers DC <-> Shopify DC.
Overall Results
To wrap things up, here’s a couple of summary charts from MachMetrics showing the speed improvement over time:
Speed Index and TTFB improvements:
Page Complete and Doc Load improvements:
Overall we were able to shave off about 2.5 seconds of Page Complete time – that’s 2.5 more seconds that this ecommerce site’s visitors are going to spend looking at their products instead of waiting around. Win!
Awesome 2.5 second improvement is huge!!!
Thanks for this guide.
So glad to have helped!
Nice 1.5 seconds. I can always use help, though with Shopify.
awesome tips, i purchased Plak shopify theme (the speediest one), my speed now is at 92%, i’m so happy
i anyone could help me to implement this in my shopify
You must hire a pro to increase your store speed, otherwise, you can just purchase PLAK theme which is pretty fast
Me too iam using Plak, i dont even have to hire a speed expert, this theme is great and allowed me to quit my job quickmy.
Thanks
thank you,this script work in my site , thank you very much
really awesome guide. so helpful for anybody. it is easy understandable even though I am a beginner in shopify.
Thank you for the break down of possibilities, I went from a loading time of 10 seconds using the shoptimized theme to right at 2.7 seconds loading time now.
That’s amazing, Michael – great job!
sir how did you pull up that menu ? I am in the google developer settings but i cant find what you are on .
do you think you could help me out with me site as well ? any tips on how to decrease load times. I have compressioned images etc still at quite a low score on lighthouse/ google speed test. i am 17 years old so i am just learning lol.
Thank you