Skip to content

Instantly share code, notes, and snippets.

@jacksleight
Last active January 27, 2022 17:49
Show Gist options
  • Save jacksleight/467af74557bbf029cbd43cd3092f11f2 to your computer and use it in GitHub Desktop.
Save jacksleight/467af74557bbf029cbd43cd3092f11f2 to your computer and use it in GitHub Desktop.
Statamic Nav Performance Experiment

I'm seeing some significant performance issues with the nav tag, so have been experimenting with a custom version.

In my tests there are two main issue with the current nav tag:

  1. It fully augments all data in every entry.
  2. It makes multiple calls to the $page object's various URL methods, to resolve is_current etc. these calls also slow things down quite a bit for me (I haven't yet looked into exactly why).

This custom tag is the code from the nav and structure tags, modified slightly to work around these issues. I have made the following changes:

  1. The entire $page object is no longer augmented, instead I'm only augmenting the title key.
  2. Instead of multiple calls to $page->url() I'm making just one and including the return value in the data (for use in the view)
  3. For now I have removed is_parent and is_external as I'm not using them, I may add a flag to toggle them on only when needed.
  4. Breadcrumbs has also been modified (see below)

In my tests with a nav containing 50 items using this tag adds ~80ms. Using the default tag adds ~670ms.

Enabling is_parent and is_external adds another ~100ms.

Usage is mostly identical to the default nav tag, except:

  1. Entry data needs to be accessed via the entry key
  2. Breadcrumbs works differently, it now returns data structured in a similar way to the main nav method (see also: statamic/ideas#537)
<?php
namespace App\Tags;
use Statamic\Tags\Tags;
use Statamic\Contracts\Structures\Structure as StructureContract;
use Statamic\Facades\Site;
use Statamic\Facades\URL;
use Statamic\Structures\TreeBuilder;
use Statamic\Facades\Data;
use Statamic\Support\Str;
class CustomNav extends Tags
{
public function wildcard($tag)
{
$handle = $this->context->value($tag, $tag);
// Allow {{ structure:collection:pages }} rather than needing to use the double colon.
if (is_string($handle)) {
$handle = str_replace(':', '::', $tag);
}
return $this->structure($handle);
}
public function index()
{
return $this->structure($this->params->get('handle', 'collection::pages'));
}
protected function structure($handle)
{
if ($handle instanceof StructureContract) {
$handle = $handle->handle();
}
$tree = (new TreeBuilder)->build([
'structure' => $handle,
'include_home' => $this->params->get('include_home'),
'show_unpublished' => $this->params->get('show_unpublished', false),
'site' => $this->params->get('site', Site::current()->handle()),
'from' => $this->params->get('from'),
'max_depth' => $this->params->get('max_depth'),
]);
return $this->toArray($tree);
}
public function toArray($tree, $parent = null, $depth = 1)
{
return collect($tree)->map(function ($item) use ($parent, $depth) {
$page = $item['page'];
$url = $page->url();
$data = [
'title' => $page->augmentedValue('title'),
'url' => $url,
];
$children = empty($item['children']) ? [] : $this->toArray($item['children'], $data, $depth + 1);
return array_merge($data, [
'children' => $children,
'parent' => $parent,
'depth' => $depth,
'is_current' => rtrim(URL::getCurrent(), '/') == rtrim($url, '/'),
'is_parent' => null,
'is_external' => null,
// 'is_parent' => Site::current()->url() === $url ? false : URL::isAncestorOf(URL::getCurrent(), $page->urlWithoutRedirect()),
// 'is_external' => URL::isExternal($page->absoluteUrl()),
]);
})->filter()->values()->all();
}
public function breadcrumbs()
{
$currentUrl = URL::makeAbsolute(URL::getCurrent());
$url = Str::removeLeft($currentUrl, Site::current()->absoluteUrl());
$url = Str::ensureLeft($url, '/');
$segments = explode('/', $url);
$segments[0] = '/';
if (! $this->params->bool('include_home', true)) {
array_shift($segments);
}
$crumbs = collect($segments)->map(function () use (&$segments) {
$uri = URL::tidy(join('/', $segments));
array_pop($segments);
return $uri;
})->mapWithKeys(function ($uri) {
$uri = Str::ensureLeft($uri, '/');
return [$uri => Data::findByUri($uri, Site::current()->handle())];
})->filter();
if (! $this->params->bool('reverse', false)) {
$crumbs = $crumbs->reverse();
}
if ($this->params->bool('trim', true)) {
$this->content = trim($this->content);
}
return $crumbs->values()->map(function ($crumb) {
$url = $crumb->url();
return [
'entry' => $crumb,
'url' => $url,
'is_current' => URL::getCurrent() === $url,
];
})->all();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment