6 Easy Tips to improve the performance of a SilverStripe Site

We have decided to share some insights on how Innovative Codes speeds up websites using the SilverStripe CMS

6 Easy Tips to improve the performance of a SilverStripe Site

Silverstripe as a CMS is not very popular however unlike other Content Management Systems out there it enables the developers of websites to do more with a framework rather then the simple "query the database and present it to the user model"... It puts the average Joe to the side and empowers the developer to be more efficient and neat in his / her work with a very structured model that is daunting as is flexible.

With the brief overview of SilverStripe over let's dive in making these websites faster...

As an example we're going to start with this code snippet:

<% loop Page.filter('ShowInPage','IndexPage').sort('LastEdited','DESC').limit(50) %>
    <% loop BannerImages.limit(3) %>
        <img src="$Image.CroppedImage(50,50).Link" alt="$Up.Title" title="$Up.MenuTitle" />
    <% end_loop %>
<% end_loop %>

How can we improve this piece of code?

 

Partial Caching

As a first flaw to this code I can see that we are not using partial caching which means that SilverStripe needs to process all this code on EVERY page load. So before we implement Partial Caching let's analyze how much data we are requesting from the database and how we can reduce that, here's a small breakdown:

  1. Get 50 rows from the database where it matches our criteria ( 1 Query )
  2. Loop over those 50 rows of data and get the BannerImages ( 50 Queries )
  3. For every BannerImages DataList loop 3 times and get the image object ( 150 queries )

With this simple sum we can conclude that we are already doing 201 queries to generate this part of the website and that is not factoring in that we need to process 150 images through compression & resizing, WOW!

So let's go ahead and cache this puppy, but before running in and caching the whole block think about of what could change so that we can generate the cache key off of that. From this example I can deduce that Pages have a one-to-many relationship with BannerImages therefore these have their own unique tables so we need to consider both tables for our cache-key.

function getBannerImagesCacheKey(){
    $page_last_change = strtotime( Page::get()->filter('ShowInPage','IndexPage')->sort('LastEdited','DESC')->First()->LastEdited );
    $banners_last_change = strtotime( BannerImage::get()->sort('LastEdited','DESC')->First()->LastEdited ); 
    return 'banner_key'.$page_last_change.$banners_last_change;
}

and we'll wrap our code block in <% cached $BannerImagesCacheKey %>
Note: This is not perfect and can be improved a little bit further however to keep this simple it will do the job just fine.

With this technique we have dropped from 201 queries to just 2 and completely eliminated the image processing unless the pages or banner images change of course.

 

Extend Classes rather then just creating your own

From the code snippet in the top of the page I can safely assume that the BannerImage DataObject looks something like this:

<?php
    class BannerImage extends DataObject{
        ...
        private static $has_one = array(
            'Image' => 'Image'
        );
        ...
    }

This means that every time this developer wants to show the image in this DataObject the system needs to first fetch the BannerImages and then fetch the Image itself. This type of logic makes a lot of sense if we used it for example in an Article Page where each article has it's own image + a lot of very specific characteristics, but in this case we could just extends Image and add our logic to it like so:

<?php
    class BannerImage extends Image{
        // Your amazing logic and extra stuff goes here...
    }

With the setup we can completely skip the step where we need to query Image because once we call BannerImage we already have all the file data we need for that Image and therefore our code snippet would change like so:

<% loop BannerImages.limit(3) %>
    <img src="$CroppedImage(50,50).Link" alt="$Up.Title" title="$Up.MenuTitle" />
<% end_loop %>

We have just saved ourselves 50 queries! Wolla!

 

Store html on client web browser

At lot of security professionals will be screaming when they see this title as they know very well that browser data can be manipulated however this doesn't stop us from caching stuff on client systems. In the banner example we cannot really benefit from this technique however let's take this example:

<% if CurrentUser %>
    <% if hasRecentlyShopped %>
         <% include RecentlyShoppedMessage %>
    <% end_if %>
<% end_if %>

This looks very efficient doesn't it? we just have 2 if statements and a simple include no bigieee BUT we can improve that...

  1. hasRecentlyShopped can be stored in a Session which is usually way faster then the Database and if manipulated wouldn't do any harm here
  2. Even the include can be stored in a Session!
    Since there is a big chance that the message is highly personalized it would be quite hard to cache it on the server however we can take that whole chunk of html and give it to the user to keep like so:

    In PHP:

    public function getRecentlyShoppedMessage(){
        if( !Cookie::get( 'RecentlyShoppedMessage' ) ){
            Cookie::set( 'RecentlyShoppedMessage', $this->generateRecentlyShoppedMessage() );
        }
        return Cookie::get( 'RecentlyShoppedMessage' );
    }

    In Template:
    $RecentlyShoppedMessage instead of <% include RecentlyShoppedMessage %>
    

    Note:
     The generateRecentlyShoppedMessage function should return an HTML String

 

We did it again! 1 less query and completely avoided parsing a template from the second page view onwards

 

Know when to decorate and when to extend

In most cases when you extend a class you are telling SilverStripe "Hey this is my new class, it has all properties of my old class and more" and SilverStripe will go ahead and create a new table for the new class with a shared Primary Key between both classes. Neat engineering! However this comes at a very big cost! We now need to perform joins to get the data of the new class and as most of you probably know... Joins are a performance nightmare. Silverstripe does a pretty efficient joins however the bottle neck will still exist.

On the other hand a decorator will tell Silverstripe to add the data to the old class table therefore removing join however we still have the problem that large tables take a long time to query.

 

So when it comes to choosing if to decorate or to extend try to find the right balance so that tables don't get too big or end up with a lot of joins.

 

Use Indexes!

For some reasons indexes are not very popular in SilverStripe and they can improve your websites by huge margins especially when working with quite big tables. I wown't go in details on how to setup indexes as they vary a lot among different setups however head to the SilverStripe website to learn how to set them up and start using them ASAP!

Note: SilverStripe will generate indexes for foreign key relationship so you don't need to setup those.

 

Use Caching Systems

There really isn't much to explain here go ahead and make sure APC is installed on your server, default config is all you need to see a speedup in the execution of your php.