Navigation options for SharePoint Online

This article describes how to improve page load times for SharePoint Online by using structural navigation and search-driven navigation.

Global navigation and the queries required to build the structural navigation can make your pages load more slowly in SharePoint Online. This is because each of these queries sends another request to SQL server. For each site and subsite that you have, more requests are made to SQL server. This issue also affects the master pages. This means that global navigation is also impacted.

Some SharePoint sites require large and complex structures. Using the out-of-the-box structural navigation, which uses content by query, can result in slow page load times due to multiple site layers. Each of the layers of subsites also creates another query.

There are two main out-of-the-box navigation options in SharePoint as well as a third, custom, search-driven approach. Each option has pros and cons as outlined in the following table.

Structural navigation

Managed navigation

Search-driven navigation


  • Easy to configure

  • Security-trimmed

  • Automatically updates as sites are added


  • Easy to maintain


  • Security-trimmed

  • Automatically updates as sites are added

  • Fast loading time and locally cached navigation structure


  • Can perform poorly with complex site structure


  • Not automatically updated to reflect site structure


  • No ability to easily order sites

  • Requires customization of the master page (technical skills required)

If you have a site with a lot of subsites and you are using structural navigation, it may be slowing your page loads down significantly. The most appropriate option for your site will depend on your site requirements and on your technical capability. If you are comfortable using a custom master page and have some capability in the organization to maintain the changes that may occur in the default master page for SharePoint Online, then the search-driven option will produce the best user experience. If you want a simple middle ground between the out-of-the-box structural navigation and search, then the managed navigation is a very good option. The managed navigation option can be maintained through configuration, does not involve code customization files, and it is significantly faster than the out-of-the-box structural navigation.

Another approach is to restructure the existing site and reduce the number of navigation items and subsites required. This is because structural navigation performs well as long as the site structure and navigation is not too complicated.

This article compares the various approaches in an example site collection. The example site collection has 11 subsites and each sub site has at least four additional subsites.

Screenshot showing sites and sub sites

Using structural navigation in SharePoint Online

This is the out-of-the-box navigation used by default and is the most straightforward and appropriate solution in most circumstances. Unless there is a complex structure of multiple subsites or many levels of subsites, structural navigation performs well. The main advantages of this approach are that it is security-trimmed, automatically updates when new sites are added and doesn’t require any customization of the master page. A non-technical user can also easily add items, hide items, and manage the navigation from the settings page.

Turning on structural navigation in SharePoint Online

To illustrate how the performance in a standard SharePoint Online solution with structural navigation and the show subsites option turned on. Below is a screen shot settings found on the page Site Settings > Navigation.

Screenshot showing subsites

Analyzing structural navigation performance in SharePoint Online

To analyze the performance of a SharePoint page use the Network tab of the F12 developer tools in Internet Explorer.

Screenshot showing F12 dev tools Network tab

On the Network tab, click on the .aspx page that is being loaded and then click on the Details tab.

Screenshot showing the details tab

Click Response headers.

Screenshot of Details tab

SharePoint returns some useful diagnostic information in its response headers. One of the most useful is SPRequestDuration    which is the value, in milliseconds, of how long a request took to process on the server.

In the following screen shot show subsites is unchecked for the structural navigation. This means that there is only the site collection link in the global navigation:

Screenshot showing load times as request duration

The SPRequestDuration    key has a value of 245 milliseconds. This represents the time it took to return the request. Since there is only one navigation item on the site, this is a good benchmark for how SharePoint Online performs without heavy navigation. The next screen shot shows how adding in the subsites affects this key.

Screenshot showing a request duration of 2502 ms

Adding the subsites has significantly increased the time it takes to return the page request.

The advantages of using the regular structured navigation is that you can easily organize the order, hide sites, add pages, the results are security-trimmed, and you are not deviating from the supported master pages used in SharePoint Online. If you structure your site carefully and minimize the amount of subsites in your site collection then structural navigation performs well.

Using managed navigation and managed metadata in SharePoint Online

Managed navigation is another out-of-the-box option that you can use to recreate the same sort of functionality as structural navigation.

The advantage of using managed metadata is that it is much faster to retrieve the data than using content by query to build the site navigation. Although it is much faster there is no way to security trim the results so if a user doesn’t have access to a given site, the link will still show but will lead to an error message.

How to implement managed navigation and the results   

There are several articles on TechNet about the details of managed navigation, for example, see Overview of managed navigation in SharePoint Server 2013.

In order to implement managed navigation, you need to have term store administrator permissions. By setting up terms with URLs that match the structure of a site collection, managed navigation can be used to replace structural navigation. For example:

Screenshot of Subsite1 example

The following example shows the performance of the complex navigation using managed navigation.

Screenshot of SPRequestDuration example

Using managed navigation consistently improves performance compared to the content by query structural navigation approach.

Using Search-driven client-side scripting

Using search you can leverage the indexes that are built up in the background using continuous crawl. This means there are no heavy content queries. The search results are pulled from the search index and the results are security-trimmed. This is faster than using regular content queries. Using search for structural navigation, especially if you have a complex site structure, will speed up page loading time considerably. The main advantage of this over managed navigation is that you benefit from security trimming.

This approach involves creating a custom master page and replacing the out-of-the-box navigation code with custom HTML. Follow this procedure to replace the navigation code in the file seattle.html.

In this example, you will open the seattle.html file and replace the whole element id=”DeltaTopNavigation” with the custom HTML code.

Example: To replace the out-of-the-box navigation code in a master page

  1. Navigate to the Site Settings page.

  2. Open the master page gallery by clicking Master Pages.

  3. From here you can navigate through the library and download the file seattle.master.

  4. Edit the code using a text editor and delete the code block in the following screen shot.

    Screenshot of DeltaTopNavigation code to delete
  5. Remove the code between the <SharePoint:AjaxDelta id=”DeltaTopNavigation”> and <\SharePoint:AjaxDelta> tags and replace it with the following snippet:

    <div id="loading">
      <!--Replace with path to loading image.-->
      <div style="background-image: url(''); height: 22px; width: 22px; ">
    <!-- Main Content-->
    <div id="navContainer" style="display:none">
        <div data-bind="foreach: hierarchy" class="noindex ms-core-listMenu-horizontalBox">
            <a class="dynamic menu-item ms-core-listMenu-item ms-displayInline ms-navedit-linkNode" data-bind="attr: { href: item.Url, title: item.Title }">
                <span class="menu-item-text" data-bind="text: item.Title">
            <ul id="menu" data-bind="foreach: $data.children" style="padding-left:20px">
                <li class="static dynamic-children">
                    <a class="static dynamic-children menu-item ms-core-listMenu-item ms-displayInline ms-navedit-linkNode" data-bind="attr: { href: item.Url, title: item.Title }">
                        <span aria-haspopup="true" class="additional-background ms-navedit-flyoutArrow dynamic-children">
                            <span class="menu-item-text" data-bind="text: item.Title">
                    <ul id="menu" data-bind="foreach: children; visible: children.length>0" class="dynamic" >
                        <li class="dynamic">
                            <a class="dynamic menu-item ms-core-listMenu-item ms-displayInline ms-navedit-linkNode" data-bind="attr: { href: item.Url, title: item.Title }">
                                <span class="menu-item-text" data-bind="text: item.Title">
  6. Replace the URL in the loading image anchor tag at the beginning, with a link to a loading image in your site collection. After you have made the changes, rename the file and then upload it to the master page gallery. This generates a new .master file.

  7. This HTML is the basic markup that will be populated by the search results returned from JavaScript code. You will need to edit the following code to change the value for the var root = “site collection URL as demonstrated in the following snippet:

    var root = “”;

    The entire JavaScript file is as follows:

    //Models and Namespaces
    var SPOCustom = SPOCustom || {};
    SPOCustom.Models = SPOCustom.Models || {}
    SPOCustom.Models.NavigationNode = function () {
        this.Url = ko.observable("");
        this.Title = ko.observable("");
        this.Parent = ko.observable("");
    var root = "";
    var baseUrl = root + "/_api/search/query?querytext=";
    var query = baseUrl + "'contentClass=\"STS_Web\"+path:" + root + "'&trimduplicates=false&rowlimit=300";
    var baseRequest = {
        url: "",
        type: ""
    //Parses a local object from JSON search result.
    function getNavigationFromDto(dto) {
        var item = new SPOCustom.Models.NavigationNode();
        if (dto != undefined) {
            var webTemplate = getSearchResultsValue(dto.Cells.results, 'WebTemplate');
            if (webTemplate != "APP") {
                item.Title(getSearchResultsValue(dto.Cells.results, 'Title')); //Key = Title
                item.Url(getSearchResultsValue(dto.Cells.results, 'Path')); //Key = Path
                item.Parent(getSearchResultsValue(dto.Cells.results, 'ParentLink')); //Key = ParentLink
        return item;
    function getSearchResultsValue(results, key) {
        for (i = 0; i < results.length; i++) {
            if (results[i].Key == key) {
                return results[i].Value;
        return null;
    //Parse a local object from the serialized cache.
    function getNavigationFromCache(dto) {
        var item = new SPOCustom.Models.NavigationNode();
        if (dto != undefined) {
        return item;
    /* create a new OData request for JSON response */
    function getRequest(endpoint) {
        var request = baseRequest;
        request.type = "GET";
        request.url = endpoint;
        request.headers = { ACCEPT: "application/json;odata=verbose" };
        return request;
    /* Navigation Module*/
    function NavigationViewModel() {
        "use strict";
        var self = this;
        self.nodes = ko.observableArray([]);
        self.hierarchy = ko.observableArray([]);;
        self.loadNavigatioNodes = function () {
            //Check local storage for cached navigation datasource.
            var fromStorage = localStorage["nodesCache"];
            if (false) {
                var cachedNodes = JSON.parse(localStorage["nodesCache"]);
                if (cachedNodes && timeStamp) {
                    //Check for cache expiration. Currently set to 3 hrs.
                    var now = new Date();
                    var diff = now.getTime() - timeStamp;
                    if (Math.round(diff / (1000 * 60 * 60)) < 3) {
                        //return from cache.
                        var cacheResults = [];
                        $.each(cachedNodes, function (i, item) {
                            var nodeitem = getNavigationFromCache(item, true);
            //No cache hit, REST call required.
        //Executes a REST call and builds the navigation hierarchy.
        self.queryRemoteInterface = function () {
            var oDataRequest = getRequest(query);
            $.ajax(oDataRequest).done(function (data) {
                var results = [];
                $.each(data.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results, function (i, item) {
                    if (i == 0) {
                        //Add root element.
                        var rootItem = new SPOCustom.Models.NavigationNode();
                    var navItem = getNavigationFromDto(item);
                //Add to local cache
                localStorage["nodesCache"] = ko.toJSON(results);
                localStorage["nodesCachedAt"] = new Date().getTime();
                if (self.nodes().length > 0) {
                    var unsortedArray = self.nodes();
                    var sortedArray = unsortedArray.sort(self.sortObjectsInArray);
            }).fail(function () {
                //Handle error here!!
        self.toggleView = function () {
            var navContainer = document.getElementById("navContainer");
            ko.applyBindings(self, navContainer);
        //Uses linq.js to build the navigation tree.
        self.buildHierarchy = function (enumerable) {
            self.hierarchy(Enumerable.From(enumerable).ByHierarchy(function (d) {
                return d.Parent() == null;
            }, function (parent, child) {
                if (parent.Url() == null || child.Parent() == null)
                    return false;
                return parent.Url().toUpperCase() == child.Parent().toUpperCase();
        self.sortChildren = function (parent) {
            // sjip processing if no children
            if (!parent || !parent.children || parent.children.length === 0) {
            parent.children = parent.children.sort(self.sortObjectsInArray2);
            for (var i = 0; i < parent.children.length; i++) {
                var elem = parent.children[i];
                if (elem.children && elem.children.length > 0) {
        // ByHierarchy method breaks the sorting in chrome and firefix 
        // we need to resort  as ascending
        self.sortObjectsInArray2 = function (a, b) {
            if (a.item.Title() > b.item.Title())
                return 1;
            if (a.item.Title() < b.item.Title())
                return -1;
            return 0;
        self.sortObjectsInArray = function (a, b) {
            if (a.Title() > b.Title())
                return -1;
            if (a.Title() < b.Title())
                return 1;
            return 0;
    //Loads the navigation on load and binds the event handlers for mouse interaction.
    function InitCustomNav() {
        var viewModel = new NavigationViewModel();
    function addEventsToElements() {
        $("li.dynamic-children").mouseover(function () {
            var position = $(this).position();
            $(this).find("ul").css({ width: 125, left: position.left + 10, top: 50 });
            .mouseout(function () {
                $(this).find("ul").css({ width: 0, left: -99999, top: 0 });

    To summarize the code shown above in the jQuery $(document).ready function there is a viewModel object created and then the loadNavigationNodes() function on that object is called. This function either loads the previously built navigation hierarchy stored in the HTML5 local storage of the client browser or it calls the function queryRemoteInterface().

    QueryRemoteInterface() builds a request using the getRequest() function with the query parameter defined earlier in the script and then returns data from the server. This data is essentially an array of all the sites in the site collection represented as data transfer objects with various properties. This data is then parsed into the previously defined SPO.Models.NavigationNode objects which use Knockout.js to create observable properties for use by data binding the values into the HTML that we defined earlier. The objects are then put into a results array. This array is parsed into JSON using Knockout and stored in the local browser storage for improved performance on future page loads.

  8. Next, the results are assigned to the self.nodes array and a hierarchy is built out of the objects using linq.js assigning the output to an array self.heirarchy. This array is the object that is bound to the HTML. This is done in the toggleView() function by passing the self object to the ko.applyBinding() function. This then causes the hierarchy array to be bound to the following HTML:

    <div data-bind=”foreach: hierarchy” class=”noindex ms-core-listMenu-horizontalBox”>

    Finally, the event handlers for mouseenter and mouseexit are added to the top-level navigation to handle the subsite drop-down menus which is done in the addEventsToElements() function.

    The results of the navigation can be seen in the screen shot below:

    Screenshot of navigation results

    In our complex navigation example a fresh page load without the local caching shows the time spent on the server has been cut down from the benchmark structural navigation to get a similar result as the managed navigation approach.

    Screenshot of SPRequestDuration 301

    One major benefit of this approach is that by using HTML5 local storage, the navigation is stored locally for the user the next time they load the page.

We get major performance improvements from using the search API for structural navigation; however, it takes some technical capability to execute and customize this functionality. In the example implementation, the sites are ordered in the same way as the out-of-the-box structural navigation; alphabetical order. If you wanted to deviate from this order, it would be more complicated to develop and maintain. Also, this approach requires you to deviate from the supported master pages. If the custom master page is not maintained, your site will miss out on updates and improvements that Microsoft makes to the master pages.

The above code has the following dependencies:

The current version of LinqJS does not contain the ByHierarchy method used in the above code and will break the navigation code. To fix this, add the following method to the Linq.js file before the line "Flatten: function ()".

ByHierarchy: function(firstLevel, connectBy, orderBy, ascending, parent) {
     ascending = ascending == undefined ? true : ascending;
     var orderMethod = ascending == true ? 'OrderBy' : 'OrderByDescending';
     var source = this;
     firstLevel = Utils.CreateLambda(firstLevel);
     connectBy = Utils.CreateLambda(connectBy);
     orderBy = Utils.CreateLambda(orderBy);
     //Initiate or increase level
     var level = parent === undefined ? 1 : parent.level + 1;

    return new Enumerable(function() {
         var enumerator;
         var index = 0;

        var createLevel = function() {
                 var obj = {
                     item: enumerator.Current(),
                     level : level
                 obj.children = Enumerable.From(source).ByHierarchy(firstLevel, connectBy, orderBy, ascending, obj);
                 if (orderBy !== undefined) {
                     obj.children = obj.children[orderMethod](function(d) {
                         return orderBy(d.item); //unwrap the actual item for sort to work
                 obj.children = obj.children.ToArray();
                 Enumerable.From(obj.children).ForEach(function(child) {
                     child.getParent = function() {
                         return obj;
                 return obj;

        return new IEnumerator(

        function() {
             enumerator = source.GetEnumerator();
         }, function() {
             while (enumerator.MoveNext()) {
                 var returnArr;
                 if (!parent) {
                     if (firstLevel(enumerator.Current(), index++)) {
                         return this.Yield(createLevel());

                } else {
                     if (connectBy(parent.item, enumerator.Current(), index++)) {
                         return this.Yield(createLevel());
             return false;
         }, function() {
Connect with an expert
Contact us
Expand your skills
Explore training

Was this information helpful?

Thank you for your feedback!

Thank you for your feedback! It sounds like it might be helpful to connect you to one of our Office support agents.