{"id":5506,"date":"2019-01-15T16:04:52","date_gmt":"2019-01-15T16:04:52","guid":{"rendered":"https:\/\/www.bronco.co.uk\/our-ideas\/?p=5506"},"modified":"2019-01-15T16:04:52","modified_gmt":"2019-01-15T16:04:52","slug":"a-tale-of-optimisation-using-craft-cms","status":"publish","type":"post","link":"https:\/\/www.bronco.co.uk\/our-ideas\/a-tale-of-optimisation-using-craft-cms\/","title":{"rendered":"A Tale of Optimisation using Craft CMS"},"content":{"rendered":"<p>As developers we have all been there, you have a site and people complain its slow.\u00a0 So where do you start?\u00a0 There are different levels of optimisation as well as various tools for front and back end optimisation. Some changes will have a big impact on performance, whilst others may not be really noticeable (unless you are interested in a performance score, for example}.<\/p>\n<p>We recently took over development of a Craft Commerce site (v2), which is never easy as you don\u2019t know the history of why something was done in a certain way.\u00a0 This is not a naming and shaming exercise, it\u2019s just an opportunity to run through some optimisation examples of what you can check and how to fix them.\u00a0 (A disclaimer here, this is just optimising existing code, not a reworking\/refactoring exercise).<\/p>\n<p>So overall the site wasn\u2019t actually performing that badly \u2013 most of the time.\u00a0 For the improvements we are going to look into a specific category page, this page loaded at around 700ms, so I know what you are thinking, what\u2019s the problem?\u00a0 The problem occurred when you tried to update products in the CMS. At this point the site slowed down considerably, we found out this was because it was heavily cached.<\/p>\n<p>When the cache had been cleared it rose to 3.81 seconds &#8211; so over three second slower on the live site.\u00a0 The site cache over a period of time would get bigger and bigger, so every time you needed to clear the cache it took longer and longer.\u00a0 At one point the caching table was at 1.6GB, about a third of the size of the whole database. This has subsequently changed to use Redis, but shows how cache can get out of control when left unmonitored.<\/p>\n<p>To get a clearer picture of what was happening, I removed all the caching elements throughout the site.\u00a0 Caching is not just a get out jail free card, it has overheads as well, not just in terms of getting the cached data but about clearing the cache as well.\u00a0 Here are a few code examples<\/p>\n<p>So, there were several instances of caching around just plain html, here is one example<\/p>\n<pre class=\"lang:default decode:true \">{% cache globally unless currentUser %}\r\n                 &lt;\/ul&gt;\r\n             &lt;\/div&gt;\r\n         &lt;\/nav&gt;\r\n     &lt;\/header&gt;\r\n{% endcache %}\r\n<\/pre>\n<p>In these cases, its actually more expensive to use the caching.<\/p>\n<p>The next example is a case of redundant code. This is found in the site\u2019s main navigation, which is cached globally &#8211; this means that it will only cache globally for the site local rather than one per URL.\u00a0 So this seems fair enough, however it\u2019s also getting the top-level categories and the sub categories, again fair enough, however the class the JavaScript relied on to display the sub navigation as a dropdown had been removed.\u00a0 In this case, the dropdown navigation was not required so I just removed the sub category navigation functionality and as previously mention removed the caching.\u00a0 It\u2019s now dynamic and won\u2019t be much difference in terms of performance as the cache version.<\/p>\n<pre class=\"lang:default decode:true \">{% cache globally %}\r\n    {% set productTypeList = craft.categories.group('productCategories').level(1) %}\r\n    {% for productType in productTypeList %}\r\n    {% set parentCat = craft.categories.slug(productType.slug).first() %}\r\n    {% set cat = productType.getDescendants(1) %}\r\n    \r\n    &lt;li class=\"text-center whole gapless {{ (cat|length) ? 'parent' : '' }}\" itemprop=\"name\"&gt;\r\n        &lt;a itemprop=\"url\" href=\"\/{{ productType.slug }}\" class=\"color-whiter no-margin-right no-padding-right whole gapless color-white-hover color-blue-bg-hover color-grey-15-border weight-semi color-black-75 slight-small no-underline-hover height-full align-center flex flex-row\"&gt;&lt;span class=\"whole gapless\"  itemprop=\"name\"&gt;{{ productType.title }}&lt;\/span&gt;&lt;\/a&gt;\r\n            {% if cat|length %}\r\n                &lt;div class=\"whole color-grey-15-bg pos-abs whole gapless shado hidden\" data-missing-class=\"dropdown\"&gt;\r\n                    &lt;div class=\"container\"&gt;\r\n                        &lt;p class=\"header-font color-black large weight-bold text-left\"&gt;Shop by {{ productType.title }}&lt;\/p&gt;\r\n                        &lt;ul class=\"children no-padding-total flex columns\"&gt;\r\n                            {% for children in cat %}\r\n                                &lt;li class=\"text-left\" itemprop=\"name\"&gt;\r\n                                    &lt;a itemprop=\"url\" href=\"{{ children.getUrl() }}\" class=\"text-left color-white color-white-hover color-blue-bg-hover color-grey-15-border weight-semi color-black-75 slight-small no-underline-hover height-full flex flex-row\"&gt;&lt;span class=\"whole gapless\"  itemprop=\"name\"&gt;{{ children.title}}&lt;\/span&gt;&lt;\/a&gt;\r\n                                &lt;\/li&gt;\r\n                            {% endfor %}\r\n                        &lt;\/ul&gt;\r\n                    &lt;\/div&gt;\r\n                &lt;\/div&gt;\r\n            {% endif %}\r\n        &lt;\/li&gt;\r\n    {% endfor %}\r\n{% endcache %}\r\n<\/pre>\n<p>It\u2019s worth reading and understanding fully how the cache works <a href=\"https:\/\/docs.craftcms.com\/v2\/templating\/cache.html\">https:\/\/docs.craftcms.com\/v2\/templating\/cache.html<\/a> before implementing it.\u00a0 Caching should not be used to mask underlying performance issues.\u00a0 It never ends well.\u00a0 You just end up moving the issue elsewhere in the site.<\/p>\n<h2>Debugging in Dev mode<\/h2>\n<p>So now the cache is removed the next thing I did was to enable debugging on the site to see what was actually going on.<\/p>\n<p>craft &gt; config &gt; general.php<\/p>\n<p>Enable devMode i.e.<\/p>\n<p>&#8216;devMode&#8217; =&gt; true<\/p>\n<p>One thing to remember is, when testing the load times again remember to disable the dev mode as it does slow down the page.\u00a0 More information about dev mode can be found here https:\/\/craftcms.com\/guides\/what-dev-mode-does<\/p>\n<p>Now when browsing the category page in the Console tab of developer tools, some debug information will magically appear.<\/p>\n<p><a href=\"https:\/\/www.bronco.co.uk\/our-ideas\/wp-content\/uploads\/2019\/01\/console-debug-mode.png\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-5517\" src=\"https:\/\/www.bronco.co.uk\/our-ideas\/wp-content\/uploads\/2019\/01\/console-debug-mode.png\" alt=\"\" width=\"1225\" height=\"369\" srcset=\"https:\/\/www.bronco.co.uk\/our-ideas\/wp-content\/uploads\/2019\/01\/console-debug-mode.png 1225w, https:\/\/www.bronco.co.uk\/our-ideas\/wp-content\/uploads\/2019\/01\/console-debug-mode-300x90.png 300w, https:\/\/www.bronco.co.uk\/our-ideas\/wp-content\/uploads\/2019\/01\/console-debug-mode-1080x325.png 1080w, https:\/\/www.bronco.co.uk\/our-ideas\/wp-content\/uploads\/2019\/01\/console-debug-mode-768x231.png 768w\" sizes=\"auto, (max-width: 1225px) 100vw, 1225px\" \/><\/a><\/p>\n<p>Under the Profiling Summary Report there is a few stats that we can have a look at.\u00a0 The one that stood out was the Total Queries, 248 of them.\u00a0 Now I know that off the shelf CMS\u2019s have overheads but 248 for 57 products in this case seemed excessive.\u00a0 I\u2019d also point out just because there is a large amount of database queries, it doesn\u2019t automatically mean there is something wrong or that it will slow down the site.<\/p>\n<p>The site templates are split into core components, the main template has a header and footer template and the category page just extends that.\u00a0 Commenting out the main template and checking the console again, it reduced it to 33, so that will be the various plugins and in-built queries for that page, but at least we know that the rest are in the templates themselves. Commenting out just the footer and a CTA block reduced the number of database calls by 22.<\/p>\n<p>At the bottom of each page there are some social links, these are editable in the CMS. Below is an example of one of the social links:<\/p>\n<pre class=\"lang:default decode:true \">{% if socialLinks.social[0] is defined and socialLinks.social[0].socialLink.url is not empty %}\r\n    &lt;li class=\"quarter flex align-center\"&gt;\r\n        &lt;a href=\"{{ socialLinks.social[0].socialLink.url }}\" {{ (socialLinks.social[0].socialLink.target|length)? ' rel=\"noopener\" target=\"_blank\"' : '' }} class=\"display-block whole flex align-center flex-cols\"&gt;\r\n            &lt;img class=\"whole\" src=\"\/assets\/imgs\/social\/fb.svg\" alt=\"Facebook\" \/&gt;\r\n        &lt;\/a&gt;\r\n    &lt;\/li&gt;\r\n{% endif %}\r\n<\/pre>\n<p>Personally, I think this is a bit over kill, I can\u2019t really see the need to ever update the social urls, if it did happen then it\u2019s not a big job to go into the template and manually change it.\u00a0 I didn\u2019t want to remove CMS functionality so I decided just to cache the whole footer which did include some navigation pulled in from the CMS.\u00a0 Strangely looking back at the history this section was never cached originally.<\/p>\n<h2>Eager loading<\/h2>\n<p>The bulk of the 248 queries were related to the actual category page itself, which makes sense.<\/p>\n<p>Looking through the code I found the following, which just grabs all the products from the category.\u00a0 It doesn\u2019t have a limit on it because looking through the code further it uses the data to create the filters on the category page.\u00a0 (Not the way I would do it, but rewriting the filters in not in the scope of this development phase)<\/p>\n<pre class=\"lang:default decode:true \">{% set products = craft.commerce.products.relatedTo(category).limit(null)  %}<\/pre>\n<p>Now looking at what data the filters need, it requires the category information, brand (which is also set up as categories) and images.\u00a0 Here comes eager loading to the rescue <a href=\"https:\/\/docs.craftcms.com\/v2\/templating\/eager-loading-elements.html\">https:\/\/docs.craftcms.com\/v2\/templating\/eager-loading-elements.html<\/a><\/p>\n<pre class=\"lang:default decode:true \">{% set products = craft.commerce.products.relatedTo(category).with(['mainImage','productCategories','productBrand','variants']).limit(null)  %}<\/pre>\n<p>Then changing a few other lines like this because it\u2019s changed from an object to and array<\/p>\n<pre class=\"lang:default decode:true\">{% set image = product.mainImage.first() %}<\/pre>\n<p>to<\/p>\n<pre class=\"lang:default decode:true \">{% set image = product.mainImage[0] %}<\/pre>\n<p>Checking the console again, the number of queries went from 248 to 78.\u00a0 Not bad for changing a few lines of code.<\/p>\n<p>I did the same with other templates throughout the site, some had bigger savings than others but all worthwhile doing.<\/p>\n<h2>Imager<\/h2>\n<p>A couple of things to mention here, this site is using the imager version one with imagick, so may differ slightly if using version two or using gd.<\/p>\n<p>The next thing to look at was images.\u00a0 The site uses imager <a href=\"https:\/\/github.com\/aelvan\/Imager-Craft\">https:\/\/github.com\/aelvan\/Imager-Craft<\/a>,\u00a0 and looking through the code there was a lot of transformations going on.\u00a0 Some were related to an SEO plugin, but looking through the homepage code there was the following options set up for imager<\/p>\n<pre class=\"lang:default decode:true \">[\r\n    { width: 1920, height: 1080, jpegQuality: 100 },\r\n    { width: 1650, height: 442, jpegQuality: 100 },\r\n    { width: 1200, height: 321, jpegQuality: 90 },\r\n    { width: 1000, height: 268, jpegQuality: 90  },\r\n    { width: 800, height: 339, jpegQuality: 90  },\r\n    { width: 600, height: 254, jpegQuality: 90  },\r\n    { width: 400, height: 170, jpegQuality: 90  },\r\n    { width: 300, height: 140, jpegQuality: 90  }\r\n]\r\n<\/pre>\n<p>That\u2019s eight images for the homepage scrolling banner, and there are three banners in total (at this point in time).\u00a0 Every time a new banner gets uploaded in the cms it has to resize it eight times.\u00a0 In this case, what is the advantage of serving an image 300 and 400 pixels wide?\u00a0 It can be a big drain on server resources to resize images on the server.\u00a0 You can adjust things like the max execution time memory limit but you need to remember the consequences.\u00a0 I think the lesson here is just to be sensible with the number of resized images.<\/p>\n<p>Other things to look at is the config for imager, this can be found in<\/p>\n<p>craft\/config\/imager.php<\/p>\n<p>Some defaults had been changed already<\/p>\n<pre class=\"lang:default decode:true \">return [\r\n  'cacheDuration' =&gt; 31536000,\r\n  'resizeFilter' =&gt; 'hermite'\r\n];<\/pre>\n<p>The cacheDuration is a year, there is no point resizing images every 14 days if there is no need to, in this case the images are not going to change that often if ever.<\/p>\n<p>The resizeFilter is the filter used by imagick, all the different options can be found here <a href=\"https:\/\/github.com\/aelvan\/Imager-Craft#resizefilter-string\">https:\/\/github.com\/aelvan\/Imager-Craft#resizefilter-string<\/a> with the different performances, its all a case of weighing up performance vs quality of image.<\/p>\n<p>Other things to consider<\/p>\n<p>removeMetadata \u2013 If you are using imagick then setting this to true will strip meta data from the image resulting in smaller file size<\/p>\n<p>jpegoptimEnabled\/optipngEnabled &#8211; If you have jpegoptim and\/or optipng installed then you can enable them by setting these options to true, by default these will be run as a job rather than at runtime.\u00a0 However just like the resizing images on the server, optimising images on the server can also take up lots of server resources.\u00a0 Again, just be sensible.<\/p>\n<p>WebP \u2013 Imager also supports saving images in webp format, \u00a0<a href=\"https:\/\/github.com\/aelvan\/Imager-Craft#usecwebp-bool\">https:\/\/github.com\/aelvan\/Imager-Craft#usecwebp-bool<\/a>\u00a0 big savings can be made in terms of image size, you can see some examples here <a href=\"https:\/\/developers.google.com\/speed\/webp\/gallery1\">https:\/\/developers.google.com\/speed\/webp\/gallery1<\/a> There are two options with this, if your imagick is compiled with webp then it will just work (you can check by having a look at your php info and under ImageMagick supported formats it will say webp if its supported), otherwise you can set useCwebp to true and that will use cwebp command line tool (you will need it installed <a href=\"https:\/\/developers.google.com\/speed\/webp\/docs\/compiling#unix\">https:\/\/developers.google.com\/speed\/webp\/docs\/compiling#unix<\/a>) which is slightly slower.<\/p>\n<h2>In conclusion<\/h2>\n<p>It maybe took a day or two to complete all the changes including testing, but it was well worth it.\u00a0 It shows what can be done, in this case with some very small tweaks.\u00a0 Sometimes you may be able to find the issues quite quickly and get some quick wins like the ones I have mentioned, other times you won\u2019t be able to.\u00a0\u00a0 I think the main thing to take from this is don\u2019t just turn to cache to fix issues, there is nothing wrong with cache when used correctly but caching to mask issues will just cause headaches down the line.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>As developers we have all been there, you have a site and people complain its slow.\u00a0 So where do you start?\u00a0 There are different levels of optimisation as well as various tools for front and back end optimisation. Some changes will have a big impact on performance, whilst others may not be really noticeable (unless [&hellip;]<\/p>\n","protected":false},"author":24,"featured_media":5512,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[2],"class_list":["post-5506","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-web-and-ux"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.bronco.co.uk\/our-ideas\/wp-json\/wp\/v2\/posts\/5506","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.bronco.co.uk\/our-ideas\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.bronco.co.uk\/our-ideas\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.bronco.co.uk\/our-ideas\/wp-json\/wp\/v2\/users\/24"}],"replies":[{"embeddable":true,"href":"https:\/\/www.bronco.co.uk\/our-ideas\/wp-json\/wp\/v2\/comments?post=5506"}],"version-history":[{"count":0,"href":"https:\/\/www.bronco.co.uk\/our-ideas\/wp-json\/wp\/v2\/posts\/5506\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.bronco.co.uk\/our-ideas\/wp-json\/wp\/v2\/media\/5512"}],"wp:attachment":[{"href":"https:\/\/www.bronco.co.uk\/our-ideas\/wp-json\/wp\/v2\/media?parent=5506"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.bronco.co.uk\/our-ideas\/wp-json\/wp\/v2\/categories?post=5506"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}