Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Share your Performance Wins! #41

Closed
wizardlyhel opened this issue Jan 31, 2020 · 41 comments
Closed

Share your Performance Wins! #41

wizardlyhel opened this issue Jan 31, 2020 · 41 comments

Comments

@wizardlyhel
Copy link
Contributor

wizardlyhel commented Jan 31, 2020

We would love to learn how you use this tool and solve your Liquid rendering issues. Comment below and tell us:

  • How many milliseconds have you saved? (Even a 50ms saving makes a big difference)
  • What was the cause? (Bad liquid pattern? Injected by another app? Zombies?)

Tweet us your wins! @shopifydevs

@wizardlyhel wizardlyhel pinned this issue Jan 31, 2020
@tehaksbrid
Copy link

Have spent about a week learning how different liquid structures perform. Just attacked our homepage for the last day, targeting low-hanging (no rewrite) stuff.

  • 88% reduction in liquid transpile time on homepage.
  • Killed two zombie app integrations that were adding 3s each. Killed a monstrous nested loop over products/variants/options/tags. Increased parallelism by merging some snippets into the same file; they were checking a lot of the same properties but were originally many different files.

This is awesome so far, thank you. I was seriously considering a total rewrite the week before you released this because of how opaque our liquid was.

@lukechadwick
Copy link

From:
screenshot

To:
screenshot

Very useful tool.

@dan-gamble
Copy link

@lukechadwick I'm more interested in finding out how on earth it was that high, to begin with!

@lukechadwick
Copy link

lukechadwick commented Feb 10, 2020

A combination of Bold and Swym snippet loops

OG: https://screenshot.click/11-30-vi3qy-f7dwg.png
After removing Swym and Bold code: https://screenshot.click/11-30-wy8tk-jol0w.png

Not necessarily the fault of Bold/Swym's apps, the general state of this theme was in pretty bad shape. A lot of app code scattered around from installs/uninstalls over time, and built on a pretty old version of Brooklyn.

@dan-gamble
Copy link

Fair enough. Thanks for sharing @lukechadwick.

It's awesome the extension was able to help you so much!

@tehaksbrid
Copy link

@lukechadwick We're in the same boat; even our numbers were similar. The liquid speedups were excellent but we learned that only about 1/10 visitors were triggering a liquid recompile.

This was the real impact of refactoring from 12.5s compile time to 1.2s

homepage

Blue is liquid compile time, averaging in 5 minute intervals.

@grigs
Copy link

grigs commented Feb 18, 2020

@tehaksbrid How did you determine that only 1/10 were triggering the liquid recompile? We've been seeing 50% of people getting CDN misses.

What did you use to generate the graph you shared? Is the liquid compile time you're measuring the actual compile time or a close approximation metric like time to first byte?

@tehaksbrid
Copy link

We sample performance metrics from all site visitors whose browsers support navigation timing level 2. That graph doesn't measure it directly, it's something like TTFB (specifically, the PerformanceNavigationTiming value of responseStart - requestStart); something that is a reasonable proxy for liquid compile time.

When plotting our homepage first loads, we found that 90% of users fell in a band 0.5-2s TTFB and the remainder showed the TTFB times we were expecting from liquid profiling. To me this was strong evidence that the template cache was succeeding.

@susanlangenes
Copy link

Went from ~5000ms down to 353ms. 😱

All by removing Locksmith app (on test site). Can't completely remove it from live site, but after a first pass of tidying up the app, live site went from 5052ms to 3811ms. Clearly more to do here.

This may be a good argument for using the Shopify Plus wholesale channel!

@PaulNewton
Copy link

@susanlangenes i'm interested in this usecase with locksmith could you elaborate on what's blocking making changes in regards to locksmith?
Is this a typical wholesale customization with the default locksmith install that could be replicated for testing against?

@wizardlyhel
Copy link
Contributor Author

wizardlyhel commented Feb 20, 2020

@tehaksbrid That is amazing! Even if only 1/10th of your visitors are triggering a liquid re-render, you prevented these visitors from experiencing 12.5 seconds wait on the page.

@susanlangenes Thank you for identifying a liquid rendering impacting app! This is worth noting for wholesale shops

@susanlangenes
Copy link

susanlangenes commented Feb 20, 2020

@susanlangenes i'm interested in this usecase with locksmith could you elaborate on what's blocking making changes in regards to locksmith?
Is this a typical wholesale customization with the default locksmith install that could be replicated for testing against?

@PaulNewton maybe? I personally wasn't involved in setting up Locksmith and this is my only client who uses it, so I don't know if it's a typical install. If you want to check it out, the site is slumberkins.com. What I think I see is that for every item in any linklist (navigation menus) Locksmith appears to be doing stuff.
Here's an example of Locksmith's code within a section of the footer:

<ul class="no-bullets site-footer__linklist">
            {%- comment %}<locksmith:c607>{% endcomment -%}
              {%- assign locksmith_2578_forloop__size = 0 %}{% for link in linklists[section.settings.footer_link_list].links %}{% capture var %}{% render 'locksmith-variables', scope: 'subject', subject: link, variable: 'transparent' %}{% endcapture %}{% if var == 'true' %}{% assign locksmith_2578_forloop__size = locksmith_2578_forloop__size | plus: 1 %}{% endif %}{% endfor %}{% assign locksmith_2578_forloop__index = nil -%}
            {%- comment %}</locksmith:c607>{% endcomment -%}
            {% for link in linklists[section.settings.footer_link_list].links %}
      {%- comment %}<locksmith:14a0>{% endcomment -%}
        {%- capture var %}{% render 'locksmith-variables', scope: 'subject', subject: link, subject_parent: linklists[section.settings.footer_link_list], variable: 'transparent' %}{% endcapture %}{% if var == "true" %}{% if locksmith_2578_forloop__index == nil %}{% assign locksmith_2578_forloop__index = 1 %}{% assign locksmith_2578_forloop__index0 = 0 %}{% else %}{% assign locksmith_2578_forloop__index = locksmith_2578_forloop__index | plus: 1 %}{% assign locksmith_2578_forloop__index0 = locksmith_2578_forloop__index0 | plus: 1 %}{% endif %}{% if locksmith_2578_forloop__index == 1 %}{% assign locksmith_2578_forloop__first = true %}{% else %}{% assign locksmith_2578_forloop__first = false %}{% endif %}{% if locksmith_2578_forloop__index == locksmith_2578_forloop__size %}{% assign locksmith_2578_forloop__last = true %}{% else %}{% assign locksmith_2578_forloop__last = false %}{% endif %}{% assign locksmith_2578_forloop__rindex = locksmith_2578_forloop__size | minus: locksmith_2578_forloop__index | minus: 1 %}{% assign locksmith_2578_forloop__rindex0 = locksmith_2578_forloop__size | minus: locksmith_2578_forloop__index0 | minus: 1 %}{% else %}{% continue %}{% endif -%}
      {%- comment %}</locksmith:14a0>{% endcomment -%}

Similar code exists for all navigation items in the header as well, which contains two versions of navigation (desktop and mobile). When I went through the theme and removed every instance of this type of code, that's when I got down to ~350ms.

@lukechadwick
Copy link

@susanlangenes @PaulNewton similar experience on several different stores, here's an example:

Before: https://screenshot.click/21-31-9prk8-2r23q.png
After removing locksmith: https://screenshot.click/21-39-ev7hs-ygew4.png

I think the main issue is that locksmith wraps it's code around so many different things (lock-smith has an autoinstall feature), including larger loops like collections and link-lists. This means the locksmith-variables snippet has to be rendered many times, which has it's own loops, variables and logic that add delay for each iteration.

This tanks performance when you have a fairly long link list or collection, so I think the only solution is to remove lock-smith from non-sensitive snippets that it doesn't need to protect.

@susanlangenes
Copy link

@lukechadwick thank you for chiming in - I really didn't know what to make of this, but my experience seems to be matching yours, and I agree with your suggestion of stripping it down where it's not needed.

@PaulNewton
Copy link

I went through the theme and removed every instance of this type of code, that's when I got down to ~350ms.

@susanlangenes
Definitely great for identifying the source of the issue, but almost a moot point since customers can't use it and making custom versions of an apps liquid code is a nightmare to upkeep.
One edge to test ,without going into the app snippets, is if there is any perf change when the linklists are small or preassigned?
Or if a settings lookup is replaced with a direct linklist object name,etc.
i.e. so linklists[section.settings.footer_link_list].linkslinklists.main-menu.links

@PaulNewton
Copy link

so I think the only solution is to remove lock-smith from non-sensitive snippets that it doesn't need to protect

@lukechadwick
Apps like that touch everything, they have to unfortunately to create a permission layer out of the box.
Not something to expect shopify to have to solve, though maybe there's something with a lot of work that could be made better in the liquid project itself.
Using susans locksmith example it's not simple to rule out linklists with tests easily since we can't really create objects in liquid and possibly never will be able to.
(Create object on the templating side? Shopify/liquid#387)

For anyone where this is critical a possible alternate to explore is ajax loading the auth content; move the perf hit into perceptual speed with loaders,transitions, or on user interaction.

Pull in the auth content from different page templates one set for what is "public" content and the other for serving authorized content(without a layout), or two themes one preinstall one post install. (meaning "public" is a blank surface serving nothing but basic presentation to avoid unauthorized access)

Obviously that really only works if the theme is:

  1. already setup for ajax loading or it can be a lot of work
  2. data and themes are already set up in advance to not render all product data by default willy-nilly

Though having 1&2 probably means it's all custom so no app like locksmith is being used anyway.

@susanlangenes
Copy link

@PaulNewton yeah I thought about an ajax solution for shops like this where it's necessary to have a lot of links. Or hey! Could throw a react app on top of it and away we go! Unfortunately there isn't budget for that at the moment.
With my client here, I am pretty sure the way they're using Locksmith will allow for removal of the Locksmith code wrapping linklist items. I have to check, but I believe they are only using Locksmith for certain types of pre-launch sales, where a set of users is given a link to arrive through, or a password. It's not full-scale wholesale. On Tuesday, we'll be evaluating the setup and their needs, and also talking to Locksmith's support channel if we can. Will report back.

@isaacbowen
Copy link

Hey crew! I wrote Locksmith. Performance is hard, when you're doing the kind of comprehensive checks that have to happen, to cover all the bases we're covering, without a cache mechanism available at the Liquid tag level. Having said that, there's gotta be a way to make this better. I feel you. I'm looking. And I'm glad the theme inspector is help you find the pain points - massively useful tool.

In the meantime, when you identify a place where Locksmith's snippet is draining your performance and where it doesn't need to be there: add that theme asset to your Locksmith asset blacklist, available in your Locksmith account settings. Locksmith will strip itself out of that asset, when you do so. If you need to selectively re-add Locksmith to part of that asset, to protect something specific, use our guide on manual mode as a reference.

If you're still having trouble, yes, [email protected] is the place to start.

❤️❤️

@susanlangenes
Copy link

@isaacbowen that's very helpful! Thank you!

@susanlangenes
Copy link

susanlangenes commented Mar 4, 2020

@isaacbowen @wizardlyhel I have an update.

After adding quite a few filenames to the Liquid asset blacklist in Locksmith (Settings > Advanced), our PDP went from 4496ms to 802ms. Homepage went from 2877ms to 470ms, and a custom holiday page template went from 3473ms to 749ms.

This is a reasonable solution for my client because they typically only use Locksmith for early product releases to VIP groups where they lock a collection or product and give the VIP folks a direct link; there is never access through navigation or anywhere else on the site.

Isaac, and everyone, thank you for taking the time to discuss this and help sort it out. We are thrilled to be able to keep Locksmith, AND have a very snappy site now!

@isaacbowen
Copy link

@susanlangenes Phenomenal. Excellent work! 👏 Thanks for letting us know; happy this approach worked well for you. :)

@bakura10
Copy link

bakura10 commented May 30, 2020

While trying this tool to try to optimize our themes (started with our theme Warehouse), I figured out that actually, a lot of time was spent on... the header! It is true that our header has lot of feature, but it is one of the piece that is the most optimized in our code and where we cannot do a lot better in terms of Liquid code (each if/else break early, we only use simple condition...).

It seems the time is actually spent on building the dropdown menu (iterating through menu.links, link.links and sub_link.links). This code cannot be optimized further on our end, but this lead me to think about something:

  • if all the links within a menu point to, let's say, a collection. Even if we do not directly access the collection (through link.object), will Shopify still pre-load all the related data (so collection info, and first 50 products associated with the collection). If that's the case, that may indeed explain why an abnormaly high time is spent on rendering the menu.

If that is indeed the case, this means there would be a huge possible improvement to be done on Shopify end by making sure to not eagerly load all the associated data of a menu. In overall I found that when trying to optimize themes (especially for themes with lot of features like ours), one main issue is that we do not have a lot of info on what is Shopify doing "under the hood".

For instance if we have a link with a collection, does accessing the link will load the collection? If we iterate through the products of a collection, does accessing a property of the product will pre-load the variants... that kind of things! Having an advanced guide on how exactly the Liquid renderer works internally would help a lot.

@wizardlyhel
Copy link
Contributor Author

@bakura10 You are spot on with your points. At Shopify, we actually have no idea how external developers use liquid on themes and the different ways to work around the limitations of the platform.

The purpose of this Chrome extension is to help you better analysis what is happing on your theme from the server level so that you can find ways to optimize and gain that web performance that you need.

My hopes is that this knowledge is shared as we all aim to build for a better user experience.

If you find some performance issue with any liquid drops, let us know (even better if you explain how you test it). You are right about linklist object. It's slow and it is not easy to fix this problem.

@bakura10
Copy link

Thanks for confirming!

linklist was indeed my biggest surprise, I really didn't expect this to be an actual bottleneck in our theme's code. I suppose that we cannot do anything to optimize further this (it is not possible to hardcode the links in a theme). I hope there will be some improvement in that area :).

@bakura10
Copy link

Another two big bottleneck of our theme @wizardlyhel:

  • On our theme Warehouse, the collection page uses advanced filters (https://warehouse-theme-metal.myshopify.com/collections/all). Those are build by parsing tags (that must follow a structure like "Group_Value"). This parsing is extremely expensive and hardly optimizable. Ideally, I feel Shopify should offer a proper way to filter products (tags only are too limited, and the current approaches to make them powerful have a lot of performance implications).

  • Inventory: in Warehouse for instance, we show the total inventory per product on collection page (you can see it here: https://warehouse-theme-metal.myshopify.com/collections/all). The issue is that we have to go through each variant of every product to get the inventory, sum it up and show it. Looping through variants has a lot of performance impact, which could be somewhat avoided if Shopify would offer a new attribute like "product.inventory_quantity" that could allow to get the count without having to loop through each variants.

  • Color swatchs on collection page: this is another big one! On Warehouse for instance, we show color swatches, and when clicking on it, it automatically change the image to the variant featured image, and change the link (by appending the variant ID) so that you automatically get you to the proper variant. The issue is that it forces us to loop through the variants as well. I am exploring various approaches (such as loading the additional info in Ajax), but as said, the main issue is that we have little visibility on how exactly Liquid works internally and when data are actually accessed so that we can further optimize our theme. Having a detailed guide targeted to developers would help a lot!

Basically, I have figured out that as soon as you start accessing product variants on collection page, the impact on performance is high! I would like to be able to build the collection pages without having to access any variant variable at all, but there are many limitation for now that prevent me to do that.

@bakura10
Copy link

Another very odd thing I have found: getting the product.price has a super big impact on performance. I have tried to set up a collection with several products all containing 100 variants to stress test it.

Doing: {{product.title}} has nearly no impact on performance. However doing {{product.price}} triples the render time ! Not sure why, as it seems to be a product variable.

@PaulNewton
Copy link

@wizardlyhel will there be a wiki for this project or a separate place to make collaborative docs?

Until then,
Optimizing renders for linklist based logic:
If your managing a theme's perf directly(read not a theme distributor) and are rendering expensive content , like objects through linklists or complicated menus, and the underlying key structure|data doesn't change, consider just rendering out that output by itself and plugging it directly back into the theme as static content. This process can be similar to generating Critical CSS.

Alternatively skip it in the main content and load it into a blank layout with only that content and load it with ajax separately on demand.

Validating and Measuring Bottlenecks:
If you need to validate the source of a problem and how the problem behaves with an objects data being loaded|parsed by shopify make reduced test cases of those objects with different amounts of content. Example - large collections accessed through linklists just need few different collections with small, medium, and large amounts of products to reveal behavior.
The shopify-app-cli can be used to quickly populate example products,customers,&orders quickly either with the command (populate for node or populate with rails)

@PaulNewton
Copy link

PaulNewton commented May 31, 2020

@bakura10

Having an advanced guide on how exactly the Liquid renderer works internally would help a lot.

I agree with that partially but I get the sense with theme perf shopify reaaally wants to see use cases,partner feedback,docs, etc. or seeing what partners build ontop of it.
If that happens though the first topic I'd want to see is about the cache if there's more we can do to optimize for it or avoid getting bit by it.

Those are build by parsing tags (that must follow a structure like "Group_Value"). This parsing is extremely expensive and hardly optimizable

It's been a while since I touched the warehouse theme.
What method are you using to parse tags just a straight forloop?
Are you doing any prune checks {% if contains... ,etc?

For inventory a UX approach could be a much healthier option, or temporary boost.
Set a minimum threshold and when the loop count hits that {% break %} out and simply show an estimate 'more than 50 in stockℹ' then on the PDP show the actual amount.
Or also retrieve on lists when requested(more than 50 in stockℹ).
For merchant concerned about messaging make showing the full accurate count on lists something that needs a merchants opt-in, a setting, so the choice becomes the merchants to use up the perf-budget.

Color swatchs on collection page...The issue is that it forces us to loop through the variants as well

Isn't the liquid perf hit here because the theme fetches the entire page and not just the json endpoint?
Why not just stow the relevant swatch data values in data-attributes on the collection pages right from the start?

@bakura10
Copy link

bakura10 commented Jun 1, 2020

Hi @PaulNewton ,

Thank you for your replies.

  1. Of course, as a theme developer, neither the approach of hardcoding the menu nor loading it in Ajax for SEO reasons is a realistic workaround for the menu. I suppose that for the specific case of linklists, we will have no other choice than waiting that this is improved at Shopify level. I will pass the message to the custom team department though, as they may benefit from hardcoding links (although it honestly removes lot of flexibility for the merchant, so I am not sure a lot of merchant will agree with the fact they have to reach a development team whenever they want to edit a link in the navigation).

  2. For the inventory, this does not remove the fact that to calculate the "more than xx", you will need to iterate through variants. My experience is that as soon as you access the product.variants drop, the performance hit is incurred.

  3. For the color swatches, we are actually doing that in Liquid. Here is a short snippet:

{%- for option in product.options_with_values -%}
            {%- assign downcased_option = option.name | downcase -%}

            {%- if color_label contains downcased_option -%}
              {%- assign variant_option = 'option' | append: forloop.index -%}

              {%- for value in option.values -%}
                {%- assign downcased_value = value | downcase -%}

                {%- for variant in product.variants -%}
                  {%- if variant[variant_option] == value -%}
                    {%- assign variant_for_value = variant -%}
                    {%- break -%}
                  {%- endif -%}
                {%- endfor -%}

Basically, what I am looking to do is, for each color, getting the first associated variant with this color, so I can add attribute such as the variant ID, changing the tile image to the variant image...

But on collection pages that contains products with lot of variants, as soon as you access the product.variants performance are hit. With a collection page with 10 products each containing 100 variants, we already cross the 10s in render time.

I have tried an approach yesterday that avoids getitng at all the "product.variants" drop, by fetching it dynamically in Ajax whenever someone click on a swatch. There is of course a small delay incur, but this allowed to reduce the render time for 10s to 2s for those heavy use cases.

Which is why I suppose that accessing variants on collection page is something that should be avoided at all costs due to the performance impact. Does that make sense? :)

@susanlangenes
Copy link

susanlangenes commented Jun 1, 2020 via email

@wizardlyhel
Copy link
Contributor Author

Will there be a wiki for this project or a separate place to make collaborative docs?

That's an interesting idea

Having an advanced guide on how exactly the Liquid renderer works internally would help a lot.

Stay tune on this

product.price vs product.title

product.price is more query expansive than product.title since a product price is determine by the number of variants (if any) this product has. It's often not the issue of rendering on a product template but it is an issue rendering on a collection template. A collection template is a collective of products. So the query behind it would need to fetch for the prices for each product in a collection. Let's say a single product tile on a collection page took 20ms to render. When you iterate through 50 products on a single pagination, that is 1 full second of server rendering required.

This is where we need to balance out the responsibilities of where should the render happen. Can we optimize for both the server and client end? For example, instead of iterating all 50 products on a single pagination, reduce this to 10 (number of products you can see above fold), and ajax load the rest - optimizing for time to first byte and first render timing.

@bakura10
Copy link

bakura10 commented Jun 5, 2020

Interesting finding: product.featured_media is much more expensive than product.featured_image (about two times slower, which adds up on collection pages).

@Tom-SUS
Copy link

Tom-SUS commented Jun 6, 2020

Could someone explain to me how exactly this theme inspector tool can be utilized? I've downloaded the tool, accessed it through the Developer Tools under Inspect, but no cigar. It does not show up as an option. Any ideas on what's going wrong?

Note: I'm not a developer. Just interested in improving Shopify site speed overall for my clients.

image

@dan-gamble
Copy link

I'm not sure where best to ask this and I feel another issue might be off-topic for this but what are we aiming for in terms of render time? I've been aiming for ~1k or less but I have no idea if that is still crazy high or even too low.

Does anyone have any targets they try and strive for?

@wizardlyhel
Copy link
Contributor Author

@Tom-SUS You need to be a staff or a collaborator in order to run the profile. Since the Shopify tab isn't there, please make sure you are login with the Shopify icon in the top left of your screenshot.

@dan-gamble Not really off-topic. I personally aim for about 200ms - 500ms page render. This is based on a past guideline that Google set for a "good" time-to-first-byte (TTFB) target. While it is no longer a main metric that Google use to determine a performing website, it is a good developer metric to aim for

@Tom-SUS
Copy link

Tom-SUS commented Jun 8, 2020

Hey @wizardlyhel, still no avail. I've logged in and now it's showing me an error message.

I have this video for reference: https://www.loom.com/share/0169016289a54668a6d963a60c83f5ca

@dan-gamble
Copy link

@wizardlyhel thank you Helen. That's something much better to aim for, thank you :)

@astridchin
Copy link

mine is not a development store. but i am encountering the same problem as @Tom-SUS is having, just getting a "This page cannot be profiled" message..... :( any insight please @wizardlyhel ?

@astridchin
Copy link

mine is not a development store. but i am encountering the same problem as @Tom-SUS is having, just getting a "This page cannot be profiled" message..... :( any insight please @wizardlyhel ?

I have logged in as Store Owner and also as staff, both did not work.

@astridchin
Copy link

anyone any idea please?

mine is not a development store. but i am encountering the same problem as @Tom-SUS is having, just getting a "This page cannot be profiled" message..... :( any insight please @wizardlyhel ?

anyone any idea please?

@tehaksbrid
Copy link

Have continued working on this since this tool was released. Am finishing up a total rewrite of our main store's theme. Overall 90% load time reduction (as measured by DCL) on every page and device/network tier. This tool has been indispensable.

One mystery I've uncovered is anomalous document response times on pages with essentially no liquid. 200-800ms to respond with a 37kB document that consistently profiles to 20-30ms of liquid time.

Serving the same document with the same code from our dev Plus store gives a consistent 70-120ms response. Traffic? IDK. Any insight on factors that impact response time outside liquid, @wizardlyhel?

Sure would be nice to have a root service worker to bypass that TTFB entirely...😉😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests