Skip to main content

This site requires you to update your browser. Your browsing experience maybe affected by not having the most up to date version.

Template Questions /

Menu Slowing Load Times - SOLVED WITH PARTIAL CACHING


Reply


8 Posts   1274 Views

Avatar
davepolyester

Community Member, 48 Posts

11 October 2012 at 5:22pm

Hi all,

I've built a site (in SS 2.4.7) for a client to use as a knowledgebase and, as such, it contains a vast number of pages with much more hierarchical depth than your average site.

We've been having speed issues which may be a result of hosting on Dreamhost, but I've noticed that the menu system seems to affect the page load speed greatly.
A page with the menu switched on takes about 6-8 seconds to load, but the same page with the menu switched off takes about 2 seconds.
I'm concerned this means each page load is having to dig the whole content of every page out of the database so it can populate the menu with all the Titles of every sub page!

The menu is a fairly standard 'Suckerfish' style flyout, with about six levels of depth, and I've attached my template code for you to check out. Please let me know if you spot anything dodgy in there that could help.

I wonder if there might be a way to delay the populating of deeper levels of the menu until someone actually hovers over the menu?

Any advice on dealing with this kind of site in SS will be much appreciated.

BTW, am getting YSlow scores of about 83-94 so the site overall should be pretty speedy I think.

Cheers!

Attached Files
Avatar
kaanuni

Community Member, 22 Posts

13 October 2012 at 12:19am

Edited: 13/10/2012 12:42am

First of all if there is a will there is a way. So yes you can 'delay the populating deeper levels of the menu until someone actually hovers over the menu'. It would however be complicated. You would need to use ajax/json. It appears you are using css for the menu except for old browsers. So what you would do is load the first level of the menu and add a onmouseover="loading_function($li_id)" property to the <li>'s or use jQuery to the same effect (note whatever way you do it you will have to add id properties to the menu items). Next you will need to define loading_function in a javascript file so that it reads a url on your server to get the sub menu items. And finally you will have to create a new template and page type for this url, possibly with a custom controller so that it returns one level of menus in json format.

However, besides not being the easiest solution this is not the best solution either, since it won't work for browsers that have disabled javascript. There are other options:

1- Overload the getMenu method of your controller and use custom SQL to fetch only the columns you need. Or write a new function like getSimpleMenu (probably a better idea).
2- Write a getEntireMenu method in your controller that uses custom SQL to fetch the relevant columns for site tree for the entire menu and output it. This will allow you to optimize the code for your menu depth and will result in less function calls. However you will either have to include HTML in your controller (which is icky) or output javascript (which is not only equally icky, but also sucks for SEO).
3- Use partial or total caching, read: http://doc.silverstripe.org/framework/en/reference/partial-caching http://doc.silverstripe.org/framework/en/2.4/reference/partial-caching

Method #1 is not terrible and you can do it along side #3 for even better performance. Method #2 is a bad idea in that it requires a lot of effort and any gains come at the expense of breaching the mvc model. #3 is quicker to implement and will likely give the largest performance boost on average.

In this instance you would wrap your menu with this:

<% cached 'navigation', List(Page).max(LastEdited) %> ***
<% cached 'navigation', Aggregate(Page).Max(LastEdited) %>
<% end_cached %>

*** The struck out line is for 3.0, my bad.

Now I haven't tried this, but according to the page at the link I posted, the section wrapped in this will be cached until any page on the site is edited. So if after you are done making edits you refresh a page on your site with the navigation, your visitors will always get a cached copy of the menu.

Alternatively you can cache entire pages. You can read up on that here: http://doc.silverstripe.org/framework/en/reference/staticpublisher
http://doc.silverstripe.org/framework/en/2.4/reference/staticpublisher

Avatar
davepolyester

Community Member, 48 Posts

15 October 2012 at 9:20am

Thanks Kaanumi for your fantastic response. Great to get such excellent detail around possible solutions, their pros and cons, and how they'll work rather than just a simple "Do this..." with no explanation.

I agree with you that the caching option should be the best method. I certainly don't want to introduce any ajax or other javascript to my menus, I much prefer lean, mean CSS styled versions.

I'll take a look at the links in your posts and come back with any questions and the results from my implementation.

Thanks once again!

Avatar
davepolyester

Community Member, 48 Posts

15 October 2012 at 4:46pm

Success! Partial-caching is now my best friend! By caching my menus I've reduced page load times from 8-10 seconds down to 2-5 seconds.
Ran into a few gotchas though:

1) If a <% require %> tag is inside the <% cache %> tag, it ceases to be called once the content is cached. I had a CSS file in this situation and my menu would completely restyle itself once it was cached, defaulting to another CSS file further up the chain.

2) I can't use the same menu for the Secure and Public sections of the site. If I do, the public end up seeing the Secure pages listed in their menu, though they can't access them of course.

3) I've had to tweak my CSS as, for some reason, the a.Section and a.Current tags seem to cache sometimes too, resulting in the menu continuing to display a previously visited page as the Current page. This seems a bit intermittent so I'm confused, but I've simply disabled my styling for Current pages so it won't affect end users.

Thanks once again Kaanumi for pointing me towards the partial-caching feature - it has saved Silverstripe from being dumped by the client on this project!

Avatar
Willr

Forum Moderator, 5513 Posts

24 October 2012 at 8:36pm

2) Use the CurrentMember flag as part of your cache key.

3) Add ID as part of the cache key

<% cached 'Menu', Aggregate(SiteTree).Max(LastEdited), ID, CurrentMember.ID %>

Avatar
davepolyester

Community Member, 48 Posts

20 November 2012 at 11:17am

Thanks for the CurrentMember.ID tip Will, that should fix the anomalies for me. Unfortnately I'm still getting some issues:

If I log in with one user, then log out and log back in with a second user who has different page view permissions, the second user still sees the menu as it appeared to the first user, including items that should be restricted. (If they click the items they are told they don't have access.)

Is there something else I need to do to force the menu to render according the current user's page access rights?

Could it be caused by the fact that the cached menus are all in Includes, not directly in any page template?

Thanks again.

Avatar
lx

Community Member, 83 Posts

20 May 2014 at 8:07am

Edited: 20/05/2014 8:11am

I would like to address this topic once gain.
For me, SS generates too many queries to build the navigation.

Lets take this example template

<nav class="primary">
   <ul>
<% loop $Menu(1) %>
         <li class="$LinkingMode"><a href="$Link" title="$Title.XML">$MenuTitle.XML</a>
<% if $Children %>
<ul>
<% loop $Children %>
<li class="$LinkingMode"><a href="$Link" title="$Title.XML">$MenuTitle.XML</a>
<% end_loop %>
</ul>
<% end_if %>
</li>
<% end_loop %>
   </ul>
</nav>

I didn't do any SS for at least one year, but i think this is correct :)
It creates only a 2-level-navigation.

  • Home
  • Products
    • Product 1
    • Product 2

  • About Us
    • Our dog
    • Our Cat

  • Contact Us
    • Contact Me
    • Contact my dog

This little sitetree of 10 pages already needs 12 database queries to be created. (use ?showqueries=1 to see all queries)
So with big websites the number of needed queries can become really high.

Somehow, adding partial caching feels like a patch for the code that has to generate the navigation.

I am wondering if it could be possible to write a function that just makes a single query to get all pages and then build a tree of them in php.

Any other thoughts on this ?

Avatar
HARVS1789UK

Community Member, 21 Posts

31 May 2014 at 9:01am

@Willr can you provide any information on @DavePolyester's 1st point:

1) If a <% require %> tag is inside the <% cache %> tag, it ceases to be called once the content is cached.

I have, what I assume to be, a similar issue where I am trying to cache 2 x WidgetAreas which (optionally) appear on every page of my site. I am using the below cache key:

<% cached 'WidgetAreas', ID, List(Page).max(LastEdited) %>

I understand this should create one new cache file for the enclosed markup for each page on my site due to the use of 'ID' in my cache key. Note that you may expect me to just use 'LastEdited' for the second part but I have implemented an 'Inherit Widget Area From Parent' feature meaning widgets changing on any of this pages parents could potentially mean my cache needs invalidating.

The problem I am having is that my widgets CSS files are no longer included when my cached version is used, I don't have any <% require %> tags within my <% cache %> tag but my widgets CSS is included via the widget controllers init() method() i.e.:

public function init() {
parent::init();
Requirements::css('widgets_CallToActionWidget/css/CallToActionWidget.css');
}

I imagine that this is the same issue as raised above, and that moving the require logic into the widget template, rather than its controller class, would have the exact same result.

Can you offer any insight as to why content pulled in via the Requirements class is excluded from cached content? and more importantly for me, how I can fix it in order to cache my WidgetAreas?

Thanks in advance,

HARVS1789UK