Announcing Online Store 2.0
Online Store 2.0 is an end-to-end overhaul of how themes are built at Shopify, launched June 2021. While the information in the following article is still correct, it doesn't account for Online Store 2.0 best practices, and may not include references to recent features or functionality. To learn more about how to build with Online Store 2.0, visit our updated documentation.
Visit docsWith Shopify, you have lots of flexibility to build themes representing the brand of a merchant’s online store. However, like with any programming language, you might not be aware of the performance impact of the code that you write. Whether it be the performance impact on Shopify’s servers or observable performance impact on the browser, ultimately, it’s the customers that experience the slowness.
The speed of server-side rendering is one of the most important performance timings to optimize for. While server-side rendering completes, customers wait on a blank screen—not a good experience for them. Even though we’re working hard to make server-side rendering as fast as possible, bottlenecks may still originate from the Liquid source itself.
Luckily, there are tools available to help you analyze Liquid render performance. Specifically, the Shopify Theme Inspector Chrome extension can help you debug performance and make your themes fast. In this article, we’ll look at how to interpret the flame graphs generated by the inspector, how unoptimized Liquid code patterns show up in the flame graphs, and share tips for spotting and avoiding these performance issues.
1. Install the Shopify Theme Inspector
Using a Google Chrome browser, install the Shopify Theme Inspector extension. Jump into our previous article on using the Shopify Theme Inspector to get started with the extension and get to a point where you can produce a flame graph on your store like the one shown below.
The flame graph produced by this tool is a data representation of the code path and the time it took to execute. With this tool, as a developer, you can find out how long a piece of code took to render.
You might also like: How to Refactor a Shopify Site for Javascript Performance.
2. Start with clean code
We often forget what clean implementation looks like. As time passes, code becomes complicated, especially as you find workarounds and ways to achieve your goals. But to get an accurate picture of render speeds, we need to go back to the clean implementation to understand why it takes the time it does to render.
"We often forget what clean implementation looks like."
The simple code above creates the following flame graph when run through the Theme Inspector:
The template section took 13 ms to complete rendering. But, let’s dig in to get a better understanding of what we are seeing here.
The area where the server took the time to render is where the code for the pagination loop is executed. In this case, we rendered 10 product titles. Then, there’s a block of time that seems to disappear—this is actually the time spent on Shopify’s side collecting all the information that belongs to the products in the paginate collection.
3. Take a look at inefficient code
To know what’s inefficient code, you need to know what it looks like, why it’s slow, and how to recognize it in the flame graph. This section walks through a side-by-side comparison of code and its flame graphs, and how a seemingly simple change can result in bad performance.
Heavy loop
Let’s take that clean code example and make it heavy.
What I’ve done here is accessed attributes in a product while iterating through a collection. Here’s the corresponding flame graph:
The total render time of this loop is now at 162 ms, compared to 13 ms from the clean example. The product attributes access changes a less than one ms render time per tile to 16 ms render time per tile. This produces exactly the same markup as the clean example, but at the cost of 16 times more rendering time. If we increase the number of products to paginate from 10 to 50, it takes 800 ms to render.
Tips:
- Instead of focusing on how many one millisecond bars there are, focus on the total rendering time of each loop iteration
- Clean up any attributes that aren’t being used
- Reduce the number of products in a paginated page (potentially AJAX the next page of products)
- Simplify the functionality of the rendered product
You might also like: Working with Product Variants When Building a Shopify Theme.
Nested loops
Let’s take that clean code example and make it render with nested loops.
This code snippet is a typical example of iterating through the options and variations of a product. Here’s the corresponding flame graph:
This code snippet is a two-level nested loop rendering at 55 ms.
Nested loops are hard to notice when just looking at code because they’re separated by files. But with the flame graph, we see additional rows representing them.
As highlighted in the screenshot above, the two inner `for` loops stack side by side. This is okay if there are only one or two loops. However, each iteration’s rendering time will vary based on how many inner iterations it has.
Let’s look at what a three-nested loop looks like.
This three-level nested loop rendered at 72 ms. This can get out of hand really quickly if we aren’t careful. A small addition to the code inside the loop could blow your budget on server rendering time.
Tips:
- Look for a sawtooth shaped flame graph to target potential performance problems
- Evaluate each flame graph layer and see if the nested loops are required
Mix usage of multiple global Liquid scope
Let’s now take that clean code example and add another global scoped Liquid variable.
And here’s the corresponding flame graph:
This flame graph is an example of a badly nested loop, where each variation is accessing the cart items. As more items are added to the cart, the page takes longer to render.
Tips:
- Look for hair comb or sawtooth shaped flame graphs to target potential performance problems.
- Compare flame graphs between having one item and multiple items in the cart.
- Don’t mix global Liquid variable usage. If you have to, use AJAX to fetch for cart items instead.
You might also like: How We Improved Theme Development Tooling Using Checksums.
4. Understand what is fast enough
When using the Theme Inspector extension to measure rendering time, try to aim for 200 ms, but no more than 500 ms total page rendering time. We didn’t just pick a number out of the hat: it’s made with careful consideration of what other processes require page render time, and how time is allocated between those to meet our performance goals.
"When using the Theme Inspector extension to measure rendering time, try to aim for 200 ms, but no more than 500 ms total page rendering time."
Google Web Vitals has stated that a good score for Largest Content Paint (LCP) is less than two and a half seconds. However, the largest content paint is dependent on many other metrics, like time to first byte (TTFB) and first content paint (FCP).
So, let’s make some time allocation! Also, let’s understand what each metric represents:
- Network overhead time is the time required from Shopify’s server to communicate with a browser. It varies based on the network the browser is on. For example, whether the user is navigating the store on 3G or Wi-Fi.
- From a blank browser page (TTFB) to showing anything on that page (FCP) is the time the browser needs to read and display the page.
- From the FCP to the LCP is the time the browser needs to get all other resources (images, CSS, fonts, scripts, video, etc.) to complete the page.
The goal is an LCP of less than two and a half seconds:
- Server → Browser: 300 ms for network overhead
- Browser → FCP: 200 ms for the browser to do its work
- FCP → LCP: one and a half seconds for above-the-fold image and assets to download
This leaves us with 500 ms for total page render time.
Does this mean that as long as we keep server rendering below 500 ms we can get a good LCP score? Unfortunately no—there are other considerations, like critical rendering path, that aren’t addressed here. But, at least this gets us half of the way there.
Tip:
- Optimizing for critical rendering path on the theme level can bring the 200 ms requirement between the browser to FCP timing down to a lower number
So, we have 500 ms for total page render time, but this doesn’t mean you have all 500 ms to spare. There are some mandatory server render times that are dedicated to Shopify and others that the theme dedicates to rendering global sections like the header and footer. Depending on how you want to allocate the rendering resources, the available rendering time you leave yourself with for the page content varies. For example:
Total |
500 ms |
Shopify (content for header) |
50 ms |
Header (with menu) |
100 ms |
Footer |
25 ms |
Cart |
25 ms |
Page content |
300 ms |
I mentioned trying to aim for a 200 ms total page rendering time—this is a stretch goal. By keeping ourselves mindful of this goal, it’s much easier to start recognizing when performance starts to degrade.
A stronger overall performance
By using the Shopify Theme Inspector Chrome extension, you can run in-depth analyses like the ones you’ve seen in this article to make sure your clients’ stores are performing at their best, helping them lose fewer conversions and make more sales.
If you are experimenting with the Shopify Theme Inspector, please consider sharing your experience with us and let us know how we can improve. You can also tweet us at @shopifydevs.
Read more
- Free Course] 10x Your Shopify Theme Development Skills with These Free Courses
- Using Placeholder Images for Products with the placeholder_svg_tag Filter
- How to Use Liquid's \"case/when\" Control Tags in Shopify Themes
- Web Performance: 11 Must-Read Tips to Give Your Site a Speed Boost
- How the Routes and Page_Image Liquid Objects Can Help You Build More Robust Themes
- Understanding Progressive JPEG and WebP Formats
- How Safe As Milk Grew a Successful Theme Partner Business
- How to Work With Shopify Theme Blocks
- How to Personalize Shopify Themes with the customer Object
- How to Get the Most out of the Timber Theme Framework