From 457dce36515d454228b7504bfd57ab92131c6dad Mon Sep 17 00:00:00 2001
From: Peter Mosses <18308236+pdmosses@users.noreply.github.com>
Date: Mon, 12 Sep 2022 05:31:17 +0200
Subject: [PATCH] Improve build time of navigation panel (#956)

Fix #863.

The current Liquid code to generate the navigation panel involves the inefficient extraction of a list of pages from a list of page groups (identified by @captn3m0 in his original [explanation of the performance issue](https://github.com/just-the-docs/just-the-docs/issues/863)).

The optimisation implemented by this PR generates navigation links directly from the list of page groups, thereby avoiding the extraction of a list of pages from it. The Liquid code is now a bit tedious, but I don't see a simpler solution.

The need for grouping pages arises because Jekyll doesn't provide a filter to sort a list of pages on the value of an arbitrary expression.

Using Jekyll v4.2.2 (macOS 12.5, M2 MacBook Air, 16 GB memory), building https://github.com/endoflife-date/endoflife.date using https://github.com/pdmosses/just-the-docs/blob/fix-nav-performance/_includes/nav.html produced the following profile extract:

Filename                                                    | Count |    Bytes |   Time
------------------------------------------------------------|-------|----------|-------
| just-the-docs-0.4.0.rc1/_layouts/default.html                    |   130 |  3792.04K |  5.160 |
| _includes/nav.html                                               |   130 |  1405.20K |  4.054 |
| just-the-docs-0.4.0.rc1/_includes/head.html                      |   130 |   617.82K |  0.495 |
| _layouts/product.html                                            |   127 |  1014.38K |  0.413 |
| _includes/head_custom.html                                       |   130 |   427.83K |  0.393 |
| assets/js/zzzz-search-data.json                                  |     1 |   149.31K |  0.050 |

@nathancarter has tried adding the new `nav.html` to [a site with over 300 pages](https://nathancarter.github.io/how2data/site/), and reported that it improved the build time of more than 3 minutes to about 30 seconds.

Further optimisation of navigation might be possible (e.g., using [Jekyll include caching](https://github.com/benbalter/jekyll-include-cache)), but the current optimisation should be sufficient for v0.4.0.

To test that this PR does not appear to affect the navigation panel generated by v0.3.3:

1. Clone https://github.com/just-the-docs/just-the-docs-tests.
2. Update `_config.yml` and `Gemfile` to use this PR branch.
3. Run `bundle update`.
4. Inspect the rendering of the entire collection of navigation tests.

(Many of the differences reported in the GitHub visualisation of the changes are due to shifting much of the code 2 spaces to the left, in connection with moving the first `ul` element to be close to its first item.)
---
 _includes/nav.html | 175 ++++++++++++++++++++++++++++-----------------
 1 file changed, 108 insertions(+), 67 deletions(-)

diff --git a/_includes/nav.html b/_includes/nav.html
index 9d322dc3..53838bc9 100644
--- a/_includes/nav.html
+++ b/_includes/nav.html
@@ -1,63 +1,71 @@
-<ul class="nav-list">
-  {%- assign titled_pages = include.pages
-        | where_exp:"item", "item.title != nil" -%}
+{%- comment -%}
+  Pages with no `title` are implicitly excluded from the navigation.
+  
+  The values of `title` and `nav_order` can be numbers or strings.
+  Jekyll gives build failures when sorting on mixtures of different types,
+  so numbers and strings need to be sorted separately.
 
-  {%- comment -%}
-    The values of `title` and `nav_order` can be numbers or strings.
-    Jekyll gives build failures when sorting on mixtures of different types,
-    so numbers and strings need to be sorted separately.
+  Here, numbers are sorted by their values, and come before all strings.
+  An omitted `nav_order` value is equivalent to the page's `title` value
+  (except that a numerical `title` value is treated as a string).
 
-    Here, numbers are sorted by their values, and come before all strings.
-    An omitted `nav_order` value is equivalent to the page's `title` value
-    (except that a numerical `title` value is treated as a string).
+  The case-sensitivity of string sorting is determined by `site.nav_sort`.
+{%- endcomment -%}
 
-    The case-sensitivity of string sorting is determined by `site.nav_sort`.
-  {%- endcomment -%}
-  
-  {%- assign string_ordered_pages = titled_pages
-        | where_exp:"item", "item.nav_order == nil" -%}
-  {%- assign nav_ordered_pages = titled_pages
-        | where_exp:"item", "item.nav_order != nil"  -%}
-
-  {%- comment -%}
-    The nav_ordered_pages have to be added to number_ordered_pages and
-    string_ordered_pages, depending on the nav_order value.
-    The first character of the jsonify result is `"` only for strings.
-  {%- endcomment -%}
-  {%- assign nav_ordered_groups = nav_ordered_pages
-        | group_by_exp:"item", "item.nav_order | jsonify | slice: 0" -%}
-  {%- assign number_ordered_pages = "" | split:"X" -%}
-  {%- for group in nav_ordered_groups -%}
-    {%- if group.name == '"' -%}
-      {%- assign string_ordered_pages = string_ordered_pages | concat: group.items -%}
-    {%- else -%}
-      {%- assign number_ordered_pages = number_ordered_pages | concat: group.items -%}
-    {%- endif -%}
-  {%- endfor -%}
-  
-  {%- assign sorted_number_ordered_pages = number_ordered_pages | sort:"nav_order" -%}
+{%- assign titled_pages = include.pages
+      | where_exp: "item", "item.title != nil" -%}
+
+{%- assign string_ordered_pages = titled_pages
+      | where_exp: "item", "item.nav_order == nil" -%}
+{%- assign nav_ordered_pages = titled_pages
+      | where_exp: "item", "item.nav_order != nil" -%}
+
+{%- comment -%}
+  Add the nav-ordered pages to the number-ordered pages or the string-ordered pages,
+  depending on their `nav_order` value.
   
-  {%- comment -%}
-    The string_ordered_pages have to be sorted by nav_order, and otherwise title
-    (where appending the empty string to a numeric title converts it to a string).
-    After grouping them by those values, the groups are sorted, then the items
-    of each group are concatenated.
-  {%- endcomment -%}
-  {%- assign string_ordered_groups = string_ordered_pages
-        | group_by_exp:"item", "item.nav_order | default: item.title | append:''" -%}
-  {%- if site.nav_sort == 'case_insensitive' -%}
-    {%- assign sorted_string_ordered_groups = string_ordered_groups | sort_natural:"name" -%}
+  The first character of the `jsonify` result is `"` only for strings.
+{%- endcomment -%}
+
+{%- assign nav_ordered_groups = nav_ordered_pages
+      | group_by_exp: "item", "item.nav_order | jsonify | slice: 0" -%}
+
+{%- assign number_ordered_pages = "" | split: "" -%}
+{%- for group in nav_ordered_groups -%}
+  {%- if group.name == '"' -%}
+    {%- assign string_ordered_pages = string_ordered_pages | concat: group.items -%}
   {%- else -%}
-    {%- assign sorted_string_ordered_groups = string_ordered_groups | sort:"name" -%}
+    {%- assign number_ordered_pages = number_ordered_pages | concat: group.items -%}
   {%- endif -%}
-  {%- assign sorted_string_ordered_pages = "" | split:"X" -%}
-  {%- for group in sorted_string_ordered_groups -%}
-    {%- assign sorted_string_ordered_pages = sorted_string_ordered_pages | concat: group.items -%}
-  {%- endfor -%}
+{%- endfor -%}
+
+{%- assign sorted_number_ordered_groups = number_ordered_pages
+      | sort: "nav_order" | group_by: "nav_order" -%}
 
-  {%- assign pages_list = sorted_number_ordered_pages | concat: sorted_string_ordered_pages -%}
+{%- comment -%}
+  Group the string-ordered pages by `nav_order`, if non-nil, and otherwise `title`
+  (but appending the empty string to a numeric title to convert it to a string).
   
-  {%- for node in pages_list -%}
+  Then sort the groups according to the site setting for case (in)sensitivity.
+{%- endcomment -%}
+
+{%- assign string_ordered_groups = string_ordered_pages
+      | group_by_exp:"item", "item.nav_order | default: item.title | append: '' " -%}
+
+{%- if site.nav_sort == 'case_insensitive' -%}
+  {%- assign sorted_string_ordered_groups = string_ordered_groups
+        | sort_natural: "name" -%}
+{%- else -%}
+  {%- assign sorted_string_ordered_groups = string_ordered_groups
+        | sort:"name" -%}
+{%- endif -%}
+
+{%- assign groups_list = sorted_number_ordered_groups
+      | concat: sorted_string_ordered_groups -%}
+
+<ul class="nav-list">
+  {%- for node_group in groups_list -%} 
+  {%- for node in node_group.items -%}
     {%- if node.parent == nil -%}
       {%- unless node.nav_exclude -%}
       <li class="nav-list-item{% if page.collection == include.key and page.url == node.url or page.parent == node.title or page.grand_parent == node.title %} active{% endif %}">
@@ -68,10 +76,15 @@
         {%- endif -%}
         <a href="{{ node.url | relative_url }}" class="nav-list-link{% if page.url == node.url %} active{% endif %}">{{ node.title }}</a>
         {%- if node.has_children -%}
+          {%- assign children_list = "" | split: "" -%}
+          {%- for parent_group in groups_list -%}
+            {%- assign children_list = children_list 
+                  | concat: parent_group.items
+                  | where: "parent", node.title
+                  | where_exp:"item", "item.grand_parent == nil" -%}
+          {%- endfor -%}
           {%- if node.child_nav_order == 'desc' -%}
-            {%- assign children_list = pages_list | where: "parent", node.title | where_exp:"item", "item.grand_parent == nil" | reverse -%}
-          {%- else -%}
-            {%- assign children_list = pages_list | where: "parent", node.title | where_exp:"item", "item.grand_parent == nil" -%}
+            {%- assign children_list = children_list | reverse -%}
           {%- endif -%}
           <ul class="nav-list ">
           {%- for child in children_list -%}
@@ -84,16 +97,21 @@
               {%- endif -%}
               <a href="{{ child.url | relative_url }}" class="nav-list-link{% if page.url == child.url %} active{% endif %}">{{ child.title }}</a>
               {%- if child.has_children -%}
+                {%- assign grandchildren_list = "" | split: "" -%}
+                {%- for grandparent_group in groups_list -%}
+                  {%- assign grandchildren_list = grandchildren_list
+                        | concat: grandparent_group.items
+                        | where: "parent", child.title
+                        | where: "grand_parent", node.title -%}
+                {%- endfor -%}
                 {%- if node.child_nav_order == 'desc' -%}
-                {%- assign grand_children_list = pages_list | where: "parent", child.title | where: "grand_parent", node.title | reverse -%}
-                {%- else -%}
-                {%- assign grand_children_list = pages_list | where: "parent", child.title | where: "grand_parent", node.title -%}
+                  {%- assign grandchildren_list = grandchildren_list | reverse -%}
                 {%- endif -%}
                 <ul class="nav-list">
-                {%- for grand_child in grand_children_list -%}
-                  {%- unless grand_child.nav_exclude -%}
-                  <li class="nav-list-item {% if page.url == grand_child.url %} active{% endif %}">
-                    <a href="{{ grand_child.url | relative_url }}" class="nav-list-link{% if page.url == grand_child.url %} active{% endif %}">{{ grand_child.title }}</a>
+                {%- for grandchild in grandchildren_list -%}
+                  {%- unless grandchild.nav_exclude -%}
+                  <li class="nav-list-item {% if page.url == grandchild.url %} active{% endif %}">
+                    <a href="{{ grandchild.url | relative_url }}" class="nav-list-link{% if page.url == grand_child.url %} active{% endif %}">{{ grandchild.title }}</a>
                   </li>
                   {%- endunless -%}
                 {%- endfor -%}
@@ -108,6 +126,7 @@
       {%- endunless -%}
     {%- endif -%}
   {%- endfor -%}
+  {%- endfor -%}
   {%- assign nav_external_links = site.nav_external_links -%}
   {%- for node in nav_external_links -%}
     <li class="nav-list-item external">
@@ -121,16 +140,28 @@
 
 {%- if page.collection == include.key -%}
 
-  {%- for node in pages_list -%}
+  {%- for node_group in groups_list -%}
+  {%- for node in node_group.items -%}
     {%- if node.parent == nil -%}
-      {%- if page.grand_parent == node.title or page.parent == node.title and page.grand_parent == nil -%}
+      {%- if page.grand_parent == node.title 
+              or page.parent == node.title
+              and page.grand_parent == nil -%}
         {%- assign first_level_url = node.url | relative_url -%}
       {%- endif -%}
       {%- if node.has_children -%}
-        {%- assign children_list = pages_list | where: "parent", node.title -%}
+        {%- assign children_list = "" | split: "" -%}
+        {%- for parent_group in groups_list -%}
+          {%- assign children_list = children_list | concat: 
+                parent_group.items | where: "parent", node.title -%}
+        {%- endfor -%}
+        {%- if node.child_nav_order == 'desc' -%}
+          {%- assign children_list = children_list | reverse -%}
+        {%- endif -%}
         {%- for child in children_list -%}
           {%- if child.has_children -%}
-            {%- if page.url == child.url or page.parent == child.title and page.grand_parent == child.parent -%}
+            {%- if page.url == child.url
+                    or page.parent == child.title
+                    and page.grand_parent == child.parent -%}
               {%- assign second_level_url = child.url | relative_url -%}
             {%- endif -%}
           {%- endif -%}
@@ -138,9 +169,19 @@
       {%- endif -%}
     {%- endif -%}
   {%- endfor -%}
+  {%- endfor -%}
 
   {% if page.has_children == true and page.has_toc != false %}
-    {%- assign toc_list = pages_list | where: "parent", page.title | where: "grand_parent", page.parent -%}
+    {%- assign toc_list = "" | split: "" -%}
+    {%- for parent_group in groups_list -%}
+      {%- assign toc_list = toc_list
+            | concat: parent_group.items
+            | where: "parent", page.title
+            | where: "grand_parent", page.parent -%}
+    {%- endfor -%}
+    {%- if node.child_nav_order == 'desc' -%}
+      {%- assign toc_list = toc_list | reverse -%}
+    {%- endif -%}
   {%- endif -%}
 
 {%- endif -%}
-- 
GitLab