If you’re running a Shopify store, you know how important it is to have fast load times. Slow websites can result in lost sales and frustrated customers.
Fortunately, with the release of Shopify 2.0 came the added feature of rendering sections using the AJAX Cart API, which (with some clever engineering) can be used to improve your store’s server response times.
In this article, I’ll share how I use the AJAX Cart API to create fast Shopify themes for my clients .
The problem
When you have a deep hierarchy of menus and submenus in your store, it takes Shopify a lot of time to process them before eventually serving the page.
Even worse when you have the same menu three times, once in the header, once in the footer, and once in a sidebar menu that only appears on mobile.
The more Liquid you have in your theme files, the longer it takes Shopify to process your pages.
The process
After developing themes for dozens of Shopify stores and noticing the same problem arise, it got me thinking and eventually experimenting.
The first thought that came to mind was to try loading the menus asynchronously, effectively cutting out the process of processing the menus entirely and serving the page sooner.
Starting with the homepage template (index.json), I tried the following
<script>
fetch(window.location.pathname + "?sections=header")
.then(res => res.json()).then(sections => {
let ourHeader = document.querySelector('#shopify-section-header');
if(ourHeader && sections['header']) {
ourHeader.outerHTML = sections['header'];
}
})
</script>
Which worked but still did not solve our problem of improving server response time.
To fix this, I would have to stop Shopify from loading the header in index.json, but somehow still have the section present for use with the Section Rendering API.
This was somewhat of a paradox; to asynchronously load a section, it would have to be present in a template, but if it’s present in a template, then we’ve defeated the purpose of asynchronously loading anything!
Thinking outside the box
While searching through the Shopify documentation for an answer, I noticed that we don’t need to use the Section Rendering API altogether (or at least not directly)
Instead, we can use the AJAX Cart API??
As it turns out, the Ajax Cart API allows you to fetch sections after making changes to the cart, but more importantly, we can also specify a path to use via the “sections_url” parameter.
We don’t necessarily have to make any changes to the cart to use this feature!
That, and mixed with the fact that we can use
?view=
to view any page through a specific template, we now have a clear plan to make it all work together.The solution
Creating a suffixed copy of index.json (eg. index.bashr.json) allows us to separately render any section on the homepage through the “bashr” view
<script>
fetch('/cart/update.js', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
sections: 'header',
sections_url: `${window.location.pathname}?view=bashr`
})
}).then(res => res.json()).then(data => {
document.querySelector('#shopify-section-header').outerHTML =
data.sections['header'];
})
</script>
For this to work, we need our header section to intelligently render the menus only when it’s called from the “bashr” view.
{% unless template.suffix == "bashr" %}
<div id="shopify-section-header"></div>
{% else %}
{% comment %} Here goes the section's code {% endcomment %}
...
{% endunless %}
Reformatting our section like that, we now have an asynchronously loaded header section!
We can do this for our two other menus (footer + sidebar-menu)
fetch('/cart/update.js', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
sections: 'header, sidebar-menu, footer',
sections_url: `${window.location.pathname}?view=bashr`
})
}).then(res => res.json()).then(data => {
document.querySelector('#shopify-section-header').outerHTML =
data.sections['header'];
document.querySelector('#shopify-section-footer').outerHTML =
data.sections['footer'];
document.querySelector('#shopify-section-sidebar-menu').outerHTML =
data.sections['sidebar-menu'];
})
{% unless template.suffix == "bashr" %}
<div id="shopify-section-footer"></div>
{% else %}
{% comment %} Here goes the section's code {% endcomment %}
...
{% endunless %}
{% unless template.suffix == "bashr" %}
<div id="shopify-section-sidebar-menu"></div>
{% else %}
{% comment %} Here goes the section's code {% endcomment %}
...
{% endunless %}
And voila! 🤩
We’ve now asynchronously loaded all three of our menus
CLS Issues
Although we’ve potentially improved our site’s load speed in one area, we’ve potentially worsened our site’s load speed in another.
CLS issues are caused by unpredictable shifts in layout; if the location of a button you’re trying to click abruptly changes, you might unintentionally click something else on the page or miss the button entirely.
Along with a bare
<div id="shopify-section-..." >
acting as the placeholder on “non-bashr” views, we can also try setting a height and/or width for that div so that the page accounts for the layout shift.
{% unless template.suffix == "bashr" %}
<div id="shopify-section-header"
style="min-height: 200px; min-width: 100vw;">
</div>
{% else %}
{% comment %} Here goes the section's code {% endcomment %}
...
{% endunless %}
Or better yet, a CSS class defined for different viewports
{% unless template.suffix == "bashr" %}
<div id="shopify-section-header"
class="header-placeholder">
</div>
{% else %}
{% comment %} Here goes the section's code {% endcomment %}
...
{% endunless %}
Accounting for random ids
In the above code snippets, we’ve assumed every section will have an ID of “shopify-section-” followed by the name of the file “header”
But in reality, most sections have randomly generated IDs, and it would be difficult to guess the IDs of sections that may or may not exist in a template.
To fix this, I’ve created 2 global variables at the very beginning of the document to define “asyncSects” & “asyncLoads”
<script>
window.asyncSects = window.asyncSects || [];
window.asyncLoads = window.asyncLoads || 0;
</script>
And replaced the div placeholder in every async section with the following
<script>
window.asyncSects.push({id: "{{ section.id }}",
hookname: "section-{{ section.id }}"})
</script>
And finally, at the end of the document…
<script>
window.addEventListener('DOMContentLoaded', () => {
const sects = window.asyncSects;
fetch('/cart/update.js', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
sections: sects.map(s=>s.id).join(','),
sections_url: `${window.location.pathname}?view=bashr`
})
}).then(res => res.json()).then(data => {
Object.keys(data.sections).forEach(s => {
document.getElementById(sects[0].hookname).outerHTML =
data.sections[s]
window.asyncLoads++;
})
})
})
</script>
Now, we don’t even have to name the sections, and the code will just know which sections ought to be asynchronously loaded!
And yes! We can use this for other sections that may cause a slowdown on server side Liquid processing
All you’d have to do is reformat your section to look like this
{% unless template.suffix == "bashr" %}
<script>
window.asyncSects.push({id: "{{ section.id }}",
hookname: "section-{{ section.id }}"})
</script>
{% else %}
... (whichever section's code goes here)
{% endunless %}
Do you have many sections you want asynchronously loaded? Try this!
<script>
window.addEventListener('DOMContentLoaded', () => {
const chunkSize = 5; // Shopify limits to 5 sections per request!
for (let i = 0; i < window.asyncSects.length; i += chunkSize) {
const sects = window.asyncSects.slice(i, i + chunkSize);
fetch('/cart/update.js', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
sections: sects.map(s=>s.id).join(','),
sections_url: `${window.location.pathname}?view=bashr`
})
}).then(res => res.json()).then(data => {
Object.keys(data.sections).forEach(s => {
document.getElementById(sects[0].hookname).outerHTML =
data.sections[s]
window.asyncLoads++;
})
})
}
})
</script>
Caveats to be aware of
- Every Ajax cart request allows up to 5 sections per request, which may sound great at first for limiting number of requests, but after playing around with it for a while, I’ve noticed that 1 section per 1 request is optimal.
- Some themes only load initialization code on the initial page load, and since we’re deferring our sections for after the initial page load, we might miss out on that initialization code, check out a slightly more developed version of the JS that addresses this problem on my Github.
Conslusion
Although this is not the standard way of rendering sections, it still shows the importance of thinking outside the box and coming up with alternative solutions that ultimately give you an edge over your competitors’ stores.